Statistics Import and Export

Started by Corey Huinkerover 2 years ago555 messages
#1Corey Huinker
corey.huinker@gmail.com
1 attachment(s)

pg_stats_export is a view that aggregates pg_statistic data by relation
oid and stores all of the column statistical data in a system-indepdent
(i.e.
no oids, collation information removed, all MCV values rendered as text)
jsonb format, along with the relation's relname, reltuples, and relpages
from pg_class, as well as the schemaname from pg_namespace.

pg_import_rel_stats is a function which takes a relation oid,
server_version_num, num_tuples, num_pages, and a column_stats jsonb in
a format matching that of pg_stats_export, and applies that data to
the specified pg_class and pg_statistics rows for the relation
specified.

The most common use-case for such a function is in upgrades and
dump/restore, wherein the upgrade process would capture the output of
pg_stats_export into a regular table, perform the upgrade, and then
join that data to the existing pg_class rows, updating statistics to be
a close approximation of what they were just prior to the upgrade. The
hope is that these statistics are better than the early stages of
--analyze-in-stages and can be applied faster, thus reducing system
downtime.

The values applied to pg_class are done inline, which is to say
non-transactionally. The values applied to pg_statitics are applied
transactionally, as if an ANALYZE operation was reading from a
cheat-sheet.

This function and view will need to be followed up with corresponding
ones for pg_stastitic_ext and pg_stastitic_ext_data, and while we would
likely never backport the import functions, we can have user programs
do the same work as the export views such that statistics can be brought
forward from versions as far back as there is jsonb to store it.

While the primary purpose of the import function(s) are to reduce downtime
during an upgrade, it is not hard to see that they could also be used to
facilitate tuning and development operations, asking questions like "how
might
this query plan change if this table has 1000x rows in it?", without
actually
putting those rows into the table.

Attachments:

v1-0001-Introduce-the-system-view-pg_stats_export-and-the.patchtext/x-patch; charset=US-ASCII; name=v1-0001-Introduce-the-system-view-pg_stats_export-and-the.patchDownload
From 834a415f594b3662716c9728a2ab46e425e80e82 Mon Sep 17 00:00:00 2001
From: coreyhuinker <corey.huinker@gmail.com>
Date: Thu, 31 Aug 2023 01:30:57 -0400
Subject: [PATCH v1] Introduce the system view pg_stats_export and the function
 pg_import_rel_stats.

pg_stats_export is a view that aggregates pg_statistic data by relation
oid and stores all of the column statistical data in a system-indepdent (i.e.
no oids) jsonb format, along with the relation's relname, reltuples, and
relpages from pg_class, as well as the schemaname from pg_namespace.

pg_import_rel_stats is a function which takes a relation oid,
server_version_num, num_tuples, num_pages, and a column_stats jsonb in
a format matching that of pg_stats_export, and applies that data to
the specified pg_class and pg_statistics rows for the relation
specified.

The most common use-case for such a function is in upgrades and
dump/restore, wherein the upgrade process would capture the output of
pg_stats_export into a regular table, perform the upgrade, and then
join that data to the existing pg_class rows, updating statistics to be
a close approximation of what they were just prior to the upgrade. The
hope is that these statistics are better than the early stages of
--analyze-in-stages and can be applied faster, thus reducing system
downtime.

The values applied to pg_class are done inline, which is to say
non-transactionally. The values applied to pg_statitics are applied
transactionally, as if an ANALYZE operation was reading from a
cheat-sheet.

The statistics applied are no more durable than any other, and will
likely be overwritten by the next autovacuum analyze.
---
 src/include/catalog/pg_proc.dat      |   5 +
 src/include/commands/vacuum.h        |   3 +
 src/backend/catalog/system_views.sql |  92 +++++
 src/backend/commands/analyze.c       | 581 +++++++++++++++++++++++++++
 src/test/regress/expected/rules.out  |  47 +++
 src/test/regress/expected/vacuum.out |  95 +++++
 src/test/regress/sql/vacuum.sql      |  83 ++++
 doc/src/sgml/func.sgml               |  44 +-
 doc/src/sgml/system-views.sgml       |   5 +
 9 files changed, 954 insertions(+), 1 deletion(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 9805bc6118..48c662c93c 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5636,6 +5636,11 @@
   proname => 'pg_stat_get_db_checksum_last_failure', provolatile => 's',
   proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
   prosrc => 'pg_stat_get_db_checksum_last_failure' },
+{ oid => '3813',
+  descr => 'statistics: import to relation',
+  proname => 'pg_import_rel_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'bool', proargtypes => 'oid int4 float4 int4 jsonb',
+  prosrc => 'pg_import_rel_stats' },
 { oid => '3074', descr => 'statistics: last reset for a database',
   proname => 'pg_stat_get_db_stat_reset_time', provolatile => 's',
   proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 4af02940c5..3ef05fa8a1 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -386,4 +386,7 @@ extern double anl_random_fract(void);
 extern double anl_init_selection_state(int n);
 extern double anl_get_next_S(double t, int n, double *stateptr);
 
+extern Datum pg_import_rel_stats(PG_FUNCTION_ARGS);
+
+
 #endif							/* VACUUM_H */
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 77b06e2a7a..37383be786 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -253,6 +253,98 @@ CREATE VIEW pg_stats WITH (security_barrier) AS
 
 REVOKE ALL ON pg_statistic FROM public;
 
+CREATE VIEW pg_statistic_export WITH (security_barrier) AS
+    SELECT
+        n.nspname AS schemaname,
+        r.relname AS relname,
+        current_setting('server_version_num')::integer AS server_version_num,
+        r.reltuples::float4 AS n_tuples,
+        r.relpages::integer AS n_pages,
+        (
+            SELECT
+                jsonb_object_agg(a.attname,
+                                 (
+                                    SELECT
+                                        jsonb_object_agg(attsta.inherit_type,
+                                                         attsta.stat_obj)
+                                    FROM (
+                                        SELECT
+                                            CASE s.stainherit
+                                                WHEN TRUE THEN 'inherited'
+                                                ELSE 'regular'
+                                            END AS inherit_type,
+                                            jsonb_build_object(
+                                                'attstattarget',
+                                                CASE
+                                                    WHEN a.attstattarget >= 0 THEN a.attstattarget
+                                                    ELSE current_setting('default_statistics_target')::int4
+                                                END,
+                                                'stanullfrac', s.stanullfrac::text,
+                                                'stawidth', s.stawidth::text,
+                                                'stadistinct', s.stadistinct::text,
+                                                'stakind1', s.stakind1::text,
+                                                'stakind2', s.stakind2::text,
+                                                'stakind3', s.stakind3::text,
+                                                'stakind4', s.stakind4::text,
+                                                'stakind5', s.stakind5::text,
+                                                'staop1',
+                                                CASE s.stakind1
+                                                    WHEN 0 THEN '0'
+                                                    WHEN 1 THEN '='
+                                                    ELSE '<'
+                                                END,
+                                                'staop2',
+                                                CASE s.stakind2
+                                                    WHEN 0 THEN '0'
+                                                    WHEN 1 THEN '='
+                                                    ELSE '<'
+                                                END,
+                                                'staop3',
+                                                CASE s.stakind3
+                                                    WHEN 0 THEN '0'
+                                                    WHEN 1 THEN '='
+                                                    ELSE '<'
+                                                END,
+                                                'staop4',
+                                                CASE s.stakind4
+                                                    WHEN 0 THEN '0'
+                                                    WHEN 1 THEN '='
+                                                    ELSE '<'
+                                                END,
+                                                'staop5',
+                                                CASE s.stakind5
+                                                    WHEN 0 THEN '0'
+                                                    WHEN 1 THEN '='
+                                                    ELSE '<'
+                                                END,
+                                                'stanumbers1', s.stanumbers1::text[],
+                                                'stanumbers2', s.stanumbers2::text[],
+                                                'stanumbers3', s.stanumbers3::text[],
+                                                'stanumbers4', s.stanumbers4::text[],
+                                                'stanumbers5', s.stanumbers5::text[],
+                                                /* casting these to text makes re-casting easier */
+                                                'stavalues1', s.stavalues1::text::text[],
+                                                'stavalues2', s.stavalues2::text::text[],
+                                                'stavalues3', s.stavalues3::text::text[],
+                                                'stavalues4', s.stavalues4::text::text[],
+                                                'stavalues5', s.stavalues5::text::text[]
+                                                    ) AS stat_obj
+                                        FROM pg_statistic AS s
+                                        WHERE s.starelid = a.attrelid
+                                        AND s.staattnum = a.attnum
+                                        ) AS attsta
+                                )
+                                ORDER BY a.attnum)
+            FROM pg_attribute AS a
+            WHERE a.attrelid = r.oid
+            AND NOT a.attisdropped
+            AND has_column_privilege(r.oid, a.attnum, 'SELECT')
+            AND a.attnum > 0
+        ) AS columns
+    FROM pg_class AS r
+    JOIN pg_namespace AS n
+        ON n.oid = r.relnamespace;
+
 CREATE VIEW pg_stats_ext WITH (security_barrier) AS
     SELECT cn.nspname AS schemaname,
            c.relname AS tablename,
diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c
index bfd981aa3f..426eb19990 100644
--- a/src/backend/commands/analyze.c
+++ b/src/backend/commands/analyze.c
@@ -18,6 +18,7 @@
 
 #include "access/detoast.h"
 #include "access/genam.h"
+#include "access/heapam.h"
 #include "access/multixact.h"
 #include "access/relation.h"
 #include "access/sysattr.h"
@@ -40,6 +41,7 @@
 #include "commands/vacuum.h"
 #include "common/pg_prng.h"
 #include "executor/executor.h"
+#include "fmgr.h"
 #include "foreign/fdwapi.h"
 #include "miscadmin.h"
 #include "nodes/nodeFuncs.h"
@@ -59,14 +61,17 @@
 #include "utils/datum.h"
 #include "utils/fmgroids.h"
 #include "utils/guc.h"
+#include "utils/jsonb.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
+#include "utils/numeric.h"
 #include "utils/pg_rusage.h"
 #include "utils/sampling.h"
 #include "utils/sortsupport.h"
 #include "utils/spccache.h"
 #include "utils/syscache.h"
 #include "utils/timestamp.h"
+#include "utils/typcache.h"
 
 
 /* Per-index data for ANALYZE */
@@ -3056,3 +3061,579 @@ analyze_mcv_list(int *mcv_counts,
 	}
 	return num_mcv;
 }
+
+/*
+ * Get a JsonbValue from a JsonbContainer
+ */
+static
+JsonbValue *key_lookup(JsonbContainer *cont, const char *key)
+{
+	return getKeyJsonValueFromContainer(cont, key, strlen(key), NULL);
+}
+
+/*
+ * Get a JsonbValue from a JsonbContainer and ensure that it is a string
+ */
+static
+JsonbValue *key_lookup_string(JsonbContainer *cont, const char *key)
+{
+	JsonbValue *j = key_lookup(cont,key);
+
+	if (j == NULL)
+		return NULL;
+
+	if (j->type != jbvString)
+	{
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("invalid statistics format, %s must be a string",key)));
+	}
+
+	return j;
+}
+
+/*
+ * Get a JsonbContainer from a JsonbContainer and ensure that it is a object
+ */
+static
+JsonbContainer *key_lookup_object(JsonbContainer *cont, const char *key)
+{
+	JsonbValue *j = key_lookup(cont,key);
+
+	if (j == NULL)
+		return NULL;
+
+	if ((j->type != jbvBinary) || (!JsonContainerIsObject(j->val.binary.data)))
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("invalid statistics format, %s must be a binary object", key)));
+
+	return j->val.binary.data;
+}
+
+static
+bool jbvIsBinaryArray(JsonbValue *j)
+{
+	return (j->type == jbvBinary) && JsonContainerIsArray(j->val.binary.data);
+}
+
+static
+char *jbvstr_to_cstring(JsonbValue *j)
+{
+	char *s;
+
+	Assert(j->type == jbvString);
+	/* make a string we are sure is null-terminated */
+	s = palloc(j->val.string.len + 1);
+	memcpy(s, j->val.string.val, j->val.string.len);
+	s[j->val.string.len] = '\0';
+	return s;
+}
+
+static
+Datum jbvstr_to_float4datum(JsonbValue *j)
+{
+	char * s = jbvstr_to_cstring(j);
+	Datum result = DirectFunctionCall1(float4in, CStringGetDatum(s));
+	pfree(s);
+	return result;
+}
+
+static
+Datum jbvstr_to_int32datum(JsonbValue *j)
+{
+	char * s = jbvstr_to_cstring(j);
+	Datum result = DirectFunctionCall1(int4in, CStringGetDatum(s));
+	pfree(s);
+	return result;
+}
+
+static
+Datum jbvstr_to_int16datum(JsonbValue *j)
+{
+	char * s = jbvstr_to_cstring(j);
+	Datum result = DirectFunctionCall1(int2in, CStringGetDatum(s));
+	pfree(s);
+	return result;
+}
+
+static
+Datum jbvstr_to_attrtypedatum(JsonbValue *j, FmgrInfo *finfo, Oid input_func, Oid typioparam, int32 typmod)
+{
+	char * s = jbvstr_to_cstring(j);
+	Datum result = InputFunctionCall(finfo, s, typioparam, typmod);
+	pfree(s);
+	return result;
+}
+
+static
+void import_pg_statistic_rows(Oid relid, Relation sd, TupleDesc tupleDesc,
+							  Form_pg_attribute attr,
+							  FmgrInfo *finfo, Oid input_func,
+							  Oid typioparams, Oid eq_opr, Oid lt_opr,
+							  CatalogIndexState indstate,
+							  JsonbContainer *cont, const char *name, bool inh)
+{
+
+	HeapTuple	stup,
+				tup;
+	int			i,
+				j,
+				k;
+	Datum		values[Natts_pg_statistic];
+	bool		nulls[Natts_pg_statistic];
+	bool		replaces[Natts_pg_statistic];
+	bool		newrow = true;
+	JsonbValue *jv;
+
+	/* Is there already a pg_statistic tuple for this attribute? */
+	tup = SearchSysCache3(STATRELATTINH,
+						  ObjectIdGetDatum(relid),
+						  Int16GetDatum(attr->attnum),
+						  BoolGetDatum(inh));
+
+	if (HeapTupleIsValid(tup))
+	{
+		/* use the tuple that already exists */
+		newrow = false;
+		heap_deform_tuple(tup, tupleDesc, values, nulls);
+		for (i = 0; i < Natts_pg_statistic; ++i)
+			replaces[i] = false;
+	}
+	else
+	{
+		/* new row */
+		for (i = 0; i < Natts_pg_statistic; ++i)
+		{
+			nulls[i] = false;
+			replaces[i] = true;
+		}
+
+		values[Anum_pg_statistic_starelid - 1] = relid;
+		values[Anum_pg_statistic_staattnum - 1] = attr->attnum;
+		values[Anum_pg_statistic_stainherit - 1] = BoolGetDatum(inh);
+	}
+
+	i =	Anum_pg_statistic_stanullfrac - 1;
+	jv = key_lookup_string(cont, "stanullfrac");
+	if (jv != NULL)
+	{
+		values[i] = jbvstr_to_float4datum(jv);
+		replaces[i] = true;
+	}
+	else if (newrow)
+		values[i] = Float4GetDatum(0.0);
+
+	i = Anum_pg_statistic_stawidth - 1;
+	jv = key_lookup_string(cont, "stawidth");
+	if (jv != NULL)
+	{
+		values[i] = jbvstr_to_int32datum(jv);
+		replaces[i] = true;
+	}
+	else if (newrow)
+		values[i] = Int32GetDatum(0);
+
+	i = Anum_pg_statistic_stadistinct - 1;
+	jv = key_lookup_string(cont, "stadistinct");
+	if (jv != NULL)
+	{
+		values[i] = jbvstr_to_float4datum(jv);
+		replaces[i] = true;
+	}
+	else if (newrow)
+		values[i] = Float4GetDatum(0.0);
+
+	i = Anum_pg_statistic_stakind1 - 1;
+	for (k = 1; k <= STATISTIC_NUM_SLOTS; k++)
+	{
+		char key[20];
+		sprintf(key, "stakind%d", k);
+		jv = key_lookup_string(cont, key);
+		if (jv != NULL)
+		{
+			values[i] = jbvstr_to_int16datum(jv);
+			replaces[i] = true;
+		}
+		else if (newrow)
+			values[i] = Int16GetDatum(0);
+
+		i++;
+	}
+
+
+	/*
+	 * set staopN rows. Use staopN as a proxy for when to set stacollN
+	 *
+	 * collation cannot be changed in stats, only attempt to set if this is a
+	 * new row, and set it to the attcollation - it is possible that if this
+	 * column is an expression on an index, then the collation could be
+	 * different but this will be reset anyway on the next autoanalyze.
+	 */
+	i = Anum_pg_statistic_staop1 - 1;
+	j = Anum_pg_statistic_stacoll1 - 1;
+	for (k = 1; k <= STATISTIC_NUM_SLOTS; k++)
+	{
+		char key[20];
+		Oid res_opr;
+		sprintf(key, "staop%d", k);
+		jv = key_lookup(cont, key);
+		if (jv != NULL)
+		{
+			if (jv->type != jbvString)
+				ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("invalid format: %s must be one of: '=', '<', ''", key)));
+
+			if (jv->val.string.len != 1)
+				ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("invalid format: %s must be one of: '=', '<', ''", key)));
+
+			if (jv->val.string.val[0] == '0')
+				res_opr = 0;
+			else if (jv->val.string.val[0] == '=')
+				res_opr = eq_opr;
+			else if (jv->val.string.val[0] == '<')
+				res_opr = lt_opr;
+			else
+				ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("invalid format: %s must be one of: '=', '<', ''", key)));
+
+			values[i] = ObjectIdGetDatum(res_opr);
+			replaces[i] = true;
+			/* staopN was set, so set stacollN if this row is new */
+			if (newrow)
+			{
+				if (res_opr != 0)
+					values[j] = ObjectIdGetDatum(attr->attcollation);
+				else
+					values[j] = ObjectIdGetDatum(0);
+			}
+		}
+		else if (newrow)
+		{
+			values[i] = ObjectIdGetDatum(0);
+			values[j] = ObjectIdGetDatum(0);
+		}
+
+		i++;
+		j++;
+	}
+
+	for (k = 1; k <= STATISTIC_NUM_SLOTS; k++)
+	{
+		char		key[20];
+		int			num_elements;
+		Datum	   *numdatums;
+		ArrayType  *arry;
+		int			n;
+
+		sprintf(key, "stanumbers%d", k);
+
+		/* compute offset to allow for continue bailouts */
+		i = Anum_pg_statistic_stanumbers1 - 2 + k;
+
+		jv = key_lookup(cont, key);
+
+		if (jv == NULL)
+		{
+			/* no key set, do not modify existing row value */
+			if (newrow)
+			{
+				nulls[i] = true;
+				values[i] = (Datum) 0;
+			}
+			continue;
+		}
+
+		/* can be null or a binary array */
+		if (jv->type == jbvNull)
+		{
+			/* explicitly set valuesN null */
+			nulls[i] = true;
+			values[i] = (Datum) 0;
+			replaces[i] = true;
+			continue;
+		}
+
+		if (!jbvIsBinaryArray(jv))
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("invalid statistics format, %s must be a array or null", key)));
+
+		num_elements = JsonContainerSize(jv->val.binary.data);
+
+		if (num_elements == 0)
+		{
+			/* empty array is just null */
+			nulls[i] = true;
+			values[i] = (Datum) 0;
+			replaces[i] = true;
+			continue;
+		}
+
+		numdatums = (Datum *) palloc(num_elements * sizeof(Datum));
+		for (n = 0; n < num_elements; n++)
+		{
+			JsonbValue *elem;
+
+			elem = getIthJsonbValueFromContainer(jv->val.binary.data, n);
+			if (elem->type != jbvString)
+				ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("invalid format: element %d of %s must be a string", n, key)));
+
+			numdatums[n] = jbvstr_to_float4datum(elem);
+		}
+		arry = construct_array_builtin(numdatums, num_elements, FLOAT4OID);
+		values[i] = PointerGetDatum(arry);	/* stanumbersN */
+		replaces[i] = true;
+	}
+
+	i = Anum_pg_statistic_stavalues1 - 1;
+	for (k = 1; k <= STATISTIC_NUM_SLOTS; k++)
+	{
+		char		key[20];
+		int			num_elements;
+		Datum	   *numdatums;
+		ArrayType  *arry;
+		int			n;
+
+		sprintf(key, "stavalues%d", k);
+
+		/* compute offset to allow for continue bailouts */
+		i = Anum_pg_statistic_stavalues1 - 2 + k;
+
+		jv = key_lookup(cont, key);
+
+		if (jv == NULL)
+		{
+			/* no key set, do not modify existing row value */
+			if (newrow)
+			{
+				nulls[i] = true;
+				values[i] = (Datum) 0;
+			}
+			continue;
+		}
+
+		/* can be null or a binary array */
+		if (jv->type == jbvNull)
+		{
+			/* explicitly set valuesN null */
+			nulls[i] = true;
+			values[i] = (Datum) 0;
+			replaces[i] = true;
+			continue;
+		}
+
+		if (!jbvIsBinaryArray(jv))
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("invalid statistics format, %s must be a array or null", key)));
+
+		num_elements = JsonContainerSize(jv->val.binary.data);
+
+		if (num_elements == 0)
+		{
+			/* empty array is just null */
+			nulls[i] = true;
+			values[i] = (Datum) 0;
+			replaces[i] = true;
+			continue;
+		}
+
+		numdatums = (Datum *) palloc(num_elements * sizeof(Datum));
+
+		for (n = 0; n < num_elements; n++)
+		{
+			/* All elements must be of type string that is iocoerce-friendly */
+			JsonbValue *elem = getIthJsonbValueFromContainer(jv->val.binary.data, n);
+			if (elem->type != jbvString)
+				ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("invalid format: element %d of %s must be a string",
+						   n, key)));
+			numdatums[n] = jbvstr_to_attrtypedatum(elem, finfo,
+													input_func,
+													typioparams,
+													attr->atttypmod);
+		}
+
+		arry = construct_array(numdatums, num_elements, attr->atttypid,
+								attr->attlen, attr->attbyval, attr->attalign);
+		values[i] = PointerGetDatum(arry);	/* stavaluesN */
+		replaces[i] = true;
+	}
+
+	if (newrow)
+	{
+		/* insert new tuple */
+		stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
+		CatalogTupleInsertWithInfo(sd, stup, indstate);
+	}
+	else
+	{
+		/* modify existing tuple */
+		stup = heap_modify_tuple(tup, RelationGetDescr(sd),
+								 values, nulls, replaces);
+		CatalogTupleUpdateWithInfo(sd, &stup->t_self, stup, indstate);
+		ReleaseSysCache(tup);
+	}
+
+	heap_freetuple(stup);
+}
+
+
+/*
+ * Import statistics from JSONB export into relation
+ * to-do: pg_import_ext_stats()
+ */
+Datum
+pg_import_rel_stats(PG_FUNCTION_ARGS)
+{
+	Oid			relid = PG_GETARG_OID(0);
+	int32		stats_version_num = PG_GETARG_INT32(1);
+	Jsonb	   *jb = PG_ARGISNULL(4) ? NULL : PG_GETARG_JSONB_P(4);
+	Relation	onerel;
+
+	if (jb != NULL && !JB_ROOT_IS_OBJECT(jb))
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("invalid statistics format")));
+
+	if ( stats_version_num < 80000)
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("invalid statistics version: %d is earlier than earliest supported version",
+				  stats_version_num)));
+
+	/* to-do: change this to found current server version */
+	if ( stats_version_num > 170000)
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("invalid statistics version: %d is greater than current version",
+				  stats_version_num)));
+
+	onerel = vacuum_open_relation(relid, NULL, VACOPT_ANALYZE, true,
+								  ShareUpdateExclusiveLock);
+
+	if (onerel == NULL)
+		PG_RETURN_BOOL(false);
+
+	if (!vacuum_is_relation_owner(RelationGetRelid(onerel),
+								onerel->rd_rel,
+								VACOPT_ANALYZE))
+	{
+		relation_close(onerel, ShareUpdateExclusiveLock);
+		PG_RETURN_BOOL(false);
+	}
+
+
+	/* only modify pg_class row if changes are to be made */
+	if ( ! PG_ARGISNULL(2) || ! PG_ARGISNULL(3) )
+	{
+		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);
+
+		/* to-do: allow import IF FDW allows analyze */
+		if (pg_class_rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("cannot import stats to foreign table")));
+
+
+		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 (! PG_ARGISNULL(2))
+			pgcform->reltuples = PG_GETARG_FLOAT4(2);
+		if (! PG_ARGISNULL(3))
+			pgcform->relpages = PG_GETARG_INT32(3);
+
+		heap_inplace_update(pg_class_rel, ctup);
+		table_close(pg_class_rel, ShareUpdateExclusiveLock);
+	}
+
+	/* Apply statistical updates, if any, to copied tuple */
+	if (! PG_ARGISNULL(4))
+	{
+		TupleDesc			tupdesc;
+		Relation			sd;
+		TupleDesc			stupdesc;
+		CatalogIndexState	indstate;
+		int					i;
+
+		tupdesc = RelationGetDescr(onerel);
+		sd = table_open(StatisticRelationId, RowExclusiveLock);
+		stupdesc = RelationGetDescr(sd);
+		indstate = CatalogOpenIndexes(sd);
+
+		for (i = 0; i < tupdesc->natts; i++)
+		{
+			Form_pg_attribute	attr;
+			char			   *attname;
+			JsonbContainer	   *attrcont;
+			JsonbContainer	   *inheritcont;
+
+			Oid					in_func;
+			Oid					typioparams;
+			FmgrInfo			finfo;
+			TypeCacheEntry	   *typentry;
+
+			attr = TupleDescAttr(tupdesc, i);
+			typentry = lookup_type_cache(attr->atttypid, TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR);
+
+			/* get input function for stavaluesN InputFunctionCall */
+			getTypeInputInfo(attr->atttypid, &in_func, &typioparams);
+			fmgr_info(in_func, &finfo);
+
+			/* Look for column key matching attname */
+			attname = NameStr(attr->attname);
+
+			attrcont = key_lookup_object(&jb->root, attname);
+
+			if (attrcont == NULL)
+				continue;
+
+			inheritcont = key_lookup_object(attrcont, "regular");
+			if (inheritcont != NULL)
+				import_pg_statistic_rows(relid, sd, stupdesc, attr, &finfo,
+										 in_func, typioparams,
+										 typentry->eq_opr, typentry->lt_opr,
+										 indstate,
+										 inheritcont, "regular", false);
+
+			inheritcont = key_lookup_object(attrcont, "inherited");
+			if (inheritcont != NULL)
+				import_pg_statistic_rows(relid, sd, stupdesc, attr, &finfo,
+										 in_func, typioparams,
+										 typentry->eq_opr, typentry->lt_opr,
+										 indstate,
+										 inheritcont, "inherited", true);
+		}
+
+		CatalogCloseIndexes(indstate);
+		table_close(sd, RowExclusiveLock);
+		relation_close(onerel, NoLock);
+	}
+
+	PG_RETURN_BOOL(true);
+}
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 5058be5411..66a56dee65 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2404,6 +2404,53 @@ pg_statio_user_tables| SELECT relid,
     tidx_blks_hit
    FROM pg_statio_all_tables
   WHERE ((schemaname <> ALL (ARRAY['pg_catalog'::name, 'information_schema'::name])) AND (schemaname !~ '^pg_toast'::text));
+pg_statistic_export| SELECT n.nspname AS schemaname,
+    r.relname,
+    (current_setting('server_version_num'::text))::integer AS server_version_num,
+    r.reltuples AS n_tuples,
+    r.relpages AS n_pages,
+    ( SELECT jsonb_object_agg(a.attname, ( SELECT jsonb_object_agg(attsta.inherit_type, attsta.stat_obj) AS jsonb_object_agg
+                   FROM ( SELECT
+                                CASE s.stainherit
+                                    WHEN true THEN 'inherited'::text
+                                    ELSE 'regular'::text
+                                END AS inherit_type,
+                            jsonb_build_object('attstattarget',
+                                CASE
+                                    WHEN (a.attstattarget >= 0) THEN (a.attstattarget)::integer
+                                    ELSE (current_setting('default_statistics_target'::text))::integer
+                                END, 'stanullfrac', (s.stanullfrac)::text, 'stawidth', (s.stawidth)::text, 'stadistinct', (s.stadistinct)::text, 'stakind1', (s.stakind1)::text, 'stakind2', (s.stakind2)::text, 'stakind3', (s.stakind3)::text, 'stakind4', (s.stakind4)::text, 'stakind5', (s.stakind5)::text, 'staop1',
+                                CASE s.stakind1
+                                    WHEN 0 THEN '0'::text
+                                    WHEN 1 THEN '='::text
+                                    ELSE '<'::text
+                                END, 'staop2',
+                                CASE s.stakind2
+                                    WHEN 0 THEN '0'::text
+                                    WHEN 1 THEN '='::text
+                                    ELSE '<'::text
+                                END, 'staop3',
+                                CASE s.stakind3
+                                    WHEN 0 THEN '0'::text
+                                    WHEN 1 THEN '='::text
+                                    ELSE '<'::text
+                                END, 'staop4',
+                                CASE s.stakind4
+                                    WHEN 0 THEN '0'::text
+                                    WHEN 1 THEN '='::text
+                                    ELSE '<'::text
+                                END, 'staop5',
+                                CASE s.stakind5
+                                    WHEN 0 THEN '0'::text
+                                    WHEN 1 THEN '='::text
+                                    ELSE '<'::text
+                                END, 'stanumbers1', (s.stanumbers1)::text[], 'stanumbers2', (s.stanumbers2)::text[], 'stanumbers3', (s.stanumbers3)::text[], 'stanumbers4', (s.stanumbers4)::text[], 'stanumbers5', (s.stanumbers5)::text[], 'stavalues1', ((s.stavalues1)::text)::text[], 'stavalues2', ((s.stavalues2)::text)::text[], 'stavalues3', ((s.stavalues3)::text)::text[], 'stavalues4', ((s.stavalues4)::text)::text[], 'stavalues5', ((s.stavalues5)::text)::text[]) AS stat_obj
+                           FROM pg_statistic s
+                          WHERE ((s.starelid = a.attrelid) AND (s.staattnum = a.attnum))) attsta) ORDER BY a.attnum) AS jsonb_object_agg
+           FROM pg_attribute a
+          WHERE ((a.attrelid = r.oid) AND (NOT a.attisdropped) AND has_column_privilege(r.oid, a.attnum, 'SELECT'::text) AND (a.attnum > 0))) AS columns
+   FROM (pg_class r
+     JOIN pg_namespace n ON ((n.oid = r.relnamespace)));
 pg_stats| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     a.attname,
diff --git a/src/test/regress/expected/vacuum.out b/src/test/regress/expected/vacuum.out
index 4def90b805..cf7c4029a5 100644
--- a/src/test/regress/expected/vacuum.out
+++ b/src/test/regress/expected/vacuum.out
@@ -508,3 +508,98 @@ RESET ROLE;
 DROP TABLE vacowned;
 DROP TABLE vacowned_parted;
 DROP ROLE regress_vacuum;
+CREATE TYPE stats_import_complex_type AS (
+    a integer,
+    b float,
+    c text,
+    d date,
+    e jsonb);
+CREATE TABLE stats_import_test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_import_complex_type
+);
+INSERT INTO stats_import_test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_import_complex_type
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_import_complex_type
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import_complex_type
+UNION ALL
+SELECT 4, 'four', NULL;
+ANALYZE stats_import_test;
+CREATE TABLE stats_export AS
+SELECT e.*
+FROM pg_catalog.pg_statistic_export AS e
+WHERE e.schemaname = 'public'
+AND e.relname = 'stats_import_test';
+SELECT c.reltuples, c.relpages
+FROM pg_class AS c
+WHERE oid = 'stats_import_test'::regclass;
+ reltuples | relpages 
+-----------+----------
+         4 |        1
+(1 row)
+
+-- test settting tuples and pages but no columns
+SELECT pg_import_rel_stats(c.oid, current_setting('server_version_num')::integer,
+                           1000.0, 200, NULL::jsonb)
+FROM pg_class AS c
+WHERE oid = 'stats_import_test'::regclass;
+WARNING:  relcache reference leak: relation "stats_import_test" not closed
+ pg_import_rel_stats 
+---------------------
+ t
+(1 row)
+
+SELECT c.reltuples, c.relpages
+FROM pg_class AS c
+WHERE oid = 'stats_import_test'::regclass;
+ reltuples | relpages 
+-----------+----------
+      1000 |      200
+(1 row)
+
+-- create a table just like stats_import_test
+CREATE TABLE stats_import_clone ( LIKE stats_import_test );
+-- copy table stats to clone table
+SELECT pg_import_rel_stats(c.oid, e.server_version_num,
+                            e.n_tuples, e.n_pages, e.columns)
+FROM pg_class AS c
+JOIN pg_namespace AS n
+ON n.oid = c.relnamespace
+JOIN stats_export AS e
+ON e.schemaname = 'public'
+AND e.relname = 'stats_import_test'
+WHERE c.oid = 'stats_import_clone'::regclass;
+ pg_import_rel_stats 
+---------------------
+ t
+(1 row)
+
+-- stats should match
+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 AS s
+WHERE s.starelid = 'stats_import_test'::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,
+        stavalues1::text AS sv1, stavalues2::text AS sv2, stavalues3::text AS sv3,
+        stavalues4::text AS sv4, stavalues5::text AS sv5
+FROM pg_statistic AS s
+WHERE s.starelid = 'stats_import_clone'::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)
+
+DROP TABLE stats_export;
+DROP TABLE stats_import_clone;
+DROP TABLE stats_import_test;
+DROP TYPE stats_import_complex_type;
diff --git a/src/test/regress/sql/vacuum.sql b/src/test/regress/sql/vacuum.sql
index 51d7b1fecc..602e84dfa2 100644
--- a/src/test/regress/sql/vacuum.sql
+++ b/src/test/regress/sql/vacuum.sql
@@ -377,3 +377,86 @@ RESET ROLE;
 DROP TABLE vacowned;
 DROP TABLE vacowned_parted;
 DROP ROLE regress_vacuum;
+
+
+CREATE TYPE stats_import_complex_type AS (
+    a integer,
+    b float,
+    c text,
+    d date,
+    e jsonb);
+
+CREATE TABLE stats_import_test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_import_complex_type
+);
+
+INSERT INTO stats_import_test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_import_complex_type
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_import_complex_type
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import_complex_type
+UNION ALL
+SELECT 4, 'four', NULL;
+
+ANALYZE stats_import_test;
+
+CREATE TABLE stats_export AS
+SELECT e.*
+FROM pg_catalog.pg_statistic_export AS e
+WHERE e.schemaname = 'public'
+AND e.relname = 'stats_import_test';
+
+SELECT c.reltuples, c.relpages
+FROM pg_class AS c
+WHERE oid = 'stats_import_test'::regclass;
+
+-- test settting tuples and pages but no columns
+SELECT pg_import_rel_stats(c.oid, current_setting('server_version_num')::integer,
+                           1000.0, 200, NULL::jsonb)
+FROM pg_class AS c
+WHERE oid = 'stats_import_test'::regclass;
+
+SELECT c.reltuples, c.relpages
+FROM pg_class AS c
+WHERE oid = 'stats_import_test'::regclass;
+
+-- create a table just like stats_import_test
+CREATE TABLE stats_import_clone ( LIKE stats_import_test );
+
+-- copy table stats to clone table
+SELECT pg_import_rel_stats(c.oid, e.server_version_num,
+                            e.n_tuples, e.n_pages, e.columns)
+FROM pg_class AS c
+JOIN pg_namespace AS n
+ON n.oid = c.relnamespace
+JOIN stats_export AS e
+ON e.schemaname = 'public'
+AND e.relname = 'stats_import_test'
+WHERE c.oid = 'stats_import_clone'::regclass;
+
+-- stats should match
+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 AS s
+WHERE s.starelid = 'stats_import_test'::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,
+        stavalues1::text AS sv1, stavalues2::text AS sv2, stavalues3::text AS sv3,
+        stavalues4::text AS sv4, stavalues5::text AS sv5
+FROM pg_statistic AS s
+WHERE s.starelid = 'stats_import_clone'::regclass;
+
+DROP TABLE stats_export;
+DROP TABLE stats_import_clone;
+DROP TABLE stats_import_test;
+DROP TYPE stats_import_complex_type;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 7a0d4b9134..fbb0257593 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -1,4 +1,4 @@
-<!-- doc/src/sgml/func.sgml -->
+
 
  <chapter id="functions">
   <title>Functions and Operators</title>
@@ -27904,6 +27904,48 @@ 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>server_version_num</parameter> <type>integer</type>, <parameter>num_tuples</parameter> <type>float4</type>, <parameter>num_pages</parameter> <type>integer</type>, <parameter>column_stats</parameter> <type>jsonb</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>column_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
+        is used by <program>pg_upgrade</program> and <program>pg_restore</program>
+        to convey the statistics from the old system version into the new one.
+       </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
    <table id="functions-admin-dblocation">
     <title>Database Object Location Functions</title>
     <tgroup cols="1">
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 2b35c2f91b..17430e581b 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -191,6 +191,11 @@
       <entry>extended planner statistics for expressions</entry>
      </row>
 
+     <row>
+      <entry><link linkend="view-pg-stats"><structname>pg_stats_export</structname></link></entry>
+      <entry>planner statistics for export/upgrade purposes</entry>
+     </row>
+
      <row>
       <entry><link linkend="view-pg-tables"><structname>pg_tables</structname></link></entry>
       <entry>tables</entry>
-- 
2.41.0

#2Ashutosh Bapat
ashutosh.bapat.oss@gmail.com
In reply to: Corey Huinker (#1)
Re: Statistics Import and Export

On Thu, Aug 31, 2023 at 12:17 PM Corey Huinker <corey.huinker@gmail.com> wrote:

While the primary purpose of the import function(s) are to reduce downtime
during an upgrade, it is not hard to see that they could also be used to
facilitate tuning and development operations, asking questions like "how might
this query plan change if this table has 1000x rows in it?", without actually
putting those rows into the table.

Thanks. I think this may be used with postgres_fdw to import
statistics directly from the foreigns server, whenever possible,
rather than fetching the rows and building it locally. If it's known
that the stats on foreign and local servers match for a foreign table,
we will be one step closer to accurately estimating the cost of a
foreign plan locally rather than through EXPLAIN.

--
Best Wishes,
Ashutosh Bapat

#3Corey Huinker
corey.huinker@gmail.com
In reply to: Ashutosh Bapat (#2)
Re: Statistics Import and Export

Thanks. I think this may be used with postgres_fdw to import
statistics directly from the foreigns server, whenever possible,
rather than fetching the rows and building it locally. If it's known
that the stats on foreign and local servers match for a foreign table,
we will be one step closer to accurately estimating the cost of a
foreign plan locally rather than through EXPLAIN.

Yeah, that use makes sense as well, and if so then postgres_fdw would
likely need to be aware of the appropriate query for several versions back
- they change, not by much, but they do change. So now we'd have each query
text in three places: a system view, postgres_fdw, and the bin/scripts
pre-upgrade program. So I probably should consider the best way to share
those in the codebase.

#4Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#3)
4 attachment(s)
Re: Statistics Import and Export

Yeah, that use makes sense as well, and if so then postgres_fdw would
likely need to be aware of the appropriate query for several versions back
- they change, not by much, but they do change. So now we'd have each query
text in three places: a system view, postgres_fdw, and the bin/scripts
pre-upgrade program. So I probably should consider the best way to share
those in the codebase.

Attached is v2 of this patch.

New features:
* imports index statistics. This is not strictly accurate: it re-computes
index statistics the same as ANALYZE does, which is to say it derives those
stats entirely from table column stats, which are imported, so in that
sense we're getting index stats without touching the heap.
* now support extended statistics except for MCV, which is currently
serialized as an difficult-to-decompose bytea field.
* bare-bones CLI script pg_export_stats, which extracts stats on databases
back to v12 (tested) and could work back to v10.
* bare-bones CLI script pg_import_stats, which obviously only works on
current devel dbs, but can take exports from older versions.

Attachments:

v2-0001-Additional-internal-jsonb-access-functions.patchtext/x-patch; charset=US-ASCII; name=v2-0001-Additional-internal-jsonb-access-functions.patchDownload
From 9bd1618db9e40caad773334921f0a43d9d63e73a Mon Sep 17 00:00:00 2001
From: coreyhuinker <corey.huinker@gmail.com>
Date: Mon, 30 Oct 2023 16:21:30 -0400
Subject: [PATCH v2 1/4] Additional internal jsonb access functions.

Make JsonbContainerTypeName externally visible.

Add JsonbStringValueToCString.
---
 src/include/utils/jsonb.h          |  4 ++++
 src/backend/utils/adt/jsonb.c      |  2 +-
 src/backend/utils/adt/jsonb_util.c | 15 +++++++++++++++
 3 files changed, 20 insertions(+), 1 deletion(-)

diff --git a/src/include/utils/jsonb.h b/src/include/utils/jsonb.h
index addc9b608e..b3c1e104f2 100644
--- a/src/include/utils/jsonb.h
+++ b/src/include/utils/jsonb.h
@@ -424,6 +424,8 @@ extern char *JsonbToCStringIndent(StringInfo out, JsonbContainer *in,
 								  int estimated_len);
 extern bool JsonbExtractScalar(JsonbContainer *jbc, JsonbValue *res);
 extern const char *JsonbTypeName(JsonbValue *val);
+extern const char *JsonbContainerTypeName(JsonbContainer *jbc);
+
 
 extern Datum jsonb_set_element(Jsonb *jb, Datum *path, int path_len,
 							   JsonbValue *newval);
@@ -436,4 +438,6 @@ extern Datum jsonb_build_object_worker(int nargs, const Datum *args, const bool
 extern Datum jsonb_build_array_worker(int nargs, const Datum *args, const bool *nulls,
 									  const Oid *types, bool absent_on_null);
 
+extern char *JsonbStringValueToCString(JsonbValue *j);
+
 #endif							/* __JSONB_H__ */
diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c
index 6f445f5c2b..0ad4e81d89 100644
--- a/src/backend/utils/adt/jsonb.c
+++ b/src/backend/utils/adt/jsonb.c
@@ -160,7 +160,7 @@ jsonb_from_text(text *js, bool unique_keys)
 /*
  * Get the type name of a jsonb container.
  */
-static const char *
+const char *
 JsonbContainerTypeName(JsonbContainer *jbc)
 {
 	JsonbValue	scalar;
diff --git a/src/backend/utils/adt/jsonb_util.c b/src/backend/utils/adt/jsonb_util.c
index 9cc95b773d..ae311b38ba 100644
--- a/src/backend/utils/adt/jsonb_util.c
+++ b/src/backend/utils/adt/jsonb_util.c
@@ -1992,3 +1992,18 @@ uniqueifyJsonbObject(JsonbValue *object, bool unique_keys, bool skip_nulls)
 		}
 	}
 }
+
+/*
+ * Extract a JsonbValue as a cstring.
+ */
+char *JsonbStringValueToCString(JsonbValue *j)
+{
+	char *s;
+
+	Assert(j->type == jbvString);
+	/* make a string that we are sure is null-terminated */
+	s = palloc(j->val.string.len + 1);
+	memcpy(s, j->val.string.val, j->val.string.len);
+	s[j->val.string.len] = '\0';
+	return s;
+}
-- 
2.41.0

v2-0002-Add-system-view-pg_statistic_export.patchtext/x-patch; charset=US-ASCII; name=v2-0002-Add-system-view-pg_statistic_export.patchDownload
From 920d546032a330fe1ec4c8f6a35c48a47ccd08b2 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Tue, 31 Oct 2023 02:27:17 -0400
Subject: [PATCH v2 2/4] Add system view pg_statistic_export.

This view is designed to aid in the export (and re-import) of table
statistics and extended statistics, mostly for upgrade/restore
situations.
---
 src/backend/catalog/system_views.sql | 215 +++++++++++++++++++++++++++
 src/test/regress/expected/rules.out  |  71 +++++++++
 doc/src/sgml/system-views.sgml       |   5 +
 3 files changed, 291 insertions(+)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index b65f6b5249..11a1037ceb 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -253,6 +253,221 @@ CREATE VIEW pg_stats WITH (security_barrier) AS
 
 REVOKE ALL ON pg_statistic FROM public;
 
+
+
+
+CREATE VIEW pg_statistic_export WITH (security_barrier) AS
+    SELECT
+        n.nspname AS schemaname,
+        r.relname AS relname,
+        current_setting('server_version_num')::integer AS server_version_num,
+        r.reltuples::float4 AS n_tuples,
+        r.relpages::integer AS n_pages,
+        (
+            WITH per_column_stats AS
+            (
+                SELECT
+                    s.stainherit,
+                    a.attname,
+                    jsonb_build_object(
+                        'stanullfrac', s.stanullfrac::text,
+                        'stawidth', s.stawidth::text,
+                        'stadistinct', s.stadistinct::text,
+                        'stakinds',
+                        (
+                            SELECT
+                                jsonb_agg(
+                                    CASE kind.kind
+                                        WHEN 0 THEN 'TRIVIAL'
+                                        WHEN 1 THEN 'MCV'
+                                        WHEN 2 THEN 'HISTOGRAM'
+                                        WHEN 3 THEN 'CORRELATION'
+                                        WHEN 4 THEN 'MCELEM'
+                                        WHEN 5 THEN 'DECHIST'
+                                        WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM'
+                                        WHEN 7 THEN 'BOUNDS_HISTOGRAM'
+                                    END::text
+                                    ORDER BY kind.ord)
+                            FROM unnest(ARRAY[s.stakind1, s.stakind2,
+                                        s.stakind3, stakind4,
+                                        s.stakind5])
+                                 WITH ORDINALITY AS kind(kind, ord)
+                        ),
+                        'stanumbers',
+                        jsonb_build_array(
+                            s.stanumbers1::text::text[],
+                            s.stanumbers2::text::text[],
+                            s.stanumbers3::text::text[],
+                            s.stanumbers4::text::text[],
+                            s.stanumbers5::text::text[]),
+                        'stavalues',
+                        jsonb_build_array(
+                            s.stavalues1::text::text[],
+                            s.stavalues2::text::text[],
+                            s.stavalues3::text::text[],
+                            s.stavalues4::text::text[],
+                            s.stavalues5::text::text[])
+                    ) AS stats
+                FROM pg_attribute AS a
+                JOIN pg_statistic AS s
+                    ON s.starelid = a.attrelid
+                    AND s.staattnum = a.attnum
+                WHERE a.attrelid = r.oid
+                AND NOT a.attisdropped
+                AND a.attnum > 0
+                AND has_column_privilege(a.attrelid, a.attnum, 'SELECT')
+            ),
+            attagg AS
+            (
+                SELECT
+                    pcs.stainherit,
+                    jsonb_build_object(
+                        'columns',
+                        jsonb_object_agg(
+                            pcs.attname,
+                            pcs.stats
+                        )
+                    ) AS stats
+                FROM per_column_stats AS pcs
+                GROUP BY pcs.stainherit
+            ),
+            extended_object_stats AS
+            (
+                SELECT
+                    sd.stxdinherit,
+                    e.stxname,
+                    jsonb_build_object(
+                        'stxkinds',
+                        to_jsonb(e.stxkind),
+                        'stxdndistinct',
+                        ndist.stxdndistinct,
+                        'stxdndependencies',
+                        ndep.stxdndependencies,
+                        'stxdmcv',
+                        mcv.stxdmcv,
+                        'stxdexprs',
+                        x.stdxdexprs
+                    ) AS stats
+                FROM pg_statistic_ext AS e
+                JOIN pg_statistic_ext_data AS sd
+                    ON sd.stxoid = e.oid
+                LEFT JOIN LATERAL
+                    (
+                        -- att1 [, att2..]: ndistinct
+                        SELECT
+                            jsonb_agg(
+                                jsonb_build_object(
+                                    'attnums', string_to_array(nd.attnums, ', '),
+                                    'ndistinct', nd.ndistinct
+                                    )
+                                ORDER BY nd.ord
+                            )
+                        -- jsonb does not preserve parsed order so use json
+                        FROM json_each_text(sd.stxdndistinct::text::json)
+                             WITH ORDINALITY AS nd(attnums, ndistinct, ord)
+                    ) AS ndist(stxdndistinct) ON sd.stxdndistinct IS NOT NULL
+                LEFT JOIN LATERAL
+                    (
+                        -- att1, [, att2 ...] => attN: degree
+                        SELECT
+                            jsonb_agg(
+                                jsonb_build_object(
+                                    'attnums',
+                                    string_to_array( replace(dep.attrs, ' => ', ', '), ', '),
+                                    'degree',
+                                    dep.degree
+                                    )
+                                ORDER BY dep.ord
+                            )
+                        -- jsonb does not preserve parsed order so use json
+                        FROM json_each_text(sd.stxddependencies::text::json)
+                             WITH ORDINALITY AS dep(attrs, degree, ord)
+                    ) AS ndep(stxdndependencies) ON sd.stxddependencies IS NOT NULL
+                LEFT JOIN LATERAL
+                    (
+                        -- TODO SELECT sd.stxdmcv
+                        SELECT NULL AS stxdmcv
+                    ) AS mcv(stxdmcv) ON sd.stxdmcv IS NOT NULL
+                LEFT JOIN LATERAL
+                    (
+                        SELECT
+                            jsonb_agg(
+                                jsonb_build_object(
+                                    'stanullfrac', s.stanullfrac::text,
+                                    'stawidth', s.stawidth::text,
+                                    'stadistinct', s.stadistinct::text,
+                                    'stakinds',
+                                    (
+                                        SELECT
+                                            jsonb_agg(
+                                                CASE kind.kind
+                                                    WHEN 0 THEN 'TRIVIAL'
+                                                    WHEN 1 THEN 'MCV'
+                                                    WHEN 2 THEN 'HISTOGRAM'
+                                                    WHEN 3 THEN 'CORRELATION'
+                                                    WHEN 4 THEN 'MCELEM'
+                                                    WHEN 5 THEN 'DECHIST'
+                                                    WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM'
+                                                    WHEN 7 THEN 'BOUNDS_HISTOGRAM'
+                                                END::text
+                                                ORDER BY kind.ord)
+                                        FROM unnest(ARRAY[s.stakind1, s.stakind2,
+                                                    s.stakind3, stakind4,
+                                                    s.stakind5]) WITH ORDINALITY AS kind(kind, ord)
+                                    ),
+                                    'stanumbers',
+                                    jsonb_build_array(
+                                        s.stanumbers1::text::text[],
+                                        s.stanumbers2::text::text[],
+                                        s.stanumbers3::text::text[],
+                                        s.stanumbers4::text::text[],
+                                        s.stanumbers5::text::text[]),
+                                    'stavalues',
+                                    jsonb_build_array(
+                                        s.stavalues1::text::text[],
+                                        s.stavalues2::text::text[],
+                                        s.stavalues3::text::text[],
+                                        s.stavalues4::text::text[],
+                                        s.stavalues5::text::text[])
+                                )
+                                ORDER BY s.ordinality
+                            )
+                        FROM unnest(sd.stxdexpr) WITH ORDINALITY AS s
+                    ) AS x(stdxdexprs) ON sd.stxdexpr IS NOT NULL
+                WHERE e.stxrelid = r.oid
+            ),
+            extagg AS
+            (
+                SELECT
+                    eos.stxdinherit,
+                    jsonb_build_object(
+                        'extended',
+                        jsonb_object_agg(
+                            eos.stxname,
+                            eos.stats
+                        )
+                    ) AS stats
+                FROM extended_object_stats AS eos
+                GROUP BY eos.stxdinherit
+            )
+            SELECT
+                jsonb_object_agg(
+                    CASE coalesce(a.stainherit, e.stxdinherit)
+                        WHEN TRUE THEN 'inherited'
+                        ELSE 'regular'
+                    END,
+                    coalesce(a.stats, '{}'::jsonb) || coalesce(e.stats, '{}'::jsonb)
+                )
+            FROM attagg AS a
+            FULL OUTER JOIN extagg e ON a.stainherit = e.stxdinherit
+        ) AS stats
+    FROM pg_class AS r
+    JOIN pg_namespace AS n
+        ON n.oid = r.relnamespace
+    WHERE relkind IN ('r', 'm', 'f', 'p')
+    AND n.nspname NOT IN ('pg_catalog', 'information_schema');
+
+
 CREATE VIEW pg_stats_ext WITH (security_barrier) AS
     SELECT cn.nspname AS schemaname,
            c.relname AS tablename,
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 1442c43d9c..20ab4da385 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2404,6 +2404,77 @@ pg_statio_user_tables| SELECT relid,
     tidx_blks_hit
    FROM pg_statio_all_tables
   WHERE ((schemaname <> ALL (ARRAY['pg_catalog'::name, 'information_schema'::name])) AND (schemaname !~ '^pg_toast'::text));
+pg_statistic_export| SELECT n.nspname AS schemaname,
+    r.relname,
+    (current_setting('server_version_num'::text))::integer AS server_version_num,
+    r.reltuples AS n_tuples,
+    r.relpages AS n_pages,
+    ( WITH per_column_stats AS (
+                 SELECT s.stainherit,
+                    a_1.attname,
+                    jsonb_build_object('stanullfrac', (s.stanullfrac)::text, 'stawidth', (s.stawidth)::text, 'stadistinct', (s.stadistinct)::text, 'stakinds', ( SELECT jsonb_agg(
+                                CASE kind.kind
+                                    WHEN 0 THEN 'TRIVIAL'::text
+                                    WHEN 1 THEN 'MCV'::text
+                                    WHEN 2 THEN 'HISTOGRAM'::text
+                                    WHEN 3 THEN 'CORRELATION'::text
+                                    WHEN 4 THEN 'MCELEM'::text
+                                    WHEN 5 THEN 'DECHIST'::text
+                                    WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM'::text
+                                    WHEN 7 THEN 'BOUNDS_HISTOGRAM'::text
+                                    ELSE NULL::text
+                                END ORDER BY kind.ord) AS jsonb_agg
+                           FROM unnest(ARRAY[s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5]) WITH ORDINALITY kind(kind, ord)), 'stanumbers', jsonb_build_array(((s.stanumbers1)::text)::text[], ((s.stanumbers2)::text)::text[], ((s.stanumbers3)::text)::text[], ((s.stanumbers4)::text)::text[], ((s.stanumbers5)::text)::text[]), 'stavalues', jsonb_build_array(((s.stavalues1)::text)::text[], ((s.stavalues2)::text)::text[], ((s.stavalues3)::text)::text[], ((s.stavalues4)::text)::text[], ((s.stavalues5)::text)::text[])) AS stats
+                   FROM (pg_attribute a_1
+                     JOIN pg_statistic s ON (((s.starelid = a_1.attrelid) AND (s.staattnum = a_1.attnum))))
+                  WHERE ((a_1.attrelid = r.oid) AND (NOT a_1.attisdropped) AND (a_1.attnum > 0) AND has_column_privilege(a_1.attrelid, a_1.attnum, 'SELECT'::text))
+                ), attagg AS (
+                 SELECT pcs.stainherit,
+                    jsonb_build_object('columns', jsonb_object_agg(pcs.attname, pcs.stats)) AS stats
+                   FROM per_column_stats pcs
+                  GROUP BY pcs.stainherit
+                ), extended_object_stats AS (
+                 SELECT sd.stxdinherit,
+                    e_1.stxname,
+                    jsonb_build_object('stxkinds', to_jsonb(e_1.stxkind), 'stxdndistinct', ndist.stxdndistinct, 'stxdndependencies', ndep.stxdndependencies, 'stxdmcv', mcv.stxdmcv, 'stxdexprs', x.stdxdexprs) AS stats
+                   FROM (((((pg_statistic_ext e_1
+                     JOIN pg_statistic_ext_data sd ON ((sd.stxoid = e_1.oid)))
+                     LEFT JOIN LATERAL ( SELECT jsonb_agg(jsonb_build_object('attnums', string_to_array(nd.attnums, ', '::text), 'ndistinct', nd.ndistinct) ORDER BY nd.ord) AS jsonb_agg
+                           FROM json_each_text(((sd.stxdndistinct)::text)::json) WITH ORDINALITY nd(attnums, ndistinct, ord)) ndist(stxdndistinct) ON ((sd.stxdndistinct IS NOT NULL)))
+                     LEFT JOIN LATERAL ( SELECT jsonb_agg(jsonb_build_object('attnums', string_to_array(replace(dep.attrs, ' => '::text, ', '::text), ', '::text), 'degree', dep.degree) ORDER BY dep.ord) AS jsonb_agg
+                           FROM json_each_text(((sd.stxddependencies)::text)::json) WITH ORDINALITY dep(attrs, degree, ord)) ndep(stxdndependencies) ON ((sd.stxddependencies IS NOT NULL)))
+                     LEFT JOIN LATERAL ( SELECT NULL::text AS stxdmcv) mcv(stxdmcv) ON ((sd.stxdmcv IS NOT NULL)))
+                     LEFT JOIN LATERAL ( SELECT jsonb_agg(jsonb_build_object('stanullfrac', (s.stanullfrac)::text, 'stawidth', (s.stawidth)::text, 'stadistinct', (s.stadistinct)::text, 'stakinds', ( SELECT jsonb_agg(
+CASE kind.kind
+ WHEN 0 THEN 'TRIVIAL'::text
+ WHEN 1 THEN 'MCV'::text
+ WHEN 2 THEN 'HISTOGRAM'::text
+ WHEN 3 THEN 'CORRELATION'::text
+ WHEN 4 THEN 'MCELEM'::text
+ WHEN 5 THEN 'DECHIST'::text
+ WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM'::text
+ WHEN 7 THEN 'BOUNDS_HISTOGRAM'::text
+ ELSE NULL::text
+END ORDER BY kind.ord) AS jsonb_agg
+                                   FROM unnest(ARRAY[s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5]) WITH ORDINALITY kind(kind, ord)), 'stanumbers', jsonb_build_array(((s.stanumbers1)::text)::text[], ((s.stanumbers2)::text)::text[], ((s.stanumbers3)::text)::text[], ((s.stanumbers4)::text)::text[], ((s.stanumbers5)::text)::text[]), 'stavalues', jsonb_build_array(((s.stavalues1)::text)::text[], ((s.stavalues2)::text)::text[], ((s.stavalues3)::text)::text[], ((s.stavalues4)::text)::text[], ((s.stavalues5)::text)::text[])) ORDER BY s.ordinality) AS jsonb_agg
+                           FROM unnest(sd.stxdexpr) WITH ORDINALITY s(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, stavalues2, stavalues3, stavalues4, stavalues5, ordinality)) x(stdxdexprs) ON ((sd.stxdexpr IS NOT NULL)))
+                  WHERE (e_1.stxrelid = r.oid)
+                ), extagg AS (
+                 SELECT eos.stxdinherit,
+                    jsonb_build_object('extended', jsonb_object_agg(eos.stxname, eos.stats)) AS stats
+                   FROM extended_object_stats eos
+                  GROUP BY eos.stxdinherit
+                )
+         SELECT jsonb_object_agg(
+                CASE COALESCE(a.stainherit, e.stxdinherit)
+                    WHEN true THEN 'inherited'::text
+                    ELSE 'regular'::text
+                END, (COALESCE(a.stats, '{}'::jsonb) || COALESCE(e.stats, '{}'::jsonb))) AS jsonb_object_agg
+           FROM (attagg a
+             FULL JOIN extagg e ON ((a.stainherit = e.stxdinherit)))) AS stats
+   FROM (pg_class r
+     JOIN pg_namespace n ON ((n.oid = r.relnamespace)))
+  WHERE ((r.relkind = ANY (ARRAY['r'::"char", 'm'::"char", 'f'::"char", 'p'::"char"])) AND (n.nspname <> ALL (ARRAY['pg_catalog'::name, 'information_schema'::name])));
 pg_stats| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     a.attname,
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 7078491c4c..24b44ab388 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -191,6 +191,11 @@
       <entry>extended planner statistics for expressions</entry>
      </row>
 
+     <row>
+      <entry><link linkend="view-pg-stats"><structname>pg_stats_export</structname></link></entry>
+      <entry>planner statistics for export/upgrade purposes</entry>
+     </row>
+
      <row>
       <entry><link linkend="view-pg-tables"><structname>pg_tables</structname></link></entry>
       <entry>tables</entry>
-- 
2.41.0

v2-0003-Add-pg_import_rel_stats.patchtext/x-patch; charset=US-ASCII; name=v2-0003-Add-pg_import_rel_stats.patchDownload
From 05b0a3537765a85fda8b59912b658eec1e6b5a81 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Tue, 31 Oct 2023 03:12:45 -0400
Subject: [PATCH v2 3/4] Add pg_import_rel_stats().

The function pg_import_rel_stats imports rowcount, pagecount, and column
statistics for a given table, as well as column statistics for the
underlying indexes and extended statistics for any named statistics
objects for that table.

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 best-effort approach, skipping statistics that are
expected but omitted, skipping object that are specified but do not
exist on the target system. The goal is to get better-than-empty
statistics into the table quickly, so that business operations can
resume sooner.

It should be noted that index statistics _are_ rebuilt as normal, but
use the imported column statistics from the table, so it is not
necessary for the indexes on the target table to match the indexes on
the extracted source table.

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

Extended statistics are identified by name, so those names on the target
system must match the source. If not, they are skipped.

While the column, index column, and extended 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.

The medium of exchange is jsonb, the format of which is specified in the
view pg_statistic_export. Obviously this view does not exist in older
versions of the database, but the view definition can be extracted and
adapted to older versions.

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               |   5 +
 src/include/commands/vacuum.h                 |   4 +
 .../statistics/extended_stats_internal.h      |   3 +
 src/include/statistics/statistics.h           |  10 +
 src/backend/commands/analyze.c                | 709 ++++++++++++++++--
 src/backend/statistics/dependencies.c         | 133 ++++
 src/backend/statistics/extended_stats.c       | 259 +++++++
 src/backend/statistics/mcv.c                  |   6 +
 src/backend/statistics/mvdistinct.c           | 120 +++
 src/test/regress/expected/vacuum.out          | 153 ++++
 src/test/regress/sql/vacuum.sql               | 146 ++++
 doc/src/sgml/func.sgml                        |  42 ++
 12 files changed, 1527 insertions(+), 63 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 568aa80d92..01aeb54fe3 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5639,6 +5639,11 @@
   proname => 'pg_stat_get_db_checksum_last_failure', provolatile => 's',
   proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
   prosrc => 'pg_stat_get_db_checksum_last_failure' },
+{ oid => '3813',
+  descr => 'statistics: import to relation',
+  proname => 'pg_import_rel_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'bool', proargtypes => 'oid int4 float4 int4 jsonb',
+  prosrc => 'pg_import_rel_stats' },
 { oid => '3074', descr => 'statistics: last reset for a database',
   proname => 'pg_stat_get_db_stat_reset_time', provolatile => 's',
   proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 4af02940c5..72c31eb882 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -24,6 +24,7 @@
 #include "storage/buf.h"
 #include "storage/lock.h"
 #include "utils/relcache.h"
+#include "utils/jsonb.h"
 
 /*
  * Flags for amparallelvacuumoptions to control the participation of bulkdelete
@@ -386,4 +387,7 @@ extern double anl_random_fract(void);
 extern double anl_init_selection_state(int n);
 extern double anl_get_next_S(double t, int n, double *stateptr);
 
+extern Datum pg_import_rel_stats(PG_FUNCTION_ARGS);
+
+
 #endif							/* VACUUM_H */
diff --git a/src/include/statistics/extended_stats_internal.h b/src/include/statistics/extended_stats_internal.h
index 7b55eb8ffa..fdefaa28f5 100644
--- a/src/include/statistics/extended_stats_internal.h
+++ b/src/include/statistics/extended_stats_internal.h
@@ -72,15 +72,18 @@ typedef struct StatsBuildData
 extern MVNDistinct *statext_ndistinct_build(double totalrows, StatsBuildData *data);
 extern bytea *statext_ndistinct_serialize(MVNDistinct *ndistinct);
 extern MVNDistinct *statext_ndistinct_deserialize(bytea *data);
+extern MVNDistinct *import_ndistinct(JsonbContainer *cont);
 
 extern MVDependencies *statext_dependencies_build(StatsBuildData *data);
 extern bytea *statext_dependencies_serialize(MVDependencies *dependencies);
 extern MVDependencies *statext_dependencies_deserialize(bytea *data);
+extern MVDependencies * import_dependencies(JsonbContainer *cont);
 
 extern MCVList *statext_mcv_build(StatsBuildData *data,
 								  double totalrows, int stattarget);
 extern bytea *statext_mcv_serialize(MCVList *mcvlist, VacAttrStats **stats);
 extern MCVList *statext_mcv_deserialize(bytea *data);
+extern MCVList *import_mcv(JsonbContainer *cont);
 
 extern MultiSortSupport multi_sort_init(int ndims);
 extern void multi_sort_add_dimension(MultiSortSupport mss, int sortdim,
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 5e538fec32..2b37e77887 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -101,6 +101,7 @@ extern MCVList *statext_mcv_load(Oid mvoid, bool inh);
 extern void BuildRelationExtStatistics(Relation onerel, bool inh, double totalrows,
 									   int numrows, HeapTuple *rows,
 									   int natts, VacAttrStats **vacattrstats);
+
 extern int	ComputeExtStatisticsRows(Relation onerel,
 									 int natts, VacAttrStats **vacattrstats);
 extern bool statext_is_kind_built(HeapTuple htup, char type);
@@ -127,4 +128,13 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												int nclauses);
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
+extern void ImportVacAttrStats( VacAttrStats *stats, JsonbContainer *cont);
+extern void ImportRelationExtStatistics(Relation onerel, bool inh, int natts,
+										VacAttrStats **vacattrstats,
+										JsonbContainer *cont);
+
+extern char *key_lookup_cstring(JsonbContainer *cont, const char *key);
+extern JsonbContainer *key_lookup_object(JsonbContainer *cont, const char *key);
+extern JsonbContainer *key_lookup_array(JsonbContainer *cont, const char *key);
+
 #endif							/* STATISTICS_H */
diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c
index 206d1689ef..a9715c3eb3 100644
--- a/src/backend/commands/analyze.c
+++ b/src/backend/commands/analyze.c
@@ -18,6 +18,7 @@
 
 #include "access/detoast.h"
 #include "access/genam.h"
+#include "access/heapam.h"
 #include "access/multixact.h"
 #include "access/relation.h"
 #include "access/sysattr.h"
@@ -33,6 +34,7 @@
 #include "catalog/pg_collation.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/pg_namespace.h"
+#include "catalog/pg_operator.h"
 #include "catalog/pg_statistic_ext.h"
 #include "commands/dbcommands.h"
 #include "commands/progress.h"
@@ -40,6 +42,7 @@
 #include "commands/vacuum.h"
 #include "common/pg_prng.h"
 #include "executor/executor.h"
+#include "fmgr.h"
 #include "foreign/fdwapi.h"
 #include "miscadmin.h"
 #include "nodes/nodeFuncs.h"
@@ -57,16 +60,20 @@
 #include "utils/attoptcache.h"
 #include "utils/builtins.h"
 #include "utils/datum.h"
+#include "utils/float.h"
 #include "utils/fmgroids.h"
 #include "utils/guc.h"
+#include "utils/jsonb.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
+#include "utils/numeric.h"
 #include "utils/pg_rusage.h"
 #include "utils/sampling.h"
 #include "utils/sortsupport.h"
 #include "utils/spccache.h"
 #include "utils/syscache.h"
 #include "utils/timestamp.h"
+#include "utils/typcache.h"
 
 
 /* Per-index data for ANALYZE */
@@ -109,6 +116,12 @@ static void update_attstats(Oid relid, bool inh,
 static Datum std_fetch_func(VacAttrStatsP stats, int rownum, bool *isNull);
 static Datum ind_fetch_func(VacAttrStatsP stats, int rownum, bool *isNull);
 
+static AnlIndexData *build_indexdata(Relation onerel, Relation *Irel,
+									 int nindexes, bool all_columns);
+static VacAttrStats **examine_rel_attributes(Relation onerel, int *attr_cnt);
+
+static
+void import_pg_statistics(Relation onerel, bool inh, JsonbContainer *cont);
 
 /*
  *	analyze_rel() -- analyze one relation
@@ -403,29 +416,8 @@ do_analyze_rel(Relation onerel, VacuumParams *params,
 		attr_cnt = tcnt;
 	}
 	else
-	{
-		attr_cnt = onerel->rd_att->natts;
-		vacattrstats = (VacAttrStats **)
-			palloc(attr_cnt * sizeof(VacAttrStats *));
-		tcnt = 0;
-		for (i = 1; i <= attr_cnt; i++)
-		{
-			vacattrstats[tcnt] = examine_attribute(onerel, i, NULL);
-			if (vacattrstats[tcnt] != NULL)
-				tcnt++;
-		}
-		attr_cnt = tcnt;
-	}
+		vacattrstats = examine_rel_attributes(onerel, &attr_cnt);
 
-	/*
-	 * Open all indexes of the relation, and see if there are any analyzable
-	 * columns in the indexes.  We do not analyze index columns if there was
-	 * an explicit column list in the ANALYZE command, however.
-	 *
-	 * If we are doing a recursive scan, we don't want to touch the parent's
-	 * indexes at all.  If we're processing a partitioned table, we need to
-	 * know if there are any indexes, but we don't want to process them.
-	 */
 	if (onerel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
 	{
 		List	   *idxs = RelationGetIndexList(onerel);
@@ -446,48 +438,8 @@ do_analyze_rel(Relation onerel, VacuumParams *params,
 		nindexes = 0;
 		hasindex = false;
 	}
-	indexdata = NULL;
-	if (nindexes > 0)
-	{
-		indexdata = (AnlIndexData *) palloc0(nindexes * sizeof(AnlIndexData));
-		for (ind = 0; ind < nindexes; ind++)
-		{
-			AnlIndexData *thisdata = &indexdata[ind];
-			IndexInfo  *indexInfo;
 
-			thisdata->indexInfo = indexInfo = BuildIndexInfo(Irel[ind]);
-			thisdata->tupleFract = 1.0; /* fix later if partial */
-			if (indexInfo->ii_Expressions != NIL && va_cols == NIL)
-			{
-				ListCell   *indexpr_item = list_head(indexInfo->ii_Expressions);
-
-				thisdata->vacattrstats = (VacAttrStats **)
-					palloc(indexInfo->ii_NumIndexAttrs * sizeof(VacAttrStats *));
-				tcnt = 0;
-				for (i = 0; i < indexInfo->ii_NumIndexAttrs; i++)
-				{
-					int			keycol = indexInfo->ii_IndexAttrNumbers[i];
-
-					if (keycol == 0)
-					{
-						/* Found an index expression */
-						Node	   *indexkey;
-
-						if (indexpr_item == NULL)	/* shouldn't happen */
-							elog(ERROR, "too few entries in indexprs list");
-						indexkey = (Node *) lfirst(indexpr_item);
-						indexpr_item = lnext(indexInfo->ii_Expressions,
-											 indexpr_item);
-						thisdata->vacattrstats[tcnt] =
-							examine_attribute(Irel[ind], i + 1, indexkey);
-						if (thisdata->vacattrstats[tcnt] != NULL)
-							tcnt++;
-					}
-				}
-				thisdata->attr_cnt = tcnt;
-			}
-		}
-	}
+	indexdata = build_indexdata(onerel, Irel, nindexes, (va_cols == NIL));
 
 	/*
 	 * Determine how many rows we need to sample, using the worst case from
@@ -823,6 +775,92 @@ do_analyze_rel(Relation onerel, VacuumParams *params,
 	anl_context = NULL;
 }
 
+
+/*
+ * Create VacAttrStats entries for all attributes in a relation.
+ */
+static
+VacAttrStats **examine_rel_attributes(Relation onerel, int *attr_cnt)
+{
+	int natts = onerel->rd_att->natts;
+	int nfound = 0;
+	int i;
+	VacAttrStats **attrstats;
+
+	*attr_cnt = natts;
+	attrstats = (VacAttrStats **) palloc(natts * sizeof(VacAttrStats *));
+	for (i = 1; i <= natts; i++)
+	{
+		attrstats[nfound] = examine_attribute(onerel, i, NULL);
+		if (attrstats[nfound] != NULL)
+			nfound++;
+	}
+	*attr_cnt = nfound;
+	return attrstats;
+}
+
+/*
+ * Open all indexes of the relation, and see if there are any analyzable
+ * columns in the indexes.  We do not analyze index columns if there was
+ * an explicit column list in the ANALYZE command, however.
+ *
+ * If we are doing a recursive scan, we don't want to touch the parent's
+ * indexes at all.  If we're processing a partitioned table, we need to
+ * know if there are any indexes, but we don't want to process them.
+ */
+static
+AnlIndexData *build_indexdata(Relation onerel, Relation *Irel, int nindexes, bool all_columns)
+{
+	AnlIndexData   *indexdata;
+	int				ind,
+					tcnt,
+					i;
+
+	if (nindexes == 0)
+		return NULL;
+
+	indexdata = (AnlIndexData *) palloc0(nindexes * sizeof(AnlIndexData));
+
+	for (ind = 0; ind < nindexes; ind++)
+	{
+		AnlIndexData *thisdata = &indexdata[ind];
+		IndexInfo  *indexInfo;
+
+		thisdata->indexInfo = indexInfo = BuildIndexInfo(Irel[ind]);
+		thisdata->tupleFract = 1.0; /* fix later if partial */
+		if (indexInfo->ii_Expressions != NIL && all_columns)
+		{
+			ListCell   *indexpr_item = list_head(indexInfo->ii_Expressions);
+
+			thisdata->vacattrstats = (VacAttrStats **)
+				palloc(indexInfo->ii_NumIndexAttrs * sizeof(VacAttrStats *));
+			tcnt = 0;
+			for (i = 0; i < indexInfo->ii_NumIndexAttrs; i++)
+			{
+				int			keycol = indexInfo->ii_IndexAttrNumbers[i];
+
+				if (keycol == 0)
+				{
+					/* Found an index expression */
+					Node	   *indexkey;
+
+					if (indexpr_item == NULL)	/* shouldn't happen */
+						elog(ERROR, "too few entries in indexprs list");
+					indexkey = (Node *) lfirst(indexpr_item);
+					indexpr_item = lnext(indexInfo->ii_Expressions,
+										 indexpr_item);
+					thisdata->vacattrstats[tcnt] =
+						examine_attribute(Irel[ind], i + 1, indexkey);
+					if (thisdata->vacattrstats[tcnt] != NULL)
+						tcnt++;
+				}
+			}
+			thisdata->attr_cnt = tcnt;
+		}
+	}
+	return indexdata;
+}
+
 /*
  * Compute statistics about indexes of a relation
  */
@@ -3073,3 +3111,548 @@ analyze_mcv_list(int *mcv_counts,
 	}
 	return num_mcv;
 }
+
+/*
+ * Get a JsonbValue from a JsonbContainer and ensure that it is a string,
+ * and return the cstring.
+ */
+char *key_lookup_cstring(JsonbContainer *cont, const char *key)
+{
+	JsonbValue	j;
+
+	if (!getKeyJsonValueFromContainer(cont, key, strlen(key), &j))
+		return NULL;
+
+	if (j.type != jbvString)
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("invalid statistics format, %s must be a string but is type %s",
+				  key, JsonbTypeName(&j))));
+
+	return JsonbStringValueToCString(&j);
+}
+
+/*
+ * Get a JsonbContainer from a JsonbContainer and ensure that it is a object
+ */
+JsonbContainer *key_lookup_object(JsonbContainer *cont, const char *key)
+{
+	JsonbValue	j;
+
+	if (!getKeyJsonValueFromContainer(cont, key, strlen(key), &j))
+		return NULL;
+
+	if (j.type != jbvBinary)
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("invalid statistics format, %s must be an object but is type %s",
+				  key, JsonbTypeName(&j))));
+
+	if (!JsonContainerIsObject(j.val.binary.data))
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("invalid statistics format, %s must be an object but is type %s",
+				  key, JsonbContainerTypeName(j.val.binary.data))));
+
+	return j.val.binary.data;
+}
+
+/*
+ * Get a JsonbContainer from a JsonbContainer and ensure that it is an array
+ */
+JsonbContainer *key_lookup_array(JsonbContainer *cont, const char *key)
+{
+	JsonbValue	j;
+
+	if (!getKeyJsonValueFromContainer(cont, key, strlen(key), &j))
+		return NULL;
+
+	if (j.type != jbvBinary)
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("invalid statistics format, %s must be an array but is type %s",
+				  key, JsonbTypeName(&j))));
+
+	if (!JsonContainerIsArray(j.val.binary.data))
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("invalid statistics format, %s must be an array but is type %s",
+				  key, JsonbContainerTypeName(j.val.binary.data))));
+
+	return j.val.binary.data;
+}
+
+/*
+ * Import statistics from JSONB export into relation
+ *
+ * Format is:
+ *
+ * {
+ *   "columns": { "colname1": ... },
+ *   "extended": { "statname1": ...}
+ * }
+ */
+Datum
+pg_import_rel_stats(PG_FUNCTION_ARGS)
+{
+	Oid			relid;
+	int32		stats_version_num;
+	Jsonb	   *jb;
+	Relation	onerel;
+	Oid			save_userid;
+	int			save_sec_context;
+	int			save_nestlevel;
+
+	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))
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("server_version_number cannot be NULL")));
+	stats_version_num = PG_GETARG_INT32(1);
+
+	if (stats_version_num < 80000)
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("invalid statistics version: %d is earlier than earliest supported version",
+				  stats_version_num)));
+
+	if (PG_ARGISNULL(4))
+		jb = NULL;
+	else
+	{
+		jb = PG_GETARG_JSONB_P(4);
+		if (!JB_ROOT_IS_OBJECT(jb))
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("columns must be jsonb object at root")));
+	}
+
+	onerel = vacuum_open_relation(relid, NULL, VACOPT_ANALYZE, true,
+								  ShareUpdateExclusiveLock);
+
+	if (onerel == NULL)
+		PG_RETURN_BOOL(false);
+
+	if (!vacuum_is_relation_owner(RelationGetRelid(onerel),
+								onerel->rd_rel,
+								VACOPT_ANALYZE))
+	{
+		relation_close(onerel, ShareUpdateExclusiveLock);
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * Switch to the table owner's userid, so that any index functions are run
+	 * as that user.  Also lock down security-restricted operations and
+	 * arrange to make GUC variable changes local to this command.
+	 */
+	GetUserIdAndSecContext(&save_userid, &save_sec_context);
+	SetUserIdAndSecContext(onerel->rd_rel->relowner,
+						   save_sec_context | SECURITY_RESTRICTED_OPERATION);
+	save_nestlevel = NewGUCNestLevel();
+
+	/*
+	 * Apply statistical updates, if any, to copied tuple.
+	 *
+	 * Format is:
+	 * {
+	 *   "regular": { "columns": ..., "extended": ...},
+	 *   "inherited": { "columns": ..., "extended": ...}
+	 * }
+	 *
+	 */
+	if (jb != NULL)
+	{
+		JsonbContainer	   *cont;
+
+		cont = key_lookup_object(&jb->root, "regular");
+		import_pg_statistics(onerel, false, cont);
+
+		if (onerel->rd_rel->relhassubclass)
+		{
+			cont = key_lookup_object(&jb->root, "inherited");
+			import_pg_statistics(onerel, true, cont);
+		}
+	}
+
+	/* only modify pg_class row if changes are to be made */
+	if ( ! PG_ARGISNULL(2) || ! PG_ARGISNULL(3) )
+	{
+		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 (! PG_ARGISNULL(2))
+			pgcform->reltuples = PG_GETARG_FLOAT4(2);
+		if (! PG_ARGISNULL(3))
+			pgcform->relpages = PG_GETARG_INT32(3);
+
+		heap_inplace_update(pg_class_rel, ctup);
+		table_close(pg_class_rel, ShareUpdateExclusiveLock);
+	}
+
+	/* relation_close(onerel, ShareUpdateExclusiveLock); */
+	relation_close(onerel, NoLock);
+
+	/* Roll back any GUC changes executed by index functions */
+	AtEOXact_GUC(false, save_nestlevel);
+
+	/* Restore userid and security context */
+	SetUserIdAndSecContext(save_userid, save_sec_context);
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * Convert the STATISTICS_KIND strings defined in pg_statistic_export
+ * back to their defined enum values.
+ */
+static int16
+decode_stakind_string(char *s)
+{
+	if (strcmp(s,"MCV") == 0)
+		return STATISTIC_KIND_MCV;
+	if (strcmp(s,"HISTOGRAM") == 0)
+		return STATISTIC_KIND_HISTOGRAM;
+	if (strcmp(s,"CORRELATION") == 0)
+		return STATISTIC_KIND_CORRELATION;
+	if (strcmp(s,"MCELEM") == 0)
+		return STATISTIC_KIND_MCELEM;
+	if (strcmp(s,"DECHIST") == 0)
+		return STATISTIC_KIND_DECHIST;
+	if (strcmp(s,"RANGE_LENGTH_HISTOGRAM") == 0)
+		return STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM;
+	if (strcmp(s,"BOUNDS_HISTOGRAM") == 0)
+		return STATISTIC_KIND_BOUNDS_HISTOGRAM;
+	if (strcmp(s,"TRIVIAL") != 0)
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("unknown statistics kind: %s", s)));
+
+	return 0;
+}
+
+/*
+ *
+ * Format is:
+ *
+ * {
+ *   "regular":
+ *     {
+ *       "columns": { "colname1": ... }},
+ *       "extended": { "statname1": ...}
+ *     },
+ *   "inherited":
+ *     {
+ *       "columns": { "colname1": ... }},
+ *       "extended": { "statname1": ...}
+ *     },
+ * }
+ */
+static
+void import_pg_statistics(Relation onerel, bool inh, JsonbContainer *cont)
+{
+	VacAttrStats	  **vacattrstats;
+	JsonbContainer	   *colscont;
+	JsonbContainer	   *extscont;
+	int					natts;
+	AnlIndexData	   *indexdata = NULL;
+	int					nindexes = 0;
+	int					i,
+						ind;
+	Relation		   *Irel = NULL;
+
+	/* skip if no statistics of this inheritance type available */
+	if (cont == NULL)
+		return;
+
+	vacattrstats = examine_rel_attributes(onerel, &natts);
+
+	if ((!inh) &&
+		(onerel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE))
+	{
+		vac_open_indexes(onerel, AccessShareLock, &nindexes, &Irel);
+		if (nindexes > 0)
+			indexdata = build_indexdata(onerel, Irel, nindexes, true);
+	}
+
+	colscont = key_lookup_object(cont, "columns");
+	if (colscont != NULL)
+	{
+		for (i = 0; i < natts; i++)
+		{
+			VacAttrStats   *stats;
+			JsonbContainer *attrcont;
+			const char	   *name;
+
+			stats = vacattrstats[i];
+			name = NameStr(*attnumAttName(onerel, stats->tupattnum));
+
+			attrcont = key_lookup_object(colscont, name);
+			ImportVacAttrStats(stats, attrcont);
+		}
+		update_attstats(RelationGetRelid(onerel), inh, natts, vacattrstats);
+
+		/* now compute the index stats based on imported table stats */
+		for (ind = 0; ind < nindexes; ind++)
+		{
+			AnlIndexData *thisdata = &indexdata[ind];
+
+			update_attstats(RelationGetRelid(Irel[ind]), false,
+							thisdata->attr_cnt, thisdata->vacattrstats);
+		}
+	}
+
+	extscont = key_lookup_object(cont, "extended");
+
+	if (extscont != NULL)
+	{
+		/* Build extended statistics (if there are any). */
+
+		ImportRelationExtStatistics(onerel, false, natts, vacattrstats, extscont);
+
+		if (onerel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+			ImportRelationExtStatistics(onerel, true, natts, vacattrstats, extscont);
+	}
+
+	if ((!inh) &&
+		(onerel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE))
+		vac_close_indexes(nindexes, Irel, NoLock);
+}
+
+/*
+ * Apply all found statistics within a JSON structure to a pre-examined
+ * VacAttrStats.
+ */
+void
+ImportVacAttrStats( VacAttrStats *stats, JsonbContainer *cont)
+{
+	JsonbContainer *arraycont;
+	char		   *s;
+	int				k;
+
+	/* nothing to import, skip */
+	if (cont == NULL)
+		return;
+
+	stats->stats_valid = true;
+
+	s = key_lookup_cstring(cont, "stanullfrac");
+	if (s != NULL)
+	{
+		stats->stanullfrac = float4in_internal(s, NULL, "real", s, NULL);
+		pfree(s);
+	}
+	s = key_lookup_cstring(cont, "stawidth");
+	if (s != NULL)
+	{
+		stats->stawidth = pg_strtoint32(s);
+		pfree(s);
+	}
+
+	s = key_lookup_cstring(cont, "stadistinct");
+	if (s != NULL)
+	{
+		stats->stadistinct = float4in_internal(s, NULL, "real", s, NULL);
+		pfree(s);
+	}
+
+	arraycont = key_lookup_array(cont, "stakinds");
+	if (arraycont != NULL)
+	{
+		for (k = 0; k < STATISTIC_NUM_SLOTS; k++)
+		{
+			StdAnalyzeData *mystats;
+			JsonbValue	   *e;
+			char		   *stakindstr;
+
+			mystats = (StdAnalyzeData *) stats->extra_data;
+			e = getIthJsonbValueFromContainer(arraycont, k);
+
+			if (e == NULL || (e->type != jbvString))
+				ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("invalid format: stakind elements must be strings")));
+
+			stakindstr = JsonbStringValueToCString(e);
+			stats->stakind[k] = decode_stakind_string(stakindstr);
+			pfree(stakindstr);
+			pfree(e);
+
+			switch(stats->stakind[k])
+			{
+				case STATISTIC_KIND_MCV:
+				case STATISTIC_KIND_DECHIST:
+					stats->staop[k] = mystats->eqopr;
+					stats->stacoll[k] = stats->attrcollid;
+					break;
+
+				case STATISTIC_KIND_HISTOGRAM:
+				case STATISTIC_KIND_CORRELATION:
+					stats->staop[k] = mystats->ltopr;
+					stats->stacoll[k] = stats->attrcollid;
+					break;
+
+				case STATISTIC_KIND_MCELEM:
+					stats->staop[k] = TextEqualOperator;
+					stats->stacoll[k] = DEFAULT_COLLATION_OID;
+					break;
+
+				case STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM:
+					stats->staop[k] = Float8LessOperator;
+					stats->stacoll[k] = InvalidOid;
+					break;
+
+				case STATISTIC_KIND_BOUNDS_HISTOGRAM:
+				default:
+					stats->staop[k] = InvalidOid;
+					stats->stacoll[k] = InvalidOid;
+					break;
+			}
+		}
+	}
+
+	/* stanumbers is an array of arrays of floats */
+	arraycont = key_lookup_array(cont, "stanumbers");
+	if (arraycont != NULL)
+	{
+		for (k = 0; k < STATISTIC_NUM_SLOTS; k++)
+		{
+			JsonbValue	   *j;
+			JsonbContainer *numarr;
+			int				nelems;
+			int				e;
+
+			j = getIthJsonbValueFromContainer(arraycont, k);
+
+			/* skip out-of-bounds and explicit nulls */
+			if (j == NULL)
+				continue;
+
+			if (j->type == jbvNull)
+			{
+				pfree(j);
+				continue;
+			}
+
+			if ((j->type != jbvBinary) ||
+				(!JsonContainerIsArray(j->val.binary.data)))
+				ereport(ERROR,
+				  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				   errmsg("invalid statistics format, stanumbers must be a binary array or null")));
+
+			numarr = j->val.binary.data;
+			pfree(j);
+
+			nelems = JsonContainerSize(numarr);
+			stats->numnumbers[k] = nelems;
+			stats->stanumbers[k] = (float4 *) palloc(nelems * sizeof(float4));
+
+			for (e = 0; e < nelems; e++)
+			{
+				JsonbValue *f = getIthJsonbValueFromContainer(numarr, e);
+				float4		floatval;
+				char	   *fstr;
+
+				/* skip out-of-bounds and explicit nulls */
+				if (f == NULL || f->type != jbvString)
+					ereport(ERROR,
+					  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					   errmsg("invalid statistics format, elements of stanumbers must a string")));
+
+				fstr = JsonbStringValueToCString(f);
+				floatval = float4in_internal(fstr, NULL, "realx3", fstr, NULL);
+				stats->stanumbers[k][e] = floatval;
+				pfree(f);
+				pfree(fstr);
+			}
+		}
+	}
+
+	/* stavalues is an array of arrays of the column attr type */
+	arraycont = key_lookup_array(cont, "stavalues");
+	if (arraycont != NULL)
+	{
+		Oid			in_func;
+		Oid			typioparam;
+		FmgrInfo	finfo;
+
+		getTypeInputInfo(stats->attrtypid, &in_func, &typioparam);
+		fmgr_info(in_func, &finfo);
+
+		for (k = 0; k < STATISTIC_NUM_SLOTS; k++)
+		{
+			JsonbValue	   *j;
+			JsonbContainer *attrarr;
+			int				nelems;
+			int				e;
+
+			j = getIthJsonbValueFromContainer(arraycont, k);
+
+			/* skip out-of-bounds and explicit nulls */
+			if (j == NULL)
+				continue;
+
+			if (j->type == jbvNull)
+			{
+				pfree(j);
+				continue;
+			}
+
+			if ((j->type != jbvBinary) ||
+				(!JsonContainerIsArray(j->val.binary.data)))
+				ereport(ERROR,
+				  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				   errmsg("invalid statistics format, stavalues must be a binary array or null")));
+
+			attrarr = j->val.binary.data;
+			pfree(j);
+			nelems = JsonContainerSize(attrarr);
+			stats->numvalues[k] = nelems;
+			stats->stavalues[k] = (Datum *) palloc(nelems * sizeof(Datum));
+
+			for (e = 0; e < nelems; e++)
+			{
+				JsonbValue *f;
+				char	   *fstr;
+				Datum		datum;
+
+				f = getIthJsonbValueFromContainer(attrarr, e);
+
+				/* skip out-of-bounds and explicit nulls */
+				if (f == NULL || f->type != jbvString)
+					ereport(ERROR,
+					  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					   errmsg("invalid statistics format, elements of stavalues must be a string")));
+
+				fstr = JsonbStringValueToCString(f);
+				datum = InputFunctionCall(&finfo, fstr, typioparam, stats->attrtypmod);
+				stats->stavalues[k][e] = datum;
+				pfree(fstr);
+				pfree(f);
+			}
+		}
+	}
+}
diff --git a/src/backend/statistics/dependencies.c b/src/backend/statistics/dependencies.c
index edb2e5347d..f410510a28 100644
--- a/src/backend/statistics/dependencies.c
+++ b/src/backend/statistics/dependencies.c
@@ -27,7 +27,9 @@
 #include "parser/parsetree.h"
 #include "statistics/extended_stats_internal.h"
 #include "statistics/statistics.h"
+#include "utils/builtins.h"
 #include "utils/bytea.h"
+#include "utils/float.h"
 #include "utils/fmgroids.h"
 #include "utils/fmgrprotos.h"
 #include "utils/lsyscache.h"
@@ -1829,3 +1831,134 @@ dependencies_clauselist_selectivity(PlannerInfo *root,
 
 	return s1;
 }
+
+
+/*
+ * imports functional dependencies between groups of columns
+ *
+ * Generates all possible subsets of columns (variations) and computes
+ * the degree of validity for each one. For example when creating statistics
+ * on three columns (a,b,c) there are 9 possible dependencies
+ *
+ *	   two columns			  three columns
+ *	   -----------			  -------------
+ *	   (a) -> b				  (a,b) -> c
+ *	   (a) -> c				  (a,c) -> b
+ *	   (b) -> a				  (b,c) -> a
+ *	   (b) -> c
+ *	   (c) -> a
+ *	   (c) -> b
+ */
+MVDependencies *
+import_dependencies(JsonbContainer *cont)
+{
+	MVDependencies *dependencies = NULL;
+
+	int			numcombs;
+	int			i;
+
+	/* no dependencies to import */
+	if (cont == NULL)
+		return NULL;
+
+	/*
+	 *
+	 * format example:
+	 *
+	 * "stxdndependencies": <= you are here
+	 *   [
+	 *     {
+	 *       "attnums": [ "2", "-1" ],
+	 *       "degree": "0.500"
+	 *     },
+	 *     ...
+	 *   ]
+	 *
+	 */
+	numcombs = JsonContainerSize(cont);
+
+	/* no dependencies to import */
+	if (numcombs == 0)
+		return NULL;
+
+	dependencies = palloc(offsetof(MVDependencies, deps)
+							+ numcombs * sizeof(MVDependency *));
+
+	dependencies->magic = STATS_DEPS_MAGIC;
+	dependencies->type = STATS_DEPS_TYPE_BASIC;
+	dependencies->ndeps = numcombs;
+
+	for (i = 0; i < numcombs; i++)
+	{
+		MVDependency	   *d = dependencies->deps[i];
+		JsonbValue		   *elem;
+		JsonbContainer	   *dep;
+		JsonbValue		   *attnums;
+		JsonbContainer	   *attnumscont;
+		JsonbValue		   *degree;
+		char			   *degree_str;
+		int					numattnums;
+		int					j;
+
+		elem = getIthJsonbValueFromContainer(cont, i);
+
+		if ((elem->type != jbvBinary) ||
+			(!JsonContainerIsObject(elem->val.binary.data)))
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("invalid statistics format, stxnddependencies elements must be binary objects")));
+
+		dep = elem->val.binary.data;
+		pfree(elem);
+
+		attnums = getKeyJsonValueFromContainer(dep, "attnums", strlen("attnums"), NULL);
+		if ((attnums == NULL) || (attnums->type != jbvBinary) ||
+			(!JsonContainerIsArray(attnums->val.binary.data)))
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("invalid statistics format, stxndependencies must contain attnums array")));
+
+		attnumscont = attnums->val.binary.data;
+		pfree(attnums);
+		numattnums = JsonContainerSize(attnumscont);
+
+		d = palloc(offsetof(MVDependency, attributes)
+							+ numattnums * sizeof(AttrNumber));
+
+		d->nattributes = numattnums;
+
+		for (j = 0; j < numattnums; j++)
+		{
+			JsonbValue	   *attnum;
+			char		   *s;
+
+			attnum = getIthJsonbValueFromContainer(attnumscont, j);
+
+			if (attnum->type != jbvString)
+				ereport(ERROR,
+				  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				   errmsg("invalid statistics format, stxndependencies attnums elements must be strings")));
+
+			s = JsonbStringValueToCString(attnum);
+			pfree(attnum);
+
+			d->attributes[j] = pg_strtoint16(s);
+			pfree(s);
+		}
+
+		degree = getKeyJsonValueFromContainer(dep, "degree", strlen("degree"), NULL);
+		if ((degree == NULL) || (degree->type != jbvString))
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("invalid statistics format, stxndependencies elements must have degree element")));
+
+		degree_str = JsonbStringValueToCString(degree);
+		pfree(degree);
+		d->degree = float8in_internal(degree_str, NULL, "double", degree_str, NULL);
+		pfree(degree_str);
+		dependencies->deps[i] = d;
+	}
+
+
+	return dependencies;
+}
diff --git a/src/backend/statistics/extended_stats.c b/src/backend/statistics/extended_stats.c
index 9f67a57724..4e6e444f4c 100644
--- a/src/backend/statistics/extended_stats.c
+++ b/src/backend/statistics/extended_stats.c
@@ -2636,3 +2636,262 @@ make_build_data(Relation rel, StatExtEntry *stat, int numrows, HeapTuple *rows,
 
 	return result;
 }
+
+/* form an array of pg_statistic rows (per update_attstats) */
+static Datum
+serialize_vacattrstats(VacAttrStats **exprstats, int nexprs)
+{
+	int			exprno;
+	Oid			typOid;
+	Relation	sd;
+
+	ArrayBuildState *astate = NULL;
+
+	sd = table_open(StatisticRelationId, RowExclusiveLock);
+
+	/* lookup OID of composite type for pg_statistic */
+	typOid = get_rel_type_id(StatisticRelationId);
+	if (!OidIsValid(typOid))
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("relation \"%s\" does not have a composite type",
+						"pg_statistic")));
+
+	for (exprno = 0; exprno < nexprs; exprno++)
+	{
+		int			i,
+					k;
+		VacAttrStats *stats = exprstats[exprno];
+
+		Datum		values[Natts_pg_statistic];
+		bool		nulls[Natts_pg_statistic];
+		HeapTuple	stup;
+
+		if (!stats->stats_valid)
+		{
+			astate = accumArrayResult(astate,
+									  (Datum) 0,
+									  true,
+									  typOid,
+									  CurrentMemoryContext);
+			continue;
+		}
+
+		/*
+		 * Construct a new pg_statistic tuple
+		 */
+		for (i = 0; i < Natts_pg_statistic; ++i)
+		{
+			nulls[i] = false;
+		}
+
+		values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(InvalidAttrNumber);
+		values[Anum_pg_statistic_stainherit - 1] = BoolGetDatum(false);
+		values[Anum_pg_statistic_stanullfrac - 1] = Float4GetDatum(stats->stanullfrac);
+		values[Anum_pg_statistic_stawidth - 1] = Int32GetDatum(stats->stawidth);
+		values[Anum_pg_statistic_stadistinct - 1] = Float4GetDatum(stats->stadistinct);
+		i = Anum_pg_statistic_stakind1 - 1;
+		for (k = 0; k < STATISTIC_NUM_SLOTS; k++)
+		{
+			values[i++] = Int16GetDatum(stats->stakind[k]); /* stakindN */
+		}
+		i = Anum_pg_statistic_staop1 - 1;
+		for (k = 0; k < STATISTIC_NUM_SLOTS; k++)
+		{
+			values[i++] = ObjectIdGetDatum(stats->staop[k]);	/* staopN */
+		}
+		i = Anum_pg_statistic_stacoll1 - 1;
+		for (k = 0; k < STATISTIC_NUM_SLOTS; k++)
+		{
+			values[i++] = ObjectIdGetDatum(stats->stacoll[k]);	/* stacollN */
+		}
+		i = Anum_pg_statistic_stanumbers1 - 1;
+		for (k = 0; k < STATISTIC_NUM_SLOTS; k++)
+		{
+			int			nnum = stats->numnumbers[k];
+
+			if (nnum > 0)
+			{
+				int			n;
+				Datum	   *numdatums = (Datum *) palloc(nnum * sizeof(Datum));
+				ArrayType  *arry;
+
+				for (n = 0; n < nnum; n++)
+					numdatums[n] = Float4GetDatum(stats->stanumbers[k][n]);
+				arry = construct_array_builtin(numdatums, nnum, FLOAT4OID);
+				values[i++] = PointerGetDatum(arry);	/* stanumbersN */
+			}
+			else
+			{
+				nulls[i] = true;
+				values[i++] = (Datum) 0;
+			}
+		}
+		i = Anum_pg_statistic_stavalues1 - 1;
+		for (k = 0; k < STATISTIC_NUM_SLOTS; k++)
+		{
+			if (stats->numvalues[k] > 0)
+			{
+				ArrayType  *arry;
+
+				arry = construct_array(stats->stavalues[k],
+									   stats->numvalues[k],
+									   stats->statypid[k],
+									   stats->statyplen[k],
+									   stats->statypbyval[k],
+									   stats->statypalign[k]);
+				values[i++] = PointerGetDatum(arry);	/* stavaluesN */
+			}
+			else
+			{
+				nulls[i] = true;
+				values[i++] = (Datum) 0;
+			}
+		}
+
+		stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
+
+		astate = accumArrayResult(astate,
+								  heap_copy_tuple_as_datum(stup, RelationGetDescr(sd)),
+								  false,
+								  typOid,
+								  CurrentMemoryContext);
+	}
+
+	table_close(sd, RowExclusiveLock);
+
+	return makeArrayResult(astate, CurrentMemoryContext);
+}
+
+/*
+ * Import requested extended stats, using the pre-computed (single-column)
+ * stats.
+ *
+ * This fetches a list of stats types from pg_statistic_ext, extracts the
+ * requested stats from the jsonb, and serializes them back into the catalog.
+ */
+void
+ImportRelationExtStatistics(Relation onerel, bool inh,
+						   int natts,
+						   VacAttrStats **vacattrstats,
+						   JsonbContainer *cont)
+{
+	Relation	pg_stext;
+	ListCell   *lc;
+	List	   *statslist;
+
+	/* Do nothing if there are no columns to analyze. */
+	if (!natts)
+		return;
+
+	/* Do nothing if there are no stats to import */
+	if (cont == NULL)
+		return;
+
+	/* the list of stats has to be allocated outside the memory context */
+	pg_stext = table_open(StatisticExtRelationId, RowExclusiveLock);
+	statslist = fetch_statentries_for_relation(pg_stext, RelationGetRelid(onerel));
+
+	/*
+	 * format:
+	 *
+	 * { <= you are here
+	 *   <stat-name>:
+	 *     {
+	 *        "stxkinds": array of single characters (up to 3?),
+	 *        "stxdndistinct": [ {ndistinct}, ... ],
+	 *        "stxdndependencies": [ {dependency}, ... ]
+	 *        "stxdmcv": [ {mcv}, ... ]
+	 *        "stxdexprs" : [ {pg_statistic}, ... ]
+	 *     },
+	 *     ...
+	 * }
+	 */
+
+	foreach(lc, statslist)
+	{
+		StatExtEntry *stat = (StatExtEntry *) lfirst(lc);
+		MVNDistinct *ndistinct = NULL;
+		MVDependencies *dependencies = NULL;
+		MCVList    *mcv = NULL;
+		Datum		exprstats = (Datum) 0;
+		VacAttrStats **stats;
+		JsonbValue	   *exprval;
+		JsonbContainer *attrcont,
+					   *ndistcont,
+					   *depcont,
+					   *exprcont;
+
+		/*
+		 * Check if we can build these stats based on the column analyzed. If
+		 * not, report this fact (except in autovacuum) and move on.
+		 */
+		stats = lookup_var_attr_stats(onerel, stat->columns, stat->exprs,
+									  natts, vacattrstats);
+		if (!stats)
+			continue;
+
+		attrcont = key_lookup_object(cont, stat->name);
+
+		/* staname not found, skip */
+		if (attrcont == NULL)
+			continue;
+
+		ndistcont = key_lookup_array(attrcont, "stxdndistinct");
+
+		if (ndistcont != NULL)
+			ndistinct = import_ndistinct(ndistcont);
+
+		depcont = key_lookup_array(attrcont, "stxdndependencies");
+		if (depcont != NULL)
+			dependencies = import_dependencies(depcont);
+
+		/* TODO mcvcont for "stxdmcv" and import_mcv() */
+
+		exprval = getKeyJsonValueFromContainer(attrcont, "stxdexprs", strlen("stxdexprs"), NULL);
+		if (exprval != NULL)
+		{
+			if (exprval->type != jbvNull)
+			{
+				int	nexprs;
+				int	k;
+
+				if (!JsonContainerIsArray(exprval->val.binary.data))
+			   		errmsg("invalid statistics format, stxndeprs must be array or null");
+
+				exprcont = exprval->val.binary.data;
+
+				nexprs = JsonContainerSize(exprcont);
+
+				for (k = 0; k < nexprs; k++)
+				{
+					JsonbValue *statelem;
+
+					statelem = getIthJsonbValueFromContainer(exprcont, k);
+					if ((statelem->type != jbvBinary) ||
+						(!JsonContainerIsObject(statelem->val.binary.data)))
+						ereport(ERROR,
+						  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						   errmsg("invalid statistics format, stxdexprs elements must be binary objects")));
+
+					ImportVacAttrStats(stats[k], statelem->val.binary.data);
+					pfree(statelem);
+				}
+
+				exprstats = serialize_vacattrstats(stats, nexprs);
+
+			}
+			pfree(exprval);
+		}
+
+
+		/* store the statistics in the catalog */
+		statext_store(stat->statOid, inh,
+					  ndistinct, dependencies, mcv, exprstats, stats);
+	}
+
+	list_free(statslist);
+
+	table_close(pg_stext, RowExclusiveLock);
+}
diff --git a/src/backend/statistics/mcv.c b/src/backend/statistics/mcv.c
index 03b9f04bb5..8fbb388028 100644
--- a/src/backend/statistics/mcv.c
+++ b/src/backend/statistics/mcv.c
@@ -2177,3 +2177,9 @@ mcv_clause_selectivity_or(PlannerInfo *root, StatisticExtInfo *stat,
 
 	return s;
 }
+
+MCVList *import_mcv(JsonbContainer *cont)
+{
+	return NULL;
+}
+
diff --git a/src/backend/statistics/mvdistinct.c b/src/backend/statistics/mvdistinct.c
index 6d25c14644..8489a758a6 100644
--- a/src/backend/statistics/mvdistinct.c
+++ b/src/backend/statistics/mvdistinct.c
@@ -31,6 +31,8 @@
 #include "lib/stringinfo.h"
 #include "statistics/extended_stats_internal.h"
 #include "statistics/statistics.h"
+#include "utils/builtins.h"
+#include "utils/float.h"
 #include "utils/fmgrprotos.h"
 #include "utils/lsyscache.h"
 #include "utils/syscache.h"
@@ -698,3 +700,121 @@ generate_combinations(CombinationGenerator *state)
 
 	pfree(current);
 }
+
+
+/*
+ * Import ndistinct values from JsonB.
+ *
+ * To handle expressions easily, we treat them as system attributes with
+ * negative attnums, and offset everything by number of expressions to
+ * allow using Bitmapsets.
+ */
+MVNDistinct *
+import_ndistinct(JsonbContainer *cont)
+{
+	MVNDistinct *result;
+	int			numcombs;
+	int			i;
+
+	/* no ndistinct to import */
+	if (cont == NULL)
+		return NULL;
+
+	/* each row of the JSON is a combination */
+	numcombs = JsonContainerSize(cont);
+
+	/* no ndistinct to import */
+	if (numcombs == 0)
+		return NULL;
+
+	result = palloc(offsetof(MVNDistinct, items) +
+					numcombs * sizeof(MVNDistinctItem));
+	result->magic = STATS_NDISTINCT_MAGIC;
+	result->type = STATS_NDISTINCT_TYPE_BASIC;
+	result->nitems = numcombs;
+
+	/*
+	 * format example:
+	 *
+	 * "stxdndistinct": [
+	 *     {
+	 *       "attnums": [ "2", "1" ],
+	 *       "ndistinct": "4"
+	 *     },
+	 *     ...
+	 *   ]
+	 *
+	 */
+	for (i = 0; i < numcombs; i++)
+	{
+		MVNDistinctItem	   *item;
+		JsonbValue		   *elem;
+		JsonbContainer	   *combo;
+		JsonbValue		   *attnums;
+		JsonbContainer	   *attnumscont;
+		JsonbValue		   *ndistinct;
+		char			   *ndist_str;
+		int					numattnums;
+		int					j;
+
+		item = &result->items[i];
+
+		elem = getIthJsonbValueFromContainer(cont, i);
+
+		if ((elem->type != jbvBinary) ||
+			(!JsonContainerIsObject(elem->val.binary.data)))
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("invalid statistics format, stxndistinct elements must be a binary objects")));
+
+		combo = elem->val.binary.data;
+
+		attnums = getKeyJsonValueFromContainer(combo, "attnums", strlen("attnums"), NULL);
+		if ((attnums == NULL) || (attnums->type != jbvBinary) ||
+			(!JsonContainerIsArray(attnums->val.binary.data)))
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("invalid statistics format, stxndistinct must contain attnums array")));
+
+		attnumscont = attnums->val.binary.data;
+		numattnums = JsonContainerSize(attnumscont);
+
+		item->attributes = palloc(sizeof(AttrNumber) * numattnums);
+		item->nattributes = numattnums;
+
+		for (j = 0; j < numattnums; j++)
+		{
+			JsonbValue	   *attnum;
+			char		   *s;
+			attnum = getIthJsonbValueFromContainer(attnumscont, j);
+
+			if (attnum->type != jbvString)
+				ereport(ERROR,
+				  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				   errmsg("invalid statistics format, stxndistinct attnums elements must be strings, but one is %s", 
+						  JsonbTypeName(attnum))));
+
+			s = JsonbStringValueToCString(attnum);
+
+			item->attributes[j] = pg_strtoint16(s);
+			pfree(s);
+			pfree(attnum);
+		}
+
+		ndistinct = getKeyJsonValueFromContainer(combo, "ndistinct", strlen("ndistinct"), NULL);
+		if ((ndistinct == NULL) || (ndistinct->type != jbvString))
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("invalid statistics format, stxndistinct elements must have ndistinct element")));
+
+		ndist_str = JsonbStringValueToCString(ndistinct);
+		item->ndistinct = float8in_internal(ndist_str, NULL, "double", ndist_str, NULL);
+
+		pfree(ndist_str);
+		pfree(ndistinct);
+		pfree(attnums);
+		pfree(elem);
+	}
+
+	return result;
+}
diff --git a/src/test/regress/expected/vacuum.out b/src/test/regress/expected/vacuum.out
index 4def90b805..4e609ca3b6 100644
--- a/src/test/regress/expected/vacuum.out
+++ b/src/test/regress/expected/vacuum.out
@@ -508,3 +508,156 @@ RESET ROLE;
 DROP TABLE vacowned;
 DROP TABLE vacowned_parted;
 DROP ROLE regress_vacuum;
+CREATE TYPE stats_import_complex_type AS (
+    a integer,
+    b float,
+    c text,
+    d date,
+    e jsonb);
+CREATE TABLE stats_import_test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_import_complex_type
+);
+INSERT INTO stats_import_test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_import_complex_type
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_import_complex_type
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import_complex_type
+UNION ALL
+SELECT 4, 'four', NULL;
+CREATE INDEX is_odd ON stats_import_test(((comp).a % 2 = 1));
+CREATE STATISTICS oddness ON name, ((comp).a % 2 = 1) FROM stats_import_test;
+ANALYZE stats_import_test;
+CREATE TABLE stats_export AS
+SELECT e.*
+FROM pg_catalog.pg_statistic_export AS e
+WHERE e.schemaname = 'public'
+AND e.relname = 'stats_import_test';
+SELECT c.reltuples AS before_tuples, c.relpages AS before_pages
+FROM pg_class AS c
+WHERE oid = 'stats_import_test'::regclass;
+ before_tuples | before_pages 
+---------------+--------------
+             4 |            1
+(1 row)
+
+-- test settting tuples and pages but no columns
+SELECT pg_import_rel_stats(c.oid, current_setting('server_version_num')::integer,
+                           1000.0, 200, NULL::jsonb)
+FROM pg_class AS c
+WHERE oid = 'stats_import_test'::regclass;
+ pg_import_rel_stats 
+---------------------
+ t
+(1 row)
+
+SELECT c.reltuples AS after_tuples, c.relpages AS after_pages
+FROM pg_class AS c
+WHERE oid = 'stats_import_test'::regclass;
+ after_tuples | after_pages 
+--------------+-------------
+         1000 |         200
+(1 row)
+
+CREATE TEMPORARY TABLE orig_stats
+AS
+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 AS s
+WHERE s.starelid = 'stats_import_test'::regclass;
+CREATE TEMPORARY TABLE orig_export
+AS
+SELECT e.*
+FROM stats_export AS e
+WHERE e.schemaname = 'public'
+AND e.relname = 'stats_import_test';
+-- create a table just like stats_import_test
+CREATE TABLE stats_import_clone ( LIKE stats_import_test );
+-- create an index that does not exist on stats_import_test
+CREATE INDEX is_even ON stats_import_clone(((comp).a % 2 = 0));
+-- rename statistics object to allow for a same-named one on the clone
+ALTER STATISTICS oddness RENAME TO oddness_original;
+CREATE STATISTICS oddness ON name, ((comp).a % 2 = 1) FROM stats_import_clone;
+-- copy table stats to clone table
+SELECT pg_import_rel_stats(c.oid, e.server_version_num,
+                            e.n_tuples, e.n_pages, e.stats)
+FROM pg_class AS c
+JOIN pg_namespace AS n
+ON n.oid = c.relnamespace
+JOIN orig_export AS e
+ON e.schemaname = 'public'
+AND e.relname = 'stats_import_test'
+WHERE c.oid = 'stats_import_clone'::regclass;
+ pg_import_rel_stats 
+---------------------
+ t
+(1 row)
+
+-- stats should match
+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 AS s
+WHERE s.starelid = 'stats_import_test'::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,
+        stavalues1::text AS sv1, stavalues2::text AS sv2, stavalues3::text AS sv3,
+        stavalues4::text AS sv4, stavalues5::text AS sv5
+FROM pg_statistic AS s
+WHERE s.starelid = 'stats_import_clone'::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)
+
+-- compare all except stxdmcv and oid-related stxdexprs
+SELECT d.stxdinherit, d.stxdndistinct, d.stxddependencies, xpr.xpr
+FROM pg_statistic_ext AS e
+JOIN pg_statistic_ext_data AS d ON d.stxoid = e.oid
+LEFT JOIN LATERAL (
+    SELECT
+        array_agg(ROW(x.staattnum, x.stainherit, x.stanullfrac, x.stawidth,
+                      x.stadistinct, x.stakind1, x.stakind2, x.stakind3,
+                      x.stakind4, x.stakind5, x.stanumbers1, x.stanumbers2,
+                      x.stanumbers3, x.stanumbers4, x.stanumbers5, x.stavalues1,
+                      x.stavalues2, x.stavalues3, x.stavalues4, x.stavalues5
+                      )::text ORDER BY x.ordinality)
+    FROM unnest(d.stxdexpr) WITH ORDINALITY AS x
+) AS xpr(xpr) ON d.stxdexpr IS NOT NULL
+WHERE e.stxrelid = 'stats_import_test'::regclass
+AND e.stxname = 'oddness_original'
+EXCEPT
+SELECT d.stxdinherit, d.stxdndistinct, d.stxddependencies, xpr.xpr
+FROM pg_statistic_ext AS e
+JOIN pg_statistic_ext_data AS d ON d.stxoid = e.oid
+LEFT JOIN LATERAL (
+    SELECT
+        array_agg(ROW(x.staattnum, x.stainherit, x.stanullfrac, x.stawidth,
+                      x.stadistinct, x.stakind1, x.stakind2, x.stakind3,
+                      x.stakind4, x.stakind5, x.stanumbers1, x.stanumbers2,
+                      x.stanumbers3, x.stanumbers4, x.stanumbers5, x.stavalues1,
+                      x.stavalues2, x.stavalues3, x.stavalues4, x.stavalues5
+                      )::text ORDER BY x.ordinality)
+    FROM unnest(d.stxdexpr) WITH ORDINALITY AS x
+) AS xpr(xpr) ON d.stxdexpr IS NOT NULL
+WHERE e.stxrelid = 'stats_import_clone'::regclass
+AND e.stxname = 'oddness';
+ stxdinherit | stxdndistinct | stxddependencies | xpr 
+-------------+---------------+------------------+-----
+(0 rows)
+
+DROP TABLE stats_export;
+DROP TABLE stats_import_clone;
+DROP TABLE stats_import_test;
+DROP TYPE stats_import_complex_type;
diff --git a/src/test/regress/sql/vacuum.sql b/src/test/regress/sql/vacuum.sql
index 51d7b1fecc..b42f35a8e3 100644
--- a/src/test/regress/sql/vacuum.sql
+++ b/src/test/regress/sql/vacuum.sql
@@ -377,3 +377,149 @@ RESET ROLE;
 DROP TABLE vacowned;
 DROP TABLE vacowned_parted;
 DROP ROLE regress_vacuum;
+
+
+CREATE TYPE stats_import_complex_type AS (
+    a integer,
+    b float,
+    c text,
+    d date,
+    e jsonb);
+
+CREATE TABLE stats_import_test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_import_complex_type
+);
+
+INSERT INTO stats_import_test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_import_complex_type
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_import_complex_type
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import_complex_type
+UNION ALL
+SELECT 4, 'four', NULL;
+
+CREATE INDEX is_odd ON stats_import_test(((comp).a % 2 = 1));
+
+CREATE STATISTICS oddness ON name, ((comp).a % 2 = 1) FROM stats_import_test;
+
+ANALYZE stats_import_test;
+
+CREATE TABLE stats_export AS
+SELECT e.*
+FROM pg_catalog.pg_statistic_export AS e
+WHERE e.schemaname = 'public'
+AND e.relname = 'stats_import_test';
+
+SELECT c.reltuples AS before_tuples, c.relpages AS before_pages
+FROM pg_class AS c
+WHERE oid = 'stats_import_test'::regclass;
+
+-- test settting tuples and pages but no columns
+SELECT pg_import_rel_stats(c.oid, current_setting('server_version_num')::integer,
+                           1000.0, 200, NULL::jsonb)
+FROM pg_class AS c
+WHERE oid = 'stats_import_test'::regclass;
+
+SELECT c.reltuples AS after_tuples, c.relpages AS after_pages
+FROM pg_class AS c
+WHERE oid = 'stats_import_test'::regclass;
+
+CREATE TEMPORARY TABLE orig_stats
+AS
+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 AS s
+WHERE s.starelid = 'stats_import_test'::regclass;
+
+CREATE TEMPORARY TABLE orig_export
+AS
+SELECT e.*
+FROM stats_export AS e
+WHERE e.schemaname = 'public'
+AND e.relname = 'stats_import_test';
+
+-- create a table just like stats_import_test
+CREATE TABLE stats_import_clone ( LIKE stats_import_test );
+
+-- create an index that does not exist on stats_import_test
+CREATE INDEX is_even ON stats_import_clone(((comp).a % 2 = 0));
+
+-- rename statistics object to allow for a same-named one on the clone
+ALTER STATISTICS oddness RENAME TO oddness_original;
+
+CREATE STATISTICS oddness ON name, ((comp).a % 2 = 1) FROM stats_import_clone;
+
+-- copy table stats to clone table
+SELECT pg_import_rel_stats(c.oid, e.server_version_num,
+                            e.n_tuples, e.n_pages, e.stats)
+FROM pg_class AS c
+JOIN pg_namespace AS n
+ON n.oid = c.relnamespace
+JOIN orig_export AS e
+ON e.schemaname = 'public'
+AND e.relname = 'stats_import_test'
+WHERE c.oid = 'stats_import_clone'::regclass;
+
+-- stats should match
+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 AS s
+WHERE s.starelid = 'stats_import_test'::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,
+        stavalues1::text AS sv1, stavalues2::text AS sv2, stavalues3::text AS sv3,
+        stavalues4::text AS sv4, stavalues5::text AS sv5
+FROM pg_statistic AS s
+WHERE s.starelid = 'stats_import_clone'::regclass;
+
+-- compare all except stxdmcv and oid-related stxdexprs
+SELECT d.stxdinherit, d.stxdndistinct, d.stxddependencies, xpr.xpr
+FROM pg_statistic_ext AS e
+JOIN pg_statistic_ext_data AS d ON d.stxoid = e.oid
+LEFT JOIN LATERAL (
+    SELECT
+        array_agg(ROW(x.staattnum, x.stainherit, x.stanullfrac, x.stawidth,
+                      x.stadistinct, x.stakind1, x.stakind2, x.stakind3,
+                      x.stakind4, x.stakind5, x.stanumbers1, x.stanumbers2,
+                      x.stanumbers3, x.stanumbers4, x.stanumbers5, x.stavalues1,
+                      x.stavalues2, x.stavalues3, x.stavalues4, x.stavalues5
+                      )::text ORDER BY x.ordinality)
+    FROM unnest(d.stxdexpr) WITH ORDINALITY AS x
+) AS xpr(xpr) ON d.stxdexpr IS NOT NULL
+WHERE e.stxrelid = 'stats_import_test'::regclass
+AND e.stxname = 'oddness_original'
+EXCEPT
+SELECT d.stxdinherit, d.stxdndistinct, d.stxddependencies, xpr.xpr
+FROM pg_statistic_ext AS e
+JOIN pg_statistic_ext_data AS d ON d.stxoid = e.oid
+LEFT JOIN LATERAL (
+    SELECT
+        array_agg(ROW(x.staattnum, x.stainherit, x.stanullfrac, x.stawidth,
+                      x.stadistinct, x.stakind1, x.stakind2, x.stakind3,
+                      x.stakind4, x.stakind5, x.stanumbers1, x.stanumbers2,
+                      x.stanumbers3, x.stanumbers4, x.stanumbers5, x.stavalues1,
+                      x.stavalues2, x.stavalues3, x.stavalues4, x.stavalues5
+                      )::text ORDER BY x.ordinality)
+    FROM unnest(d.stxdexpr) WITH ORDINALITY AS x
+) AS xpr(xpr) ON d.stxdexpr IS NOT NULL
+WHERE e.stxrelid = 'stats_import_clone'::regclass
+AND e.stxname = 'oddness';
+
+DROP TABLE stats_export;
+DROP TABLE stats_import_clone;
+DROP TABLE stats_import_test;
+DROP TYPE stats_import_complex_type;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index c76ec52c55..61e01d1b90 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -28014,6 +28014,48 @@ 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>server_version_num</parameter> <type>integer</type>, <parameter>num_tuples</parameter> <type>float4</type>, <parameter>num_pages</parameter> <type>integer</type>, <parameter>column_stats</parameter> <type>jsonb</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>column_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
+        is used by <program>pg_upgrade</program> and <program>pg_restore</program>
+        to convey the statistics from the old system version into the new one.
+       </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
    <table id="functions-admin-dblocation">
     <title>Database Object Location Functions</title>
     <tgroup cols="1">
-- 
2.41.0

v2-0004-Add-pg_export_stats-pg_import_stats.patchtext/x-patch; charset=US-ASCII; name=v2-0004-Add-pg_export_stats-pg_import_stats.patchDownload
From da9b9a4012757a8bb08711d4479ce5ebb38e974d Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Tue, 31 Oct 2023 03:13:24 -0400
Subject: [PATCH v2 4/4] Add pg_export_stats, pg_import_stats.

pg_export_stats is used to export stats from databases as far back as
v10. The output is currently only to stdout and should be redirected to
a file in most use cases.

pg_import_stats is used to import stats to any version that has the
function pg_import_rel_stats().
---
 src/bin/scripts/Makefile          |   6 +-
 src/bin/scripts/pg_export_stats.c | 946 ++++++++++++++++++++++++++++++
 src/bin/scripts/pg_import_stats.c | 303 ++++++++++
 3 files changed, 1254 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/Makefile b/src/bin/scripts/Makefile
index a7a9d0fea5..da45fcfa6a 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)
@@ -31,6 +31,8 @@ clusterdb: clusterdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport su
 vacuumdb: vacuumdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
 reindexdb: reindexdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
 pg_isready: pg_isready.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
+pg_export_stats: pg_export_stats.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
+pg_import_stats: pg_import_stats.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
 
 install: all installdirs
 	$(INSTALL_PROGRAM) createdb$(X)   '$(DESTDIR)$(bindir)'/createdb$(X)
@@ -41,6 +43,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..b9fc192e8f
--- /dev/null
+++ b/src/bin/scripts/pg_export_stats.c
@@ -0,0 +1,946 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_export_stats
+ *
+ * Portions Copyright (c) 1996-2023, 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);
+
+/* view definition introduced in 17 */
+const char *export_query_v17 =
+	"SELECT schemaname, relname, server_version_num, n_tuples, "
+	"n_pages, stats FROM pg_statistic_export ";
+
+/* v15-v16 have the same stats layout, but lacks view definition */
+const char *export_query_v15 =
+	"SELECT "
+	"    n.nspname AS schemaname, "
+	"    r.relname AS relname, "
+	"    current_setting('server_version_num')::integer AS server_version_num, "
+	"    r.reltuples::float4 AS n_tuples, "
+	"    r.relpages::integer AS n_pages, "
+	"    ( "
+	"        WITH per_column_stats AS "
+	"        ( "
+	"            SELECT "
+	"                s.stainherit, "
+	"                a.attname, "
+	"                jsonb_build_object( "
+	"                    'stanullfrac', s.stanullfrac::text, "
+	"                    'stawidth', s.stawidth::text, "
+	"                    'stadistinct', s.stadistinct::text, "
+	"                    'stakinds', "
+	"                    ( "
+	"                        SELECT "
+	"                            jsonb_agg( "
+	"                                CASE kind.kind "
+	"                                    WHEN 0 THEN 'TRIVIAL' "
+	"                                    WHEN 1 THEN 'MCV' "
+	"                                    WHEN 2 THEN 'HISTOGRAM' "
+	"                                    WHEN 3 THEN 'CORRELATION' "
+	"                                    WHEN 4 THEN 'MCELEM' "
+	"                                    WHEN 5 THEN 'DECHIST' "
+	"                                    WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM' "
+	"                                    WHEN 7 THEN 'BOUNDS_HISTOGRAM' "
+	"                                END::text "
+	"                                ORDER BY kind.ord) "
+	"                        FROM unnest(ARRAY[s.stakind1, s.stakind2, "
+	"                                    s.stakind3, stakind4, "
+	"                                    s.stakind5]) "
+	"                             WITH ORDINALITY AS kind(kind, ord) "
+	"                    ), "
+	"                    'stanumbers', "
+	"                    jsonb_build_array( "
+	"                        s.stanumbers1::text::text[], "
+	"                        s.stanumbers2::text::text[], "
+	"                        s.stanumbers3::text::text[], "
+	"                        s.stanumbers4::text::text[], "
+	"                        s.stanumbers5::text::text[]), "
+	"                    'stavalues', "
+	"                    jsonb_build_array( "
+	"                        s.stavalues1::text::text[], "
+	"                        s.stavalues2::text::text[], "
+	"                        s.stavalues3::text::text[], "
+	"                        s.stavalues4::text::text[], "
+	"                        s.stavalues5::text::text[]) "
+	"                ) AS stats "
+	"            FROM pg_attribute AS a "
+	"            JOIN pg_statistic AS s "
+	"                ON s.starelid = a.attrelid "
+	"                AND s.staattnum = a.attnum "
+	"            WHERE a.attrelid = r.oid "
+	"            AND NOT a.attisdropped "
+	"            AND a.attnum > 0 "
+	"            AND has_column_privilege(a.attrelid, a.attnum, 'SELECT') "
+	"        ), "
+	"        attagg AS "
+	"        ( "
+	"            SELECT "
+	"                pcs.stainherit, "
+	"                jsonb_build_object( "
+	"                    'columns', "
+	"                    jsonb_object_agg( "
+	"                        pcs.attname, "
+	"                        pcs.stats "
+	"                    ) "
+	"                ) AS stats "
+	"            FROM per_column_stats AS pcs "
+	"            GROUP BY pcs.stainherit "
+	"        ), "
+	"        extended_object_stats AS "
+	"        ( "
+	"            SELECT "
+	"                sd.stxdinherit, "
+	"                e.stxname, "
+	"                jsonb_build_object( "
+	"                    'stxkinds', "
+	"                    to_jsonb(e.stxkind), "
+	"                    'stxdndistinct', "
+	"                    ndist.stxdndistinct, "
+	"                    'stxdndependencies', "
+	"                    ndep.stxdndependencies, "
+	"                    'stxdmcv', "
+	"                    mcv.stxdmcv, "
+	"                    'stxdexprs', "
+	"                    x.stdxdexprs "
+	"                ) AS stats "
+	"            FROM pg_statistic_ext AS e "
+	"            JOIN pg_statistic_ext_data AS sd "
+	"                ON sd.stxoid = e.oid "
+	"            LEFT JOIN LATERAL "
+	"                ( "
+	"                    SELECT "
+	"                        jsonb_agg( "
+	"                            jsonb_build_object( "
+	"                                'attnums', string_to_array(nd.attnums, ', '), "
+	"                                'ndistinct', nd.ndistinct "
+	"                                ) "
+	"                            ORDER BY nd.ord "
+	"                        ) "
+	"                    FROM json_each_text(sd.stxdndistinct::text::json) "
+	"                         WITH ORDINALITY AS nd(attnums, ndistinct, ord) "
+	"                ) AS ndist(stxdndistinct) ON sd.stxdndistinct IS NOT NULL "
+	"            LEFT JOIN LATERAL "
+	"                ( "
+	"                    SELECT "
+	"                        jsonb_agg( "
+	"                            jsonb_build_object( "
+	"                                'attnums', "
+	"                                string_to_array( replace(dep.attrs, ' => ', ', '), ', '), "
+	"                                'degree', "
+	"                                dep.degree "
+	"                                ) "
+	"                            ORDER BY dep.ord "
+	"                        ) "
+	"                    FROM json_each_text(sd.stxddependencies::text::json) "
+	"                         WITH ORDINALITY AS dep(attrs, degree, ord) "
+	"                ) AS ndep(stxdndependencies) ON sd.stxddependencies IS NOT NULL "
+	"            LEFT JOIN LATERAL "
+	"                ( "
+	"                    SELECT NULL AS stxdmcv "
+	"                ) AS mcv(stxdmcv) ON sd.stxdmcv IS NOT NULL "
+	"            LEFT JOIN LATERAL "
+	"                ( "
+	"                    SELECT "
+	"                        jsonb_agg( "
+	"                            jsonb_build_object( "
+	"                                'stanullfrac', s.stanullfrac::text, "
+	"                                'stawidth', s.stawidth::text, "
+	"                                'stadistinct', s.stadistinct::text, "
+	"                                'stakinds', "
+	"                                ( "
+	"                                    SELECT "
+	"                                        jsonb_agg( "
+	"                                            CASE kind.kind "
+	"                                                WHEN 0 THEN 'TRIVIAL' "
+	"                                                WHEN 1 THEN 'MCV' "
+	"                                                WHEN 2 THEN 'HISTOGRAM' "
+	"                                                WHEN 3 THEN 'CORRELATION' "
+	"                                                WHEN 4 THEN 'MCELEM' "
+	"                                                WHEN 5 THEN 'DECHIST' "
+	"                                                WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM' "
+	"                                                WHEN 7 THEN 'BOUNDS_HISTOGRAM' "
+	"                                            END::text "
+	"                                            ORDER BY kind.ord) "
+	"                                    FROM unnest(ARRAY[s.stakind1, s.stakind2, "
+	"                                                s.stakind3, stakind4, "
+	"                                                s.stakind5]) WITH ORDINALITY AS kind(kind, ord) "
+	"                                ), "
+	"                                'stanumbers', "
+	"                                jsonb_build_array( "
+	"                                    s.stanumbers1::text::text[], "
+	"                                    s.stanumbers2::text::text[], "
+	"                                    s.stanumbers3::text::text[], "
+	"                                    s.stanumbers4::text::text[], "
+	"                                    s.stanumbers5::text::text[]), "
+	"                                'stavalues', "
+	"                                jsonb_build_array( "
+	"                                    s.stavalues1::text::text[], "
+	"                                    s.stavalues2::text::text[], "
+	"                                    s.stavalues3::text::text[], "
+	"                                    s.stavalues4::text::text[], "
+	"                                    s.stavalues5::text::text[]) "
+	"                            ) "
+	"                            ORDER BY s.ordinality "
+	"                        ) "
+	"                    FROM unnest(sd.stxdexpr) WITH ORDINALITY AS s "
+	"                ) AS x(stdxdexprs) ON sd.stxdexpr IS NOT NULL "
+	"            WHERE e.stxrelid = r.oid "
+	"        ), "
+	"        extagg AS "
+	"        ( "
+	"            SELECT "
+	"                eos.stxdinherit, "
+	"                jsonb_build_object( "
+	"                    'extended', "
+	"                    jsonb_object_agg( "
+	"                        eos.stxname, "
+	"                        eos.stats "
+	"                    ) "
+	"                ) AS stats "
+	"            FROM extended_object_stats AS eos "
+	"            GROUP BY eos.stxdinherit "
+	"        ) "
+	"        SELECT "
+	"            jsonb_object_agg( "
+	"                CASE coalesce(a.stainherit, e.stxdinherit) "
+	"                    WHEN TRUE THEN 'inherited' "
+	"                    ELSE 'regular' "
+	"                END, "
+	"                coalesce(a.stats, '{}'::jsonb) || coalesce(e.stats, '{}'::jsonb)  "
+	"            ) "
+	"        FROM attagg AS a "
+	"        FULL OUTER JOIN extagg e ON a.stainherit = e.stxdinherit "
+	"    ) AS stats "
+	"FROM pg_class AS r "
+	"JOIN pg_namespace AS n "
+	"    ON n.oid = r.relnamespace "
+	"WHERE relkind IN ('r', 'm', 'f', 'p') "
+	"AND n.nspname NOT IN ('pg_catalog', 'information_schema')";
+
+/* v14 is like v15, but lacks stxdinherit on ext_data */
+const char *export_query_v14 = 
+	"SELECT "
+	"    n.nspname AS schemaname, "
+	"    r.relname AS relname, "
+	"    current_setting('server_version_num')::integer AS server_version_num, "
+	"    r.reltuples::float4 AS n_tuples, "
+	"    r.relpages::integer AS n_pages, "
+	"    ( "
+	"        WITH per_column_stats AS "
+	"        ( "
+	"            SELECT "
+	"                s.stainherit, "
+	"                a.attname, "
+	"                jsonb_build_object( "
+	"                    'stanullfrac', s.stanullfrac::text, "
+	"                    'stawidth', s.stawidth::text, "
+	"                    'stadistinct', s.stadistinct::text, "
+	"                    'stakinds', "
+	"                    ( "
+	"                        SELECT "
+	"                            jsonb_agg( "
+	"                                CASE kind.kind "
+	"                                    WHEN 0 THEN 'TRIVIAL' "
+	"                                    WHEN 1 THEN 'MCV' "
+	"                                    WHEN 2 THEN 'HISTOGRAM' "
+	"                                    WHEN 3 THEN 'CORRELATION' "
+	"                                    WHEN 4 THEN 'MCELEM' "
+	"                                    WHEN 5 THEN 'DECHIST' "
+	"                                    WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM' "
+	"                                    WHEN 7 THEN 'BOUNDS_HISTOGRAM' "
+	"                                END::text "
+	"                                ORDER BY kind.ord) "
+	"                        FROM unnest(ARRAY[s.stakind1, s.stakind2, "
+	"                                    s.stakind3, stakind4, "
+	"                                    s.stakind5]) "
+	"                             WITH ORDINALITY AS kind(kind, ord) "
+	"                    ), "
+	"                    'stanumbers', "
+	"                    jsonb_build_array( "
+	"                        s.stanumbers1::text::text[], "
+	"                        s.stanumbers2::text::text[], "
+	"                        s.stanumbers3::text::text[], "
+	"                        s.stanumbers4::text::text[], "
+	"                        s.stanumbers5::text::text[]), "
+	"                    'stavalues', "
+	"                    jsonb_build_array( "
+	"                        s.stavalues1::text::text[], "
+	"                        s.stavalues2::text::text[], "
+	"                        s.stavalues3::text::text[], "
+	"                        s.stavalues4::text::text[], "
+	"                        s.stavalues5::text::text[]) "
+	"                ) AS stats "
+	"            FROM pg_attribute AS a "
+	"            JOIN pg_statistic AS s "
+	"                ON s.starelid = a.attrelid "
+	"                AND s.staattnum = a.attnum "
+	"            WHERE a.attrelid = r.oid "
+	"            AND NOT a.attisdropped "
+	"            AND a.attnum > 0 "
+	"            AND has_column_privilege(a.attrelid, a.attnum, 'SELECT') "
+	"        ), "
+	"        attagg AS "
+	"        ( "
+	"            SELECT "
+	"                pcs.stainherit, "
+	"                jsonb_build_object( "
+	"                    'columns', "
+	"                    jsonb_object_agg( "
+	"                        pcs.attname, "
+	"                        pcs.stats "
+	"                    ) "
+	"                ) AS stats "
+	"            FROM per_column_stats AS pcs "
+	"            GROUP BY pcs.stainherit "
+	"        ), "
+	"        extended_object_stats AS "
+	"        ( "
+	"            SELECT "
+	"                false AS stxdinherit, "
+	"                e.stxname, "
+	"                jsonb_build_object( "
+	"                    'stxkinds', "
+	"                    to_jsonb(e.stxkind), "
+	"                    'stxdndistinct', "
+	"                    ndist.stxdndistinct, "
+	"                    'stxdndependencies', "
+	"                    ndep.stxdndependencies, "
+	"                    'stxdmcv', "
+	"                    mcv.stxdmcv, "
+	"                    'stxdexprs', "
+	"                    x.stdxdexprs "
+	"                ) AS stats "
+	"            FROM pg_statistic_ext AS e "
+	"            JOIN pg_statistic_ext_data AS sd "
+	"                ON sd.stxoid = e.oid "
+	"            LEFT JOIN LATERAL "
+	"                ( "
+	"                    SELECT "
+	"                        jsonb_agg( "
+	"                            jsonb_build_object( "
+	"                                'attnums', string_to_array(nd.attnums, ', '), "
+	"                                'ndistinct', nd.ndistinct "
+	"                                ) "
+	"                            ORDER BY nd.ord "
+	"                        ) "
+	"                    FROM json_each_text(sd.stxdndistinct::text::json) "
+	"                         WITH ORDINALITY AS nd(attnums, ndistinct, ord) "
+	"                ) AS ndist(stxdndistinct) ON sd.stxdndistinct IS NOT NULL "
+	"            LEFT JOIN LATERAL "
+	"                ( "
+	"                    SELECT "
+	"                        jsonb_agg( "
+	"                            jsonb_build_object( "
+	"                                'attnums', "
+	"                                string_to_array( replace(dep.attrs, ' => ', ', '), ', '), "
+	"                                'degree', "
+	"                                dep.degree "
+	"                                ) "
+	"                            ORDER BY dep.ord "
+	"                        ) "
+	"                    FROM json_each_text(sd.stxddependencies::text::json) "
+	"                         WITH ORDINALITY AS dep(attrs, degree, ord) "
+	"                ) AS ndep(stxdndependencies) ON sd.stxddependencies IS NOT NULL "
+	"            LEFT JOIN LATERAL "
+	"                ( "
+	"                    SELECT NULL AS stxdmcv "
+	"                ) AS mcv(stxdmcv) ON sd.stxdmcv IS NOT NULL "
+	"            LEFT JOIN LATERAL "
+	"                ( "
+	"                    SELECT "
+	"                        jsonb_agg( "
+	"                            jsonb_build_object( "
+	"                                'stanullfrac', s.stanullfrac::text, "
+	"                                'stawidth', s.stawidth::text, "
+	"                                'stadistinct', s.stadistinct::text, "
+	"                                'stakinds', "
+	"                                ( "
+	"                                    SELECT "
+	"                                        jsonb_agg( "
+	"                                            CASE kind.kind "
+	"                                                WHEN 0 THEN 'TRIVIAL' "
+	"                                                WHEN 1 THEN 'MCV' "
+	"                                                WHEN 2 THEN 'HISTOGRAM' "
+	"                                                WHEN 3 THEN 'CORRELATION' "
+	"                                                WHEN 4 THEN 'MCELEM' "
+	"                                                WHEN 5 THEN 'DECHIST' "
+	"                                                WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM' "
+	"                                                WHEN 7 THEN 'BOUNDS_HISTOGRAM' "
+	"                                            END::text "
+	"                                            ORDER BY kind.ord) "
+	"                                    FROM unnest(ARRAY[s.stakind1, s.stakind2, "
+	"                                                s.stakind3, stakind4, "
+	"                                                s.stakind5]) WITH ORDINALITY AS kind(kind, ord) "
+	"                                ), "
+	"                                'stanumbers', "
+	"                                jsonb_build_array( "
+	"                                    s.stanumbers1::text::text[], "
+	"                                    s.stanumbers2::text::text[], "
+	"                                    s.stanumbers3::text::text[], "
+	"                                    s.stanumbers4::text::text[], "
+	"                                    s.stanumbers5::text::text[]), "
+	"                                'stavalues', "
+	"                                jsonb_build_array( "
+	"                                    s.stavalues1::text::text[], "
+	"                                    s.stavalues2::text::text[], "
+	"                                    s.stavalues3::text::text[], "
+	"                                    s.stavalues4::text::text[], "
+	"                                    s.stavalues5::text::text[]) "
+	"                            ) "
+	"                            ORDER BY s.ordinality "
+	"                        ) "
+	"                    FROM unnest(sd.stxdexpr) WITH ORDINALITY AS s "
+	"                ) AS x(stdxdexprs) ON sd.stxdexpr IS NOT NULL "
+	"            WHERE e.stxrelid = r.oid "
+	"        ), "
+	"        extagg AS "
+	"        ( "
+	"            SELECT "
+	"                eos.stxdinherit, "
+	"                jsonb_build_object( "
+	"                    'extended', "
+	"                    jsonb_object_agg( "
+	"                        eos.stxname, "
+	"                        eos.stats "
+	"                    ) "
+	"                ) AS stats "
+	"            FROM extended_object_stats AS eos "
+	"            GROUP BY eos.stxdinherit "
+	"        ) "
+	"        SELECT "
+	"            jsonb_object_agg( "
+	"                CASE coalesce(a.stainherit, e.stxdinherit) "
+	"                    WHEN TRUE THEN 'inherited' "
+	"                    ELSE 'regular' "
+	"                END, "
+	"                coalesce(a.stats, '{}'::jsonb) || coalesce(e.stats, '{}'::jsonb)  "
+	"            ) "
+	"        FROM attagg AS a "
+	"        FULL OUTER JOIN extagg e ON a.stainherit = e.stxdinherit "
+	"    ) AS stats "
+	"FROM pg_class AS r "
+	"JOIN pg_namespace AS n "
+	"    ON n.oid = r.relnamespace "
+	"WHERE relkind IN ('r', 'm', 'f', 'p') "
+	"AND n.nspname NOT IN ('pg_catalog', 'information_schema')";
+
+/* v12-v13 are like v14, but lacks stxdexpr on ext_data */
+const char *export_query_v12 = 
+	"SELECT "
+	"    n.nspname AS schemaname, "
+	"    r.relname AS relname, "
+	"    current_setting('server_version_num')::integer AS server_version_num, "
+	"    r.reltuples::float4 AS n_tuples, "
+	"    r.relpages::integer AS n_pages, "
+	"    ( "
+	"        WITH per_column_stats AS "
+	"        ( "
+	"            SELECT "
+	"                s.stainherit, "
+	"                a.attname, "
+	"                jsonb_build_object( "
+	"                    'stanullfrac', s.stanullfrac::text, "
+	"                    'stawidth', s.stawidth::text, "
+	"                    'stadistinct', s.stadistinct::text, "
+	"                    'stakinds', "
+	"                    ( "
+	"                        SELECT "
+	"                            jsonb_agg( "
+	"                                CASE kind.kind "
+	"                                    WHEN 0 THEN 'TRIVIAL' "
+	"                                    WHEN 1 THEN 'MCV' "
+	"                                    WHEN 2 THEN 'HISTOGRAM' "
+	"                                    WHEN 3 THEN 'CORRELATION' "
+	"                                    WHEN 4 THEN 'MCELEM' "
+	"                                    WHEN 5 THEN 'DECHIST' "
+	"                                    WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM' "
+	"                                    WHEN 7 THEN 'BOUNDS_HISTOGRAM' "
+	"                                END::text "
+	"                                ORDER BY kind.ord) "
+	"                        FROM unnest(ARRAY[s.stakind1, s.stakind2, "
+	"                                    s.stakind3, stakind4, "
+	"                                    s.stakind5]) "
+	"                             WITH ORDINALITY AS kind(kind, ord) "
+	"                    ), "
+	"                    'stanumbers', "
+	"                    jsonb_build_array( "
+	"                        s.stanumbers1::text::text[], "
+	"                        s.stanumbers2::text::text[], "
+	"                        s.stanumbers3::text::text[], "
+	"                        s.stanumbers4::text::text[], "
+	"                        s.stanumbers5::text::text[]), "
+	"                    'stavalues', "
+	"                    jsonb_build_array( "
+	"                        s.stavalues1::text::text[], "
+	"                        s.stavalues2::text::text[], "
+	"                        s.stavalues3::text::text[], "
+	"                        s.stavalues4::text::text[], "
+	"                        s.stavalues5::text::text[]) "
+	"                ) AS stats "
+	"            FROM pg_attribute AS a "
+	"            JOIN pg_statistic AS s "
+	"                ON s.starelid = a.attrelid "
+	"                AND s.staattnum = a.attnum "
+	"            WHERE a.attrelid = r.oid "
+	"            AND NOT a.attisdropped "
+	"            AND a.attnum > 0 "
+	"            AND has_column_privilege(a.attrelid, a.attnum, 'SELECT') "
+	"        ), "
+	"        attagg AS "
+	"        ( "
+	"            SELECT "
+	"                pcs.stainherit, "
+	"                jsonb_build_object( "
+	"                    'columns', "
+	"                    jsonb_object_agg( "
+	"                        pcs.attname, "
+	"                        pcs.stats "
+	"                    ) "
+	"                ) AS stats "
+	"            FROM per_column_stats AS pcs "
+	"            GROUP BY pcs.stainherit "
+	"        ), "
+	"        extended_object_stats AS "
+	"        ( "
+	"            SELECT "
+	"                false AS stxdinherit, "
+	"                e.stxname, "
+	"                jsonb_build_object( "
+	"                    'stxkinds', "
+	"                    to_jsonb(e.stxkind), "
+	"                    'stxdndistinct', "
+	"                    ndist.stxdndistinct, "
+	"                    'stxdndependencies', "
+	"                    ndep.stxdndependencies, "
+	"                    'stxdmcv', "
+	"                    mcv.stxdmcv, "
+	"                ) AS stats "
+	"            FROM pg_statistic_ext AS e "
+	"            JOIN pg_statistic_ext_data AS sd "
+	"                ON sd.stxoid = e.oid "
+	"            LEFT JOIN LATERAL "
+	"                ( "
+	"                    SELECT "
+	"                        jsonb_agg( "
+	"                            jsonb_build_object( "
+	"                                'attnums', string_to_array(nd.attnums, ', '), "
+	"                                'ndistinct', nd.ndistinct "
+	"                                ) "
+	"                            ORDER BY nd.ord "
+	"                        ) "
+	"                    FROM json_each_text(sd.stxdndistinct::text::json) "
+	"                         WITH ORDINALITY AS nd(attnums, ndistinct, ord) "
+	"                ) AS ndist(stxdndistinct) ON sd.stxdndistinct IS NOT NULL "
+	"            LEFT JOIN LATERAL "
+	"                ( "
+	"                    SELECT "
+	"                        jsonb_agg( "
+	"                            jsonb_build_object( "
+	"                                'attnums', "
+	"                                string_to_array( replace(dep.attrs, ' => ', ', '), ', '), "
+	"                                'degree', "
+	"                                dep.degree "
+	"                                ) "
+	"                            ORDER BY dep.ord "
+	"                        ) "
+	"                    FROM json_each_text(sd.stxddependencies::text::json) "
+	"                         WITH ORDINALITY AS dep(attrs, degree, ord) "
+	"                ) AS ndep(stxdndependencies) ON sd.stxddependencies IS NOT NULL "
+	"            LEFT JOIN LATERAL "
+	"                ( "
+	"                    SELECT NULL AS stxdmcv "
+	"                ) AS mcv(stxdmcv) ON sd.stxdmcv IS NOT NULL "
+	"            WHERE e.stxrelid = r.oid "
+	"        ), "
+	"        extagg AS "
+	"        ( "
+	"            SELECT "
+	"                eos.stxdinherit, "
+	"                jsonb_build_object( "
+	"                    'extended', "
+	"                    jsonb_object_agg( "
+	"                        eos.stxname, "
+	"                        eos.stats "
+	"                    ) "
+	"                ) AS stats "
+	"            FROM extended_object_stats AS eos "
+	"            GROUP BY eos.stxdinherit "
+	"        ) "
+	"        SELECT "
+	"            jsonb_object_agg( "
+	"                CASE coalesce(a.stainherit, e.stxdinherit) "
+	"                    WHEN TRUE THEN 'inherited' "
+	"                    ELSE 'regular' "
+	"                END, "
+	"                coalesce(a.stats, '{}'::jsonb) || coalesce(e.stats, '{}'::jsonb)  "
+	"            ) "
+	"        FROM attagg AS a "
+	"        FULL OUTER JOIN extagg e ON a.stainherit = e.stxdinherit "
+	"    ) AS stats "
+	"FROM pg_class AS r "
+	"JOIN pg_namespace AS n "
+	"    ON n.oid = r.relnamespace "
+	"WHERE relkind IN ('r', 'm', 'f', 'p') "
+	"AND n.nspname NOT IN ('pg_catalog', 'information_schema')";
+
+/* v10-v11 are like v12, but ext_data is gone and ndistinct and dependencies are on ext */
+const char *export_query_v10 = 
+	"SELECT "
+	"    n.nspname AS schemaname, "
+	"    r.relname AS relname, "
+	"    current_setting('server_version_num')::integer AS server_version_num, "
+	"    r.reltuples::float4 AS n_tuples, "
+	"    r.relpages::integer AS n_pages, "
+	"    ( "
+	"        WITH per_column_stats AS "
+	"        ( "
+	"            SELECT "
+	"                s.stainherit, "
+	"                a.attname, "
+	"                jsonb_build_object( "
+	"                    'stanullfrac', s.stanullfrac::text, "
+	"                    'stawidth', s.stawidth::text, "
+	"                    'stadistinct', s.stadistinct::text, "
+	"                    'stakinds', "
+	"                    ( "
+	"                        SELECT "
+	"                            jsonb_agg( "
+	"                                CASE kind.kind "
+	"                                    WHEN 0 THEN 'TRIVIAL' "
+	"                                    WHEN 1 THEN 'MCV' "
+	"                                    WHEN 2 THEN 'HISTOGRAM' "
+	"                                    WHEN 3 THEN 'CORRELATION' "
+	"                                    WHEN 4 THEN 'MCELEM' "
+	"                                    WHEN 5 THEN 'DECHIST' "
+	"                                    WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM' "
+	"                                    WHEN 7 THEN 'BOUNDS_HISTOGRAM' "
+	"                                END::text "
+	"                                ORDER BY kind.ord) "
+	"                        FROM unnest(ARRAY[s.stakind1, s.stakind2, "
+	"                                    s.stakind3, stakind4, "
+	"                                    s.stakind5]) "
+	"                             WITH ORDINALITY AS kind(kind, ord) "
+	"                    ), "
+	"                    'stanumbers', "
+	"                    jsonb_build_array( "
+	"                        s.stanumbers1::text::text[], "
+	"                        s.stanumbers2::text::text[], "
+	"                        s.stanumbers3::text::text[], "
+	"                        s.stanumbers4::text::text[], "
+	"                        s.stanumbers5::text::text[]), "
+	"                    'stavalues', "
+	"                    jsonb_build_array( "
+	"                        s.stavalues1::text::text[], "
+	"                        s.stavalues2::text::text[], "
+	"                        s.stavalues3::text::text[], "
+	"                        s.stavalues4::text::text[], "
+	"                        s.stavalues5::text::text[]) "
+	"                ) AS stats "
+	"            FROM pg_attribute AS a "
+	"            JOIN pg_statistic AS s "
+	"                ON s.starelid = a.attrelid "
+	"                AND s.staattnum = a.attnum "
+	"            WHERE a.attrelid = r.oid "
+	"            AND NOT a.attisdropped "
+	"            AND a.attnum > 0 "
+	"            AND has_column_privilege(a.attrelid, a.attnum, 'SELECT') "
+	"        ), "
+	"        attagg AS "
+	"        ( "
+	"            SELECT "
+	"                pcs.stainherit, "
+	"                jsonb_build_object( "
+	"                    'columns', "
+	"                    jsonb_object_agg( "
+	"                        pcs.attname, "
+	"                        pcs.stats "
+	"                    ) "
+	"                ) AS stats "
+	"            FROM per_column_stats AS pcs "
+	"            GROUP BY pcs.stainherit "
+	"        ), "
+	"        extended_object_stats AS "
+	"        ( "
+	"            SELECT "
+	"                false AS stxdinherit, "
+	"                e.stxname, "
+	"                jsonb_build_object( "
+	"                    'stxkinds', "
+	"                    to_jsonb(e.stxkind), "
+	"                    'stxdndistinct', "
+	"                    ndist.stxdndistinct, "
+	"                    'stxdndependencies', "
+	"                    ndep.stxdndependencies, "
+	"                ) AS stats "
+	"            FROM pg_statistic_ext AS e "
+	"            LEFT JOIN LATERAL "
+	"                ( "
+	"                    SELECT "
+	"                        jsonb_agg( "
+	"                            jsonb_build_object( "
+	"                                'attnums', string_to_array(nd.attnums, ', '), "
+	"                                'ndistinct', nd.ndistinct "
+	"                                ) "
+	"                            ORDER BY nd.ord "
+	"                        ) "
+	"                    FROM json_each_text(e.stxndistinct::text::json) "
+	"                         WITH ORDINALITY AS nd(attnums, ndistinct, ord) "
+	"                ) AS ndist(stxdndistinct) ON e.stxndistinct IS NOT NULL "
+	"            LEFT JOIN LATERAL "
+	"                ( "
+	"                    SELECT "
+	"                        jsonb_agg( "
+	"                            jsonb_build_object( "
+	"                                'attnums', "
+	"                                string_to_array( replace(dep.attrs, ' => ', ', '), ', '), "
+	"                                'degree', "
+	"                                dep.degree "
+	"                                ) "
+	"                            ORDER BY dep.ord "
+	"                        ) "
+	"                    FROM json_each_text(.stxdependencies::text::json) "
+	"                         WITH ORDINALITY AS dep(attrs, degree, ord) "
+	"                ) AS ndep(stxdndependencies) ON e.stxdependencies IS NOT NULL "
+	"            WHERE e.stxrelid = r.oid "
+	"        ), "
+	"        extagg AS "
+	"        ( "
+	"            SELECT "
+	"                eos.stxdinherit, "
+	"                jsonb_build_object( "
+	"                    'extended', "
+	"                    jsonb_object_agg( "
+	"                        eos.stxname, "
+	"                        eos.stats "
+	"                    ) "
+	"                ) AS stats "
+	"            FROM extended_object_stats AS eos "
+	"            GROUP BY eos.stxdinherit "
+	"        ) "
+	"        SELECT "
+	"            jsonb_object_agg( "
+	"                CASE coalesce(a.stainherit, e.stxdinherit) "
+	"                    WHEN TRUE THEN 'inherited' "
+	"                    ELSE 'regular' "
+	"                END, "
+	"                coalesce(a.stats, '{}'::jsonb) || coalesce(e.stats, '{}'::jsonb)  "
+	"            ) "
+	"        FROM attagg AS a "
+	"        FULL OUTER JOIN extagg e ON a.stainherit = e.stxdinherit "
+	"    ) AS stats "
+	"FROM pg_class AS r "
+	"JOIN pg_namespace AS n "
+	"    ON n.oid = r.relnamespace "
+	"WHERE relkind IN ('r', 'm', 'f', 'p') "
+	"AND n.nspname NOT IN ('pg_catalog', '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;
+
+	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);
+
+	initPQExpBuffer(&sql);
+
+	appendPQExpBufferStr(&sql, "COPY (");
+
+	if (PQserverVersion(conn) >= 170000)
+		appendPQExpBufferStr(&sql, export_query_v17);
+	else if (PQserverVersion(conn) >= 150000)
+		appendPQExpBufferStr(&sql, export_query_v15);
+	else if (PQserverVersion(conn) >= 140000)
+		appendPQExpBufferStr(&sql, export_query_v14);
+	else if (PQserverVersion(conn) >= 120000)
+		appendPQExpBufferStr(&sql, export_query_v12);
+	else if (PQserverVersion(conn) >= 100000)
+		appendPQExpBufferStr(&sql, export_query_v10);
+	else
+		pg_fatal("exporting statistics from databases prior to version 10 not supported");
+
+	appendPQExpBufferStr(&sql, ") TO STDOUT");
+
+	result = PQexec(conn, sql.data);
+	result_status = PQresultStatus(result);
+
+	if (result_status != PGRES_COPY_OUT)
+		pg_fatal("malformed copy command");
+
+	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..cfcadff769
--- /dev/null
+++ b/src/bin/scripts/pg_import_stats.c
@@ -0,0 +1,303 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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;
+
+	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 */
+
+
+	result = PQexec(conn, 
+		"CREATE TEMPORARY TABLE import_stats ( "
+		"id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, "
+		"schemaname text, relname text, server_version_num integer, "
+		"n_tuples float4, n_pages integer, stats jsonb )");
+
+	if (PQresultStatus(result) != PGRES_COMMAND_OK)
+		pg_fatal("could not create temporary file: %s", PQerrorMessage(conn));
+
+	PQclear(result);
+
+	result = PQexec(conn, 
+		"COPY import_stats(schemaname, relname, server_version_num, n_tuples, "
+		"n_pages, 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));
+
+	numtables = atol(PQcmdTuples(result));
+
+	PQclear(result);
+
+	result = PQprepare(conn, "import", 
+		"SELECT pg_import_rel_stats(c.oid, s.server_version_num, "
+		"             s.n_tuples, s.n_pages, s.stats) as import_result "
+		"FROM import_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", 
+			"SELECT s.schemaname, s.relname "
+			"FROM import_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", 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", 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);
+	}
+
+	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.41.0

#5Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Corey Huinker (#4)
Re: Statistics Import and Export

On 10/31/23 08:25, Corey Huinker wrote:

Attached is v2 of this patch.

New features:
* imports index statistics. This is not strictly accurate: it
re-computes index statistics the same as ANALYZE does, which is to
say it derives those stats entirely from table column stats, which
are imported, so in that sense we're getting index stats without
touching the heap.

Maybe I just don't understand, but I'm pretty sure ANALYZE does not
derive index stats from column stats. It actually builds them from the
row sample.

* now support extended statistics except for MCV, which is currently
serialized as an difficult-to-decompose bytea field.

Doesn't pg_mcv_list_items() already do all the heavy work?

regards

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

#6Corey Huinker
corey.huinker@gmail.com
In reply to: Tomas Vondra (#5)
Re: Statistics Import and Export

Maybe I just don't understand, but I'm pretty sure ANALYZE does not
derive index stats from column stats. It actually builds them from the
row sample.

That is correct, my error.

* now support extended statistics except for MCV, which is currently
serialized as an difficult-to-decompose bytea field.

Doesn't pg_mcv_list_items() already do all the heavy work?

Thanks! I'll look into that.

The comment below in mcv.c made me think there was no easy way to get
output.

/*
* pg_mcv_list_out - output routine for type pg_mcv_list.
*
* MCV lists are serialized into a bytea value, so we simply call byteaout()
* to serialize the value into text. But it'd be nice to serialize that into
* a meaningful representation (e.g. for inspection by people).
*
* XXX This should probably return something meaningful, similar to what
* pg_dependencies_out does. Not sure how to deal with the deduplicated
* values, though - do we want to expand that or not?
*/

#7Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Corey Huinker (#6)
Re: Statistics Import and Export

On 11/2/23 06:01, Corey Huinker wrote:

Maybe I just don't understand, but I'm pretty sure ANALYZE does not
derive index stats from column stats. It actually builds them from the
row sample.

That is correct, my error.
 

* now support extended statistics except for MCV, which is currently
serialized as an difficult-to-decompose bytea field.

Doesn't pg_mcv_list_items() already do all the heavy work?

Thanks! I'll look into that.

The comment below in mcv.c made me think there was no easy way to get
output.

/*
 * pg_mcv_list_out      - output routine for type pg_mcv_list.
 *
 * MCV lists are serialized into a bytea value, so we simply call byteaout()
 * to serialize the value into text. But it'd be nice to serialize that into
 * a meaningful representation (e.g. for inspection by people).
 *
 * XXX This should probably return something meaningful, similar to what
 * pg_dependencies_out does. Not sure how to deal with the deduplicated
 * values, though - do we want to expand that or not?
 */

Yeah, that was the simplest output function possible, it didn't seem
worth it to implement something more advanced. pg_mcv_list_items() is
more convenient for most needs, but it's quite far from the on-disk
representation.

That's actually a good question - how closely should the exported data
be to the on-disk format? I'd say we should keep it abstract, not tied
to the details of the on-disk format (which might easily change between
versions).

I'm a bit confused about the JSON schema used in pg_statistic_export
view, though. It simply serializes stakinds, stavalues, stanumbers into
arrays ... which works, but why not to use the JSON nesting? I mean,
there could be a nested document for histogram, MCV, ... with just the
correct fields.

{
...
histogram : { stavalues: [...] },
mcv : { stavalues: [...], stanumbers: [...] },
...
}

and so on. Also, what does TRIVIAL stand for?

regards

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

#8Shubham Khanna
khannashubham1197@gmail.com
In reply to: Corey Huinker (#4)
Re: Statistics Import and Export

On Mon, Nov 6, 2023 at 4:16 PM Corey Huinker <corey.huinker@gmail.com> wrote:

Yeah, that use makes sense as well, and if so then postgres_fdw would likely need to be aware of the appropriate query for several versions back - they change, not by much, but they do change. So now we'd have each query text in three places: a system view, postgres_fdw, and the bin/scripts pre-upgrade program. So I probably should consider the best way to share those in the codebase.

Attached is v2 of this patch.

While applying Patch, I noticed few Indentation issues:
1) D:\Project\Postgres>git am v2-0003-Add-pg_import_rel_stats.patch
.git/rebase-apply/patch:1265: space before tab in indent.
errmsg("invalid statistics
format, stxndeprs must be array or null");
.git/rebase-apply/patch:1424: trailing whitespace.
errmsg("invalid statistics format,
stxndistinct attnums elements must be strings, but one is %s",
.git/rebase-apply/patch:1315: new blank line at EOF.
+
warning: 3 lines add whitespace errors.
Applying: Add pg_import_rel_stats().

2) D:\Project\Postgres>git am v2-0004-Add-pg_export_stats-pg_import_stats.patch
.git/rebase-apply/patch:282: trailing whitespace.
const char *export_query_v14 =
.git/rebase-apply/patch:489: trailing whitespace.
const char *export_query_v12 =
.git/rebase-apply/patch:648: trailing whitespace.
const char *export_query_v10 =
.git/rebase-apply/patch:826: trailing whitespace.

.git/rebase-apply/patch:1142: trailing whitespace.
result = PQexec(conn,
warning: squelched 4 whitespace errors
warning: 9 lines add whitespace errors.
Applying: Add pg_export_stats, pg_import_stats.

Thanks and Regards,
Shubham Khanna.

#9Ashutosh Bapat
ashutosh.bapat.oss@gmail.com
In reply to: Corey Huinker (#4)
Re: Statistics Import and Export

On Tue, Oct 31, 2023 at 12:55 PM Corey Huinker <corey.huinker@gmail.com> wrote:

Yeah, that use makes sense as well, and if so then postgres_fdw would likely need to be aware of the appropriate query for several versions back - they change, not by much, but they do change. So now we'd have each query text in three places: a system view, postgres_fdw, and the bin/scripts pre-upgrade program. So I probably should consider the best way to share those in the codebase.

Attached is v2 of this patch.

New features:
* imports index statistics. This is not strictly accurate: it re-computes index statistics the same as ANALYZE does, which is to say it derives those stats entirely from table column stats, which are imported, so in that sense we're getting index stats without touching the heap.
* now support extended statistics except for MCV, which is currently serialized as an difficult-to-decompose bytea field.
* bare-bones CLI script pg_export_stats, which extracts stats on databases back to v12 (tested) and could work back to v10.
* bare-bones CLI script pg_import_stats, which obviously only works on current devel dbs, but can take exports from older versions.

I did a small experiment with your patches. In a separate database
"fdw_dst" I created a table t1 and populated it with 100K rows
#create table t1 (a int, b int);
#insert into t1 select i, i + 1 from generate_series(1, 100000) i;
#analyse t1;

In database "postgres" on the same server, I created a foreign table
pointing to t1
#create server fdw_dst_server foreign data wrapper postgres_fdw
OPTIONS ( dbname 'fdw_dst', port '5432');
#create user mapping for public server fdw_dst_server ;
#create foreign table t1 (a int, b int) server fdw_dst_server;

The estimates are off
#explain select * from t1 where a = 100;
QUERY PLAN
-----------------------------------------------------------
Foreign Scan on t1 (cost=100.00..142.26 rows=13 width=8)
(1 row)

Export and import stats for table t1
$ pg_export_stats -d fdw_dst | pg_import_stats -d postgres

gives accurate estimates
#explain select * from t1 where a = 100;
QUERY PLAN
-----------------------------------------------------------
Foreign Scan on t1 (cost=100.00..1793.02 rows=1 width=8)
(1 row)

In this simple case it's working like a charm.

Then I wanted to replace all ANALYZE commands in postgres_fdw.sql with
import and export of statistics. But I can not do that since it
requires table names to match. Foreign table metadata stores the
mapping between local and remote table as well as column names. Import
can use that mapping to install the statistics appropriately. We may
want to support a command or function in postgres_fdw to import
statistics of all the tables that point to a given foreign server.
That may be some future work based on your current patches.

I have not looked at the code though.

--
Best Wishes,
Ashutosh Bapat

#10Corey Huinker
corey.huinker@gmail.com
In reply to: Tomas Vondra (#7)
9 attachment(s)
Re: Statistics Import and Export

Yeah, that was the simplest output function possible, it didn't seem

worth it to implement something more advanced. pg_mcv_list_items() is

more convenient for most needs, but it's quite far from the on-disk
representation.

I was able to make it work.

That's actually a good question - how closely should the exported data
be to the on-disk format? I'd say we should keep it abstract, not tied
to the details of the on-disk format (which might easily change between
versions).

For the most part, I chose the exported data json types and formats in a
way that was the most accommodating to cstring input functions. So, while
so many of the statistic values are obviously only ever integers/floats,
those get stored as a numeric data type which lacks direct
numeric->int/float4/float8 functions (though we could certainly create
them, and I'm not against that), casting them to text lets us leverage
pg_strtoint16, etc.

I'm a bit confused about the JSON schema used in pg_statistic_export
view, though. It simply serializes stakinds, stavalues, stanumbers into
arrays ... which works, but why not to use the JSON nesting? I mean,
there could be a nested document for histogram, MCV, ... with just the
correct fields.

{
...
histogram : { stavalues: [...] },
mcv : { stavalues: [...], stanumbers: [...] },
...
}

That's a very good question. I went with this format because it was fairly
straightforward to code in SQL using existing JSON/JSONB functions, and
that's what we will need if we want to export statistics on any server
currently in existence. I'm certainly not locked in with the current
format, and if it can be shown how to transform the data into a superior
format, I'd happily do so.

and so on. Also, what does TRIVIAL stand for?

It's currently serving double-duty for "there are no stats in this slot"
and the situations where the stats computation could draw no conclusions
about the data.

Attached is v3 of this patch. Key features are:

* Handles regular pg_statistic stats for any relation type.
* Handles extended statistics.
* Export views pg_statistic_export and pg_statistic_ext_export to allow
inspection of existing stats and saving those values for later use.
* Import functions pg_import_rel_stats() and pg_import_ext_stats() which
take Oids as input. This is intentional to allow stats from one object to
be imported into another object.
* User scripts pg_export_stats and pg_import stats, which offer a primitive
way to serialize all the statistics of one database and import them into
another.
* Has regression test coverage for both with a variety of data types.
* Passes my own manual test of extracting all of the stats from a v15
version of the popular "dvdrental" example database, as well as some
additional extended statistics objects, and importing them into a
development database.
* Import operations never touch the heap of any relation outside of
pg_catalog. As such, this should be significantly faster than even the most
cursory analyze operation, and therefore should be useful in upgrade
situations, allowing the database to work with "good enough" stats more
quickly, while still allowing for regular autovacuum to recalculate the
stats "for real" at some later point.

The relation statistics code was adapted from similar features in
analyze.c, but is now done in a query context. As before, the
rowcount/pagecount values are updated on pg_class in a non-transactional
fashion to avoid table bloat, while the updates to pg_statistic are
pg_statistic_ext_data are done transactionally.

The existing statistics _store() functions were leveraged wherever
practical, so much so that the extended statistics import is mostly just
adapting the existing _build() functions into _import() functions which
pull their values from JSON rather than computing the statistics.

Current concerns are:

1. I had to code a special-case exception for MCELEM stats on array data
types, so that the array_in() call uses the element type rather than the
array type. I had assumed that the existing exmaine_attribute() functions
would have properly derived the typoid for that column, but it appears to
not be the case, and I'm clearly missing how the existing code gets it
right.
2. This hasn't been tested with external custom datatypes, but if they have
a custom typanalyze function things should be ok.
3. While I think I have cataloged all of the schema-structural changes to
pg_statistic[_ext[_data]] since version 10, I may have missed a case where
the schema stayed the same, but the values are interpreted differently.
4. I don't yet have a complete vision for how these tools will be used by
pg_upgrade and pg_dump/restore, the places where these will provide the
biggest win for users.

Attachments:

v3-0002-Add-system-view-pg_statistic_export.patchtext/x-patch; charset=US-ASCII; name=v3-0002-Add-system-view-pg_statistic_export.patchDownload
From 4ae4ac484bcaf4b74fef1284d63e6e6d4d6a236f Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Tue, 12 Dec 2023 20:48:42 -0500
Subject: [PATCH v3 2/9] Add system view pg_statistic_export.

This view is designed to aid in the export (and re-import) of table
statistics, mostly for upgrade/restore situations.
---
 src/backend/catalog/system_views.sql | 84 ++++++++++++++++++++++++++++
 src/test/regress/expected/rules.out  | 31 ++++++++++
 doc/src/sgml/system-views.sgml       |  5 ++
 3 files changed, 120 insertions(+)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 11d18ed9dd..7655bf7458 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -274,6 +274,90 @@ CREATE VIEW pg_stats WITH (security_barrier) AS
 
 REVOKE ALL ON pg_statistic FROM public;
 
+
+
+CREATE VIEW pg_statistic_export WITH (security_barrier) AS
+    SELECT
+        n.nspname AS schemaname,
+        r.relname AS relname,
+        current_setting('server_version_num')::integer AS server_version_num,
+        r.reltuples::float4 AS n_tuples,
+        r.relpages::integer AS n_pages,
+        (
+            SELECT
+                jsonb_object_agg(
+                    CASE
+                        WHEN a.stainherit THEN 'inherited'
+                        ELSE 'regular'
+                    END,
+                    a.stats
+                )
+            FROM
+            (
+                SELECT
+                    s.stainherit,
+                    jsonb_object_agg(
+                        a.attname,
+                        jsonb_build_object(
+                            'stanullfrac', s.stanullfrac::text,
+                            'stawidth', s.stawidth::text,
+                            'stadistinct', s.stadistinct::text,
+                            'stakinds',
+                            (
+                                SELECT
+                                    jsonb_agg(
+                                        CASE kind.kind
+                                            WHEN 0 THEN 'TRIVIAL'
+                                            WHEN 1 THEN 'MCV'
+                                            WHEN 2 THEN 'HISTOGRAM'
+                                            WHEN 3 THEN 'CORRELATION'
+                                            WHEN 4 THEN 'MCELEM'
+                                            WHEN 5 THEN 'DECHIST'
+                                            WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM'
+                                            WHEN 7 THEN 'BOUNDS_HISTOGRAM'
+                                        END::text
+                                        ORDER BY kind.ord)
+                                FROM unnest(ARRAY[s.stakind1, s.stakind2,
+                                            s.stakind3, stakind4,
+                                            s.stakind5])
+                                     WITH ORDINALITY AS kind(kind, ord)
+                            ),
+                            'stanumbers',
+                            jsonb_build_array(
+                                s.stanumbers1::text,
+                                s.stanumbers2::text,
+                                s.stanumbers3::text,
+                                s.stanumbers4::text,
+                                s.stanumbers5::text),
+                            'stavalues',
+                            jsonb_build_array(
+                                -- casting to text makes it easier to import using array_in()
+                                s.stavalues1::text,
+                                s.stavalues2::text,
+                                s.stavalues3::text,
+                                s.stavalues4::text,
+                                s.stavalues5::text)
+                        )
+                    ) AS stats
+                FROM pg_attribute AS a
+                JOIN pg_statistic AS s
+                    ON s.starelid = a.attrelid
+                    AND s.staattnum = a.attnum
+                WHERE a.attrelid = r.oid
+                AND NOT a.attisdropped
+                AND a.attnum > 0
+                AND has_column_privilege(a.attrelid, a.attnum, 'SELECT')
+                GROUP BY s.stainherit
+            ) AS a
+        ) AS stats
+    FROM pg_class AS r
+    JOIN pg_namespace AS n
+        ON n.oid = r.relnamespace
+    WHERE relkind IN ('r', 'm', 'f', 'p', 'i')
+    AND n.nspname NOT IN ('pg_catalog', 'pg_toast', 'information_schema');
+
+
+
 CREATE VIEW pg_stats_ext WITH (security_barrier) AS
     SELECT cn.nspname AS schemaname,
            c.relname AS tablename,
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 05070393b9..f2b059af5e 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2404,6 +2404,37 @@ pg_statio_user_tables| SELECT relid,
     tidx_blks_hit
    FROM pg_statio_all_tables
   WHERE ((schemaname <> ALL (ARRAY['pg_catalog'::name, 'information_schema'::name])) AND (schemaname !~ '^pg_toast'::text));
+pg_statistic_export| SELECT n.nspname AS schemaname,
+    r.relname,
+    (current_setting('server_version_num'::text))::integer AS server_version_num,
+    r.reltuples AS n_tuples,
+    r.relpages AS n_pages,
+    ( SELECT jsonb_object_agg(
+                CASE
+                    WHEN a.stainherit THEN 'inherited'::text
+                    ELSE 'regular'::text
+                END, a.stats) AS jsonb_object_agg
+           FROM ( SELECT s.stainherit,
+                    jsonb_object_agg(a_1.attname, jsonb_build_object('stanullfrac', (s.stanullfrac)::text, 'stawidth', (s.stawidth)::text, 'stadistinct', (s.stadistinct)::text, 'stakinds', ( SELECT jsonb_agg(
+                                CASE kind.kind
+                                    WHEN 0 THEN 'TRIVIAL'::text
+                                    WHEN 1 THEN 'MCV'::text
+                                    WHEN 2 THEN 'HISTOGRAM'::text
+                                    WHEN 3 THEN 'CORRELATION'::text
+                                    WHEN 4 THEN 'MCELEM'::text
+                                    WHEN 5 THEN 'DECHIST'::text
+                                    WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM'::text
+                                    WHEN 7 THEN 'BOUNDS_HISTOGRAM'::text
+                                    ELSE NULL::text
+                                END ORDER BY kind.ord) AS jsonb_agg
+                           FROM unnest(ARRAY[s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5]) WITH ORDINALITY kind(kind, ord)), 'stanumbers', jsonb_build_array((s.stanumbers1)::text, (s.stanumbers2)::text, (s.stanumbers3)::text, (s.stanumbers4)::text, (s.stanumbers5)::text), 'stavalues', jsonb_build_array((s.stavalues1)::text, (s.stavalues2)::text, (s.stavalues3)::text, (s.stavalues4)::text, (s.stavalues5)::text))) AS stats
+                   FROM (pg_attribute a_1
+                     JOIN pg_statistic s ON (((s.starelid = a_1.attrelid) AND (s.staattnum = a_1.attnum))))
+                  WHERE ((a_1.attrelid = r.oid) AND (NOT a_1.attisdropped) AND (a_1.attnum > 0) AND has_column_privilege(a_1.attrelid, a_1.attnum, 'SELECT'::text))
+                  GROUP BY s.stainherit) a) AS stats
+   FROM (pg_class r
+     JOIN pg_namespace n ON ((n.oid = r.relnamespace)))
+  WHERE ((r.relkind = ANY (ARRAY['r'::"char", 'm'::"char", 'f'::"char", 'p'::"char", 'i'::"char"])) AND (n.nspname <> ALL (ARRAY['pg_catalog'::name, 'pg_toast'::name, 'information_schema'::name])));
 pg_stats| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     a.attname,
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 0ef1745631..91b3ab22fb 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -191,6 +191,11 @@
       <entry>extended planner statistics for expressions</entry>
      </row>
 
+     <row>
+      <entry><link linkend="view-pg-stats"><structname>pg_stats_export</structname></link></entry>
+      <entry>planner statistics for export/upgrade purposes</entry>
+     </row>
+
      <row>
       <entry><link linkend="view-pg-tables"><structname>pg_tables</structname></link></entry>
       <entry>tables</entry>
-- 
2.43.0

v3-0001-Additional-internal-jsonb-access-functions.patchtext/x-patch; charset=US-ASCII; name=v3-0001-Additional-internal-jsonb-access-functions.patchDownload
From 7cc09a452ce4407fad83e0fcc9723d43121d4db1 Mon Sep 17 00:00:00 2001
From: coreyhuinker <corey.huinker@gmail.com>
Date: Mon, 30 Oct 2023 16:21:30 -0400
Subject: [PATCH v3 1/9] Additional internal jsonb access functions.

Make JsonbContainerTypeName externally visible.

Add JsonbStringValueToCString.
---
 src/include/utils/jsonb.h          |  4 ++++
 src/backend/utils/adt/jsonb.c      |  2 +-
 src/backend/utils/adt/jsonb_util.c | 15 +++++++++++++++
 3 files changed, 20 insertions(+), 1 deletion(-)

diff --git a/src/include/utils/jsonb.h b/src/include/utils/jsonb.h
index addc9b608e..b3c1e104f2 100644
--- a/src/include/utils/jsonb.h
+++ b/src/include/utils/jsonb.h
@@ -424,6 +424,8 @@ extern char *JsonbToCStringIndent(StringInfo out, JsonbContainer *in,
 								  int estimated_len);
 extern bool JsonbExtractScalar(JsonbContainer *jbc, JsonbValue *res);
 extern const char *JsonbTypeName(JsonbValue *val);
+extern const char *JsonbContainerTypeName(JsonbContainer *jbc);
+
 
 extern Datum jsonb_set_element(Jsonb *jb, Datum *path, int path_len,
 							   JsonbValue *newval);
@@ -436,4 +438,6 @@ extern Datum jsonb_build_object_worker(int nargs, const Datum *args, const bool
 extern Datum jsonb_build_array_worker(int nargs, const Datum *args, const bool *nulls,
 									  const Oid *types, bool absent_on_null);
 
+extern char *JsonbStringValueToCString(JsonbValue *j);
+
 #endif							/* __JSONB_H__ */
diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c
index 6f445f5c2b..0ad4e81d89 100644
--- a/src/backend/utils/adt/jsonb.c
+++ b/src/backend/utils/adt/jsonb.c
@@ -160,7 +160,7 @@ jsonb_from_text(text *js, bool unique_keys)
 /*
  * Get the type name of a jsonb container.
  */
-static const char *
+const char *
 JsonbContainerTypeName(JsonbContainer *jbc)
 {
 	JsonbValue	scalar;
diff --git a/src/backend/utils/adt/jsonb_util.c b/src/backend/utils/adt/jsonb_util.c
index 9cc95b773d..ae311b38ba 100644
--- a/src/backend/utils/adt/jsonb_util.c
+++ b/src/backend/utils/adt/jsonb_util.c
@@ -1992,3 +1992,18 @@ uniqueifyJsonbObject(JsonbValue *object, bool unique_keys, bool skip_nulls)
 		}
 	}
 }
+
+/*
+ * Extract a JsonbValue as a cstring.
+ */
+char *JsonbStringValueToCString(JsonbValue *j)
+{
+	char *s;
+
+	Assert(j->type == jbvString);
+	/* make a string that we are sure is null-terminated */
+	s = palloc(j->val.string.len + 1);
+	memcpy(s, j->val.string.val, j->val.string.len);
+	s[j->val.string.len] = '\0';
+	return s;
+}
-- 
2.43.0

v3-0003-Add-pg_import_rel_stats.patchtext/x-patch; charset=US-ASCII; name=v3-0003-Add-pg_import_rel_stats.patchDownload
From 2260a0f53572d8422857237e4065da619401a0f1 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Tue, 12 Dec 2023 22:21:42 -0500
Subject: [PATCH v3 3/9] Add pg_import_rel_stats()

The function pg_import_rel_stats imports rowcount, pagecount, and column
statistics for a given table or index.

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 best-effort approach, skipping statistics that are
expected but omitted, skipping object that are specified but do not
exist on the target system. The goal is to get better-than-empty
statistics into the table quickly, so that business operations can
resume sooner.

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.

The medium of exchange is jsonb, the format of which is specified in the
view pg_statistic_export. Obviously this view does not exist in older
versions of the database, but the view definition can be extracted and
adapted to older versions.

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               |   5 +
 src/include/statistics/statistics.h           |  16 +
 src/backend/statistics/Makefile               |   3 +-
 src/backend/statistics/meson.build            |   1 +
 src/backend/statistics/statistics.c           | 806 ++++++++++++++++++
 .../regress/expected/stats_export_import.out  | 137 +++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/stats_export_import.sql  | 119 +++
 doc/src/sgml/func.sgml                        |  43 +
 9 files changed, 1130 insertions(+), 2 deletions(-)
 create mode 100644 src/backend/statistics/statistics.c
 create mode 100644 src/test/regress/expected/stats_export_import.out
 create mode 100644 src/test/regress/sql/stats_export_import.sql

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 77e8b13764..4d1e9bde1f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5655,6 +5655,11 @@
   proname => 'pg_stat_get_db_stat_reset_time', provolatile => 's',
   proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
   prosrc => 'pg_stat_get_db_stat_reset_time' },
+{ oid => '3814',
+  descr => 'statistics: import to relation',
+  proname => 'pg_import_rel_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'bool', proargtypes => 'oid int4 float4 int4 jsonb',
+  prosrc => 'pg_import_rel_stats' },
 { oid => '3150', descr => 'statistics: number of temporary files written',
   proname => 'pg_stat_get_db_temp_files', provolatile => 's',
   proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 5e538fec32..4251558593 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -15,6 +15,7 @@
 
 #include "commands/vacuum.h"
 #include "nodes/pathnodes.h"
+#include "utils/jsonb.h"
 
 #define STATS_MAX_DIMENSIONS	8	/* max number of attributes */
 
@@ -101,6 +102,7 @@ extern MCVList *statext_mcv_load(Oid mvoid, bool inh);
 extern void BuildRelationExtStatistics(Relation onerel, bool inh, double totalrows,
 									   int numrows, HeapTuple *rows,
 									   int natts, VacAttrStats **vacattrstats);
+
 extern int	ComputeExtStatisticsRows(Relation onerel,
 									 int natts, VacAttrStats **vacattrstats);
 extern bool statext_is_kind_built(HeapTuple htup, char type);
@@ -127,4 +129,18 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												int nclauses);
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
+extern char *key_lookup_cstring(JsonbContainer *cont, const char *key);
+extern JsonbContainer *key_lookup_object(JsonbContainer *cont, const char *key);
+extern JsonbContainer *key_lookup_array(JsonbContainer *cont, const char *key);
+
+extern Datum pg_import_rel_stats(PG_FUNCTION_ARGS);
+
+extern VacAttrStats *examine_rel_attribute(Form_pg_attribute attr,
+										   Relation onerel, Node *index_expr);
+
+extern
+void import_attribute(Oid relid, const VacAttrStats *stat,
+					  JsonbContainer *cont, bool inh, Datum values[],
+					  bool nulls[], bool replaces[]);
+
 #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 e12737b011..1e6e100d3c 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..968ccfaaf2
--- /dev/null
+++ b/src/backend/statistics/statistics.c
@@ -0,0 +1,806 @@
+/*-------------------------------------------------------------------------
+ *
+ * statistics.c
+ *
+ * IDENTIFICATION
+ *	  src/backend/statistics/statistics.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_collation.h"
+#include "catalog/pg_type.h"
+#include "catalog/pg_operator.h"
+#include "fmgr.h"
+#include "nodes/nodeFuncs.h"
+#include "utils/builtins.h"
+#include "utils/datum.h" /* REMOVE */
+#include "utils/float.h"
+#include "utils/fmgroids.h"
+#include "utils/jsonb.h"
+#include "utils/numeric.h"
+#include "utils/lsyscache.h"
+#include "utils/syscache.h"
+#include "utils/typcache.h"
+#include "statistics/statistics.h"
+
+
+static int16
+decode_stakind_string(char *s);
+
+static
+void import_pg_statistic(Relation rel, bool inh, JsonbContainer *cont);
+
+static
+void import_stakinds(const VacAttrStats *stat, JsonbContainer *cont,
+					 bool inh, int16 kindenums[], Datum kindvalues[],
+					 bool kindnulls[], bool kindreplaces[], Datum opvalues[],
+					 bool opnulls[], bool opreplaces[], Datum collvalues[],
+					 bool collnulls[], bool collreplaces[]);
+
+static
+void import_stanumbers(const VacAttrStats *stat, JsonbContainer *cont,
+					   Datum kindvalues[], bool kindnulls[],
+					   bool kindreplaces[]);
+
+static
+void import_stavalues(const VacAttrStats *stat, JsonbContainer *cont,
+					  int16 kindenums[], Datum valvalues[],
+					  bool valnulls[], bool valreplaces[]);
+
+
+/*
+ * Import staistic from:
+ *   root->"regular"
+ *   and
+ *   root->"inherited"
+ *
+ * Container format is:
+ *
+ * {
+ * 	 "colname1": { ...per column stats... },
+ * 	 "colname2": { ...per column stats... },
+ *   ...
+ * }
+ *
+ */
+static
+void import_pg_statistic(Relation rel, bool inh, JsonbContainer *cont)
+{
+	TupleDesc   		tupdesc = RelationGetDescr(rel);
+	Oid					relid = RelationGetRelid(rel);
+	int					natts = tupdesc->natts;
+	CatalogIndexState	indstate = NULL;
+	Relation			sd;
+	int					i;
+	bool				has_index_exprs = false;
+	ListCell		   *indexpr_item = NULL;
+
+	if (cont == NULL)
+		return;
+
+	sd = table_open(StatisticRelationId, RowExclusiveLock);
+
+	/*
+	 * 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++)
+	{
+
+		Form_pg_attribute	att;
+		char			   *name;
+		JsonbContainer	   *attrcont;
+		VacAttrStats	   *stat;
+		Node			   *index_expr = NULL;
+
+		att = TupleDescAttr(tupdesc, i);
+
+		if (att->attisdropped)
+			continue;
+
+		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);
+		}
+
+		stat = examine_rel_attribute(att, rel, index_expr);
+
+		name = NameStr(att->attname);
+
+		attrcont = key_lookup_object(cont, name);
+
+		if (attrcont != NULL)
+		{
+			Datum		values[Natts_pg_statistic] = { 0 };
+			bool		nulls[Natts_pg_statistic] = { false };
+			bool		replaces[Natts_pg_statistic] = { false };
+			HeapTuple	stup,
+						oldtup;
+
+			import_attribute(relid, stat, attrcont, inh, values, nulls, replaces);
+
+			/* Is there already a pg_statistic tuple for this attribute? */
+			oldtup = SearchSysCache3(STATRELATTINH,
+									 ObjectIdGetDatum(RelationGetRelid(rel)),
+									 Int16GetDatum(att->attnum),
+									 BoolGetDatum(inh));
+
+			/* Open index information when we know we need it */
+			if (indstate == NULL)
+				indstate = CatalogOpenIndexes(sd);
+
+			if (HeapTupleIsValid(oldtup))
+			{
+				/* Yes, replace it */
+				stup = heap_modify_tuple(oldtup,
+										 RelationGetDescr(sd),
+										 values,
+										 nulls,
+										 replaces);
+				ReleaseSysCache(oldtup);
+				CatalogTupleUpdateWithInfo(sd, &stup->t_self, stup, indstate);
+			}
+			else
+			{
+				/* No, insert new tuple */
+				stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
+				CatalogTupleInsertWithInfo(sd, stup, indstate);
+			}
+			heap_freetuple(stup);
+		}
+		/* DEBUG pfree(stat); */
+	}
+
+	if (indstate != NULL)
+		CatalogCloseIndexes(indstate);
+	table_close(sd, RowExclusiveLock);
+}
+
+/*
+ * Import statitics for one attribute
+ *
+ */
+void
+import_attribute(Oid relid, const VacAttrStats *stat,
+				 JsonbContainer *cont, bool inh,
+				 Datum values[], bool nulls[], bool replaces[])
+{
+	JsonbContainer *arraycont;
+	char		   *s;
+	int16			kindenums[STATISTIC_NUM_SLOTS] = {0};
+
+	Assert(cont != NULL);
+
+	values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(relid);
+	values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(stat->tupattnum);
+	values[Anum_pg_statistic_stainherit - 1] = BoolGetDatum(inh);
+
+	s = key_lookup_cstring(cont, "stanullfrac");
+	if (s != NULL)
+	{
+		float4 f = float4in_internal(s, NULL, "real", s, NULL);
+		pfree(s);
+		values[Anum_pg_statistic_stanullfrac - 1] = Float4GetDatum(f);
+		replaces[Anum_pg_statistic_stanullfrac - 1] = true;
+	}
+
+	s = key_lookup_cstring(cont, "stawidth");
+	if (s != NULL)
+	{
+		int32 d = pg_strtoint32(s);
+		pfree(s);
+		values[Anum_pg_statistic_stawidth - 1] = Int32GetDatum(d);
+		replaces[Anum_pg_statistic_stawidth - 1] = true;
+	}
+
+	s = key_lookup_cstring(cont, "stadistinct");
+	if (s != NULL)
+	{
+		float4 f = float4in_internal(s, NULL, "real", s, NULL);
+		pfree(s);
+		values[Anum_pg_statistic_stadistinct - 1] = Float4GetDatum(f);
+		replaces[Anum_pg_statistic_stadistinct - 1] = true;
+	}
+
+	arraycont = key_lookup_array(cont, "stakinds");
+	import_stakinds(stat, arraycont, inh, kindenums,
+					&values[Anum_pg_statistic_stakind1 - 1],
+					&nulls[Anum_pg_statistic_stakind1 - 1],
+					&replaces[Anum_pg_statistic_stakind1 - 1],
+					&values[Anum_pg_statistic_staop1 - 1],
+					&nulls[Anum_pg_statistic_staop1 - 1],
+					&replaces[Anum_pg_statistic_staop1 - 1],
+					&values[Anum_pg_statistic_stacoll1 - 1],
+					&nulls[Anum_pg_statistic_stacoll1 - 1],
+					&replaces[Anum_pg_statistic_stacoll1 - 1]);
+
+	arraycont = key_lookup_array(cont, "stanumbers");
+	import_stanumbers(stat, arraycont,
+					  &values[Anum_pg_statistic_stanumbers1 - 1],
+					  &nulls[Anum_pg_statistic_stanumbers1 - 1],
+					  &replaces[Anum_pg_statistic_stanumbers1 - 1]);
+
+	arraycont = key_lookup_array(cont, "stavalues");
+	import_stavalues(stat, arraycont, kindenums,
+					 &values[Anum_pg_statistic_stavalues1 - 1],
+					 &nulls[Anum_pg_statistic_stavalues1 - 1],
+					 &replaces[Anum_pg_statistic_stavalues1 - 1]);
+}
+
+/*
+ * import stakinds values from json, the values of which determine
+ * the staop and stacoll values to use as well.
+ */
+static
+void import_stakinds(const VacAttrStats *stat, JsonbContainer *cont,
+					 bool inh, int16 kindenums[], Datum kindvalues[],
+					 bool kindnulls[], bool kindreplaces[], Datum opvalues[],
+					 bool opnulls[], bool opreplaces[], Datum collvalues[],
+					 bool collnulls[], bool collreplaces[])
+{
+	int k;
+	int numkinds = 0;
+
+	if (cont != NULL)
+	{
+		TypeCacheEntry *typentry = lookup_type_cache(stat->attrtypid,
+													TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR);
+		Datum	lt_opr = ObjectIdGetDatum(typentry->lt_opr);
+		Datum	eq_opr = ObjectIdGetDatum(typentry->eq_opr);
+
+		numkinds = JsonContainerSize(cont);
+
+		if (numkinds > STATISTIC_NUM_SLOTS)
+			ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("invalid format: number of stakinds %d is greater than available slots %d",
+						numkinds, STATISTIC_NUM_SLOTS)));
+
+		for (k = 0; k < numkinds; k++)
+		{
+			JsonbValue *j = getIthJsonbValueFromContainer(cont, k);
+			int16		kind;
+			char	   *s;
+
+			if (j == NULL || (j->type != jbvString))
+				ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("invalid format: stakind elements must be strings")));
+
+			s = JsonbStringValueToCString(j);
+			kind = decode_stakind_string(s);
+			pfree(s);
+			pfree(j);
+
+			kindenums[k] = kind;
+			kindvalues[k] = Int16GetDatum(kind);
+			kindreplaces[k] = true;
+
+			switch(kind)
+			{
+				case STATISTIC_KIND_MCV:
+					opvalues[k] = eq_opr;
+					opreplaces[k] = true;
+					collvalues[k] = ObjectIdGetDatum(stat->attrcollid);
+					collreplaces[k] = true;
+					break;
+
+				case STATISTIC_KIND_HISTOGRAM:
+				case STATISTIC_KIND_CORRELATION:
+					opvalues[k] = lt_opr;
+					opreplaces[k] = true;
+					collvalues[k] = ObjectIdGetDatum(stat->attrcollid);
+					collreplaces[k] = true;
+					break;
+
+				case STATISTIC_KIND_MCELEM:
+				case STATISTIC_KIND_DECHIST:
+					opvalues[k] = ObjectIdGetDatum(TextEqualOperator);
+					opreplaces[k] = true;
+					collvalues[k] = ObjectIdGetDatum(DEFAULT_COLLATION_OID);
+					collreplaces[k] = true;
+					break;
+
+				case STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM:
+					opvalues[k] = ObjectIdGetDatum(Float8LessOperator);
+					opreplaces[k] = true;
+					collvalues[k] = ObjectIdGetDatum(InvalidOid);
+					collreplaces[k] = true;
+					break;
+
+				case STATISTIC_KIND_BOUNDS_HISTOGRAM:
+				default:
+					opvalues[k] = ObjectIdGetDatum(InvalidOid);
+					opreplaces[k] = true;
+					collvalues[k] = ObjectIdGetDatum(InvalidOid);
+					collreplaces[k] = true;
+					break;
+			}
+		}
+	}
+
+	/* fill out empty slots, but do not replace */
+	for (k = numkinds; k < STATISTIC_NUM_SLOTS; k++)
+	{
+		kindvalues[k] = Int16GetDatum(0);
+		opvalues[k] = ObjectIdGetDatum(InvalidOid);
+		collvalues[k] = ObjectIdGetDatum(InvalidOid);
+	}
+}
+
+static
+void import_stanumbers(const VacAttrStats *stat, JsonbContainer *cont,
+					   Datum numvalues[], bool numnulls[],
+					   bool numreplaces[])
+{
+	int numnumbers = 0;
+	int k;
+
+	if (cont != NULL)
+	{
+		FmgrInfo	finfo;
+
+		numnumbers = JsonContainerSize(cont);
+
+		if (numnumbers > STATISTIC_NUM_SLOTS)
+			ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("invalid format: number of stanumbers %d is greater than available slots %d",
+						numnumbers, STATISTIC_NUM_SLOTS)));
+
+		fmgr_info(F_ARRAY_IN, &finfo);
+
+		for (k = 0; k < numnumbers; k++)
+		{
+			JsonbValue *j = getIthJsonbValueFromContainer(cont, k);
+
+			if (j == NULL)
+			{
+				numvalues[k] = (Datum) 0;
+				numnulls[k] = true;
+				continue;
+			}
+
+			if (j->type == jbvNull)
+			{
+				numvalues[k] = (Datum) 0;
+				numnulls[k] = true;
+				pfree(j);
+				continue;
+			}
+
+			if (j->type == jbvString)
+			{
+				char *s = JsonbStringValueToCString(j);
+
+				numvalues[k] = FunctionCall3(&finfo, CStringGetDatum(s),
+											 ObjectIdGetDatum(FLOAT4OID),
+											 Int32GetDatum(0));
+				numreplaces[k] = true;
+				pfree(s);
+				pfree(j);
+				continue;
+			}
+			else
+				ereport(ERROR,
+				  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				   errmsg("invalid statistics format, stanumbers elements "
+						  "must be a string that is castable to an array of floats")));
+
+		}
+	}
+
+	/* fill out empty slots, but do not replace */
+	for (k = numnumbers; k < STATISTIC_NUM_SLOTS; k++)
+	{
+		numvalues[k] = (Datum) 0;
+		numnulls[k] = true;
+	}
+}
+
+static
+void import_stavalues(const VacAttrStats *stat, JsonbContainer *cont,
+					  int16 kindenums[], Datum valvalues[],
+					  bool valnulls[], bool valreplaces[])
+{
+	int numvals = 0;
+	int k;
+
+	if (cont != NULL)
+	{
+		FmgrInfo	finfo;
+
+		fmgr_info(F_ARRAY_IN, &finfo);
+		numvals = JsonContainerSize(cont);
+
+		if (numvals > STATISTIC_NUM_SLOTS)
+			ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("invalid format: number of stavalues %d is greater than available slots %d",
+						numvals, STATISTIC_NUM_SLOTS)));
+
+		for (k = 0; k < numvals; k++)
+		{
+			JsonbValue *j = getIthJsonbValueFromContainer(cont, k);
+
+			if (j == NULL)
+			{
+				valvalues[k] = (Datum) 0;
+				valnulls[k] = true;
+				continue;
+			}
+
+			if (j->type == jbvNull)
+			{
+				valvalues[k] = (Datum) 0;
+				valnulls[k] = true;
+				pfree(j);
+				continue;
+			}
+
+			if (j->type == jbvString)
+			{
+				char   *s = JsonbStringValueToCString(j);
+				Oid		typoid = stat->statypid[k];
+				int32	typmod = 0;
+
+				/*
+				 * MCELEM stat arrays are of the same type as the
+				 * array base element type.
+				 */
+				if (kindenums[k] == STATISTIC_KIND_MCELEM)
+				{
+					TypeCacheEntry *typentry = lookup_type_cache(typoid, 0);
+					if (IsTrueArrayType(typentry))
+						typoid = typentry->typelem;
+				}
+				valvalues[k] = FunctionCall3(&finfo, CStringGetDatum(s),
+								 ObjectIdGetDatum(typoid),
+								 Int32GetDatum(typmod));
+				valreplaces[k] = true;
+				pfree(s);
+				pfree(j);
+			}
+			else
+				ereport(ERROR,
+				  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				   errmsg("invalid statistics format, stavalues elements must "
+						  "be a string that is castable to an array of the "
+						  "column type")));
+
+		}
+	}
+
+	/* fill out empty slots, but do not replace */
+	for (k = numvals; k < STATISTIC_NUM_SLOTS; k++)
+	{
+		valvalues[k] = (Datum) 0;
+		valnulls[k] = true;
+	}
+}
+
+/*
+ * Get a JsonbValue from a JsonbContainer and ensure that it is a string,
+ * and return the cstring.
+ */
+char *key_lookup_cstring(JsonbContainer *cont, const char *key)
+{
+	JsonbValue	j;
+
+	if (!getKeyJsonValueFromContainer(cont, key, strlen(key), &j))
+		return NULL;
+
+	if (j.type != jbvString)
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("invalid statistics format, %s must be a string but is type %s",
+				  key, JsonbTypeName(&j))));
+
+	return JsonbStringValueToCString(&j);
+}
+
+/*
+ * Get a JsonbContainer from a JsonbContainer and ensure that it is a object
+ */
+JsonbContainer *key_lookup_object(JsonbContainer *cont, const char *key)
+{
+	JsonbValue		j;
+
+	if (!getKeyJsonValueFromContainer(cont, key, strlen(key), &j))
+		return NULL;
+
+	if (j.type != jbvBinary)
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("invalid statistics format, %s must be an object but is type %s",
+				  key, JsonbTypeName(&j))));
+
+	if (!JsonContainerIsObject(j.val.binary.data))
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("invalid statistics format, %s must be an object but is type %s",
+				  key, JsonbContainerTypeName(j.val.binary.data))));
+
+	return j.val.binary.data;
+}
+
+/*
+ * Get a JsonbContainer from a JsonbContainer and ensure that it is an array
+ */
+JsonbContainer *key_lookup_array(JsonbContainer *cont, const char *key)
+{
+	JsonbValue	j;
+
+	if (!getKeyJsonValueFromContainer(cont, key, strlen(key), &j))
+		return NULL;
+
+	if (j.type != jbvBinary)
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("invalid statistics format, %s must be an array but is type %s",
+				  key, JsonbTypeName(&j))));
+
+	if (!JsonContainerIsArray(j.val.binary.data))
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("invalid statistics format, %s must be an array but is type %s",
+				  key, JsonbContainerTypeName(j.val.binary.data))));
+
+	return j.val.binary.data;
+}
+
+/*
+ * Convert the STATISTICS_KIND strings defined in pg_statistic_export
+ * back to their defined enum values.
+ */
+static int16
+decode_stakind_string(char *s)
+{
+	if (strcmp(s,"MCV") == 0)
+		return STATISTIC_KIND_MCV;
+	if (strcmp(s,"HISTOGRAM") == 0)
+		return STATISTIC_KIND_HISTOGRAM;
+	if (strcmp(s,"CORRELATION") == 0)
+		return STATISTIC_KIND_CORRELATION;
+	if (strcmp(s,"MCELEM") == 0)
+		return STATISTIC_KIND_MCELEM;
+	if (strcmp(s,"DECHIST") == 0)
+		return STATISTIC_KIND_DECHIST;
+	if (strcmp(s,"RANGE_LENGTH_HISTOGRAM") == 0)
+		return STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM;
+	if (strcmp(s,"BOUNDS_HISTOGRAM") == 0)
+		return STATISTIC_KIND_BOUNDS_HISTOGRAM;
+	if (strcmp(s,"TRIVIAL") != 0)
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("unknown statistics kind: %s", s)));
+
+	return 0;
+}
+
+/*
+ * 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(Form_pg_attribute attr, Relation onerel, Node *index_expr)
+{
+	HeapTuple		typtuple;
+	int				i;
+	bool			ok;
+	VacAttrStats   *stats;
+
+	/* Never analyze dropped columns */
+	if (attr->attisdropped)
+		return NULL;
+
+	/* Don't analyze column if user has specified not to */
+	if (attr->attstattarget == 0)
+		return NULL;
+
+	/*
+	 * Create the VacAttrStats struct.
+	 */
+	stats = (VacAttrStats *) palloc0(sizeof(VacAttrStats));
+	stats->attstattarget = attr->attstattarget;
+
+	/*
+	 * 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[attr->attnum - 1]))
+			stats->attrcollid = onerel->rd_indcollation[attr->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 = CurrentMemoryContext;
+	stats->tupattnum = attr->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;
+}
+
+/*
+ * Import statistics (pg_statistic) into a relation
+ */
+Datum
+pg_import_rel_stats(PG_FUNCTION_ARGS)
+{
+	Oid			relid;
+	int32		stats_version_num;
+	Jsonb	   *jb;
+	Relation	rel;
+
+	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))
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("server_version_number cannot be NULL")));
+	stats_version_num = PG_GETARG_INT32(1);
+
+	if (stats_version_num < 80000)
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("invalid statistics version: %d is earlier than earliest supported version",
+				  stats_version_num)));
+
+	if (PG_ARGISNULL(4))
+		jb = NULL;
+	else
+	{
+		jb = PG_GETARG_JSONB_P(4);
+		if (!JB_ROOT_IS_OBJECT(jb))
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("columns must be jsonb object at root")));
+	}
+
+	rel = relation_open(relid, ShareUpdateExclusiveLock);
+
+	/*
+	 * Apply statistical updates, if any, to copied tuple.
+	 *
+	 * Format is:
+	 * {
+	 *   "regular": { "columns": ..., "extended": ...},
+	 *   "inherited": { "columns": ..., "extended": ...}
+	 * }
+	 *
+	 */
+	if (jb != NULL)
+	{
+		JsonbContainer	   *cont;
+
+		cont = key_lookup_object(&jb->root, "regular");
+		import_pg_statistic(rel, false, cont);
+
+		if (rel->rd_rel->relhassubclass)
+		{
+			cont = key_lookup_object(&jb->root, "inherited");
+			import_pg_statistic(rel, true, cont);
+		}
+	}
+
+	/* only modify pg_class row if changes are to be made */
+	if ( ! PG_ARGISNULL(2) || ! PG_ARGISNULL(3) )
+	{
+		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 (! PG_ARGISNULL(2))
+			pgcform->reltuples = PG_GETARG_FLOAT4(2);
+		if (! PG_ARGISNULL(3))
+			pgcform->relpages = PG_GETARG_INT32(3);
+
+		heap_inplace_update(pg_class_rel, ctup);
+		table_close(pg_class_rel, ShareUpdateExclusiveLock);
+	}
+
+	/* relation_close(onerel, ShareUpdateExclusiveLock); */
+	relation_close(rel, NoLock);
+
+	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..2490472198
--- /dev/null
+++ b/src/test/regress/expected/stats_export_import.out
@@ -0,0 +1,137 @@
+CREATE TYPE stats_import_complex_type AS (
+    a integer,
+    b float,
+    c text,
+    d date,
+    e jsonb);
+CREATE TABLE stats_import_test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_import_complex_type,
+    tags text[]
+);
+INSERT INTO stats_import_test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_import_complex_type, array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_import_complex_type, array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import_complex_type, array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, NULL;
+CREATE INDEX is_odd ON stats_import_test(((comp).a % 2 = 1));
+ANALYZE stats_import_test;
+-- capture snapshot of source stats
+CREATE TABLE stats_export AS
+SELECT e.*
+FROM pg_catalog.pg_statistic_export AS e
+WHERE e.schemaname = 'public'
+AND e.relname IN ('stats_import_test', 'is_odd');
+SELECT c.reltuples AS before_tuples, c.relpages AS before_pages
+FROM pg_class AS c
+WHERE oid = 'stats_import_test'::regclass;
+ before_tuples | before_pages 
+---------------+--------------
+             4 |            1
+(1 row)
+
+-- test settting tuples and pages but no columns
+SELECT pg_import_rel_stats(c.oid,
+                           current_setting('server_version_num')::integer,
+                           1000.0, 200, NULL::jsonb)
+FROM pg_class AS c
+WHERE oid = 'stats_import_test'::regclass;
+ pg_import_rel_stats 
+---------------------
+ t
+(1 row)
+
+SELECT c.reltuples AS after_tuples, c.relpages AS after_pages
+FROM pg_class AS c
+WHERE oid = 'stats_import_test'::regclass;
+ after_tuples | after_pages 
+--------------+-------------
+         1000 |         200
+(1 row)
+
+-- create a table just like stats_import_test
+CREATE TABLE stats_import_clone ( LIKE stats_import_test );
+-- create an index just like is_odd
+CREATE INDEX is_odd2 ON stats_import_clone(((comp).a % 2 = 0));
+-- copy table stats to clone table
+SELECT pg_import_rel_stats(c.oid, e.server_version_num,
+                            e.n_tuples, e.n_pages, e.stats)
+FROM pg_class AS c
+JOIN pg_namespace AS n
+ON n.oid = c.relnamespace
+JOIN stats_export AS e
+ON e.schemaname = 'public'
+AND e.relname = 'stats_import_test'
+WHERE c.oid = 'stats_import_clone'::regclass;
+ pg_import_rel_stats 
+---------------------
+ t
+(1 row)
+
+-- copy index stats to clone index
+SELECT pg_import_rel_stats(c.oid, e.server_version_num,
+                            e.n_tuples, e.n_pages, e.stats)
+FROM pg_class AS c
+JOIN pg_namespace AS n
+ON n.oid = c.relnamespace
+JOIN stats_export AS e
+ON e.schemaname = 'public'
+AND e.relname = 'is_odd'
+WHERE c.oid = 'is_odd2'::regclass;
+ pg_import_rel_stats 
+---------------------
+ t
+(1 row)
+
+-- table stats must match
+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 AS s
+WHERE s.starelid = 'stats_import_test'::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,
+        stavalues1::text AS sv1, stavalues2::text AS sv2, stavalues3::text AS sv3,
+        stavalues4::text AS sv4, stavalues5::text AS sv5
+FROM pg_statistic AS s
+WHERE s.starelid = 'stats_import_clone'::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)
+
+-- index stats must match
+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 AS s
+WHERE s.starelid = '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,
+        stavalues1::text AS sv1, stavalues2::text AS sv2, stavalues3::text AS sv3,
+        stavalues4::text AS sv4, stavalues5::text AS sv5
+FROM pg_statistic AS s
+WHERE s.starelid = 'is_odd2'::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)
+
+DROP TABLE stats_export;
+DROP TABLE stats_import_clone;
+DROP TABLE stats_import_test;
+DROP TYPE stats_import_complex_type;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index f0987ff537..09ffd43fc6 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..e97b9d1064
--- /dev/null
+++ b/src/test/regress/sql/stats_export_import.sql
@@ -0,0 +1,119 @@
+CREATE TYPE stats_import_complex_type AS (
+    a integer,
+    b float,
+    c text,
+    d date,
+    e jsonb);
+
+CREATE TABLE stats_import_test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_import_complex_type,
+    tags text[]
+);
+
+INSERT INTO stats_import_test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_import_complex_type, array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_import_complex_type, array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import_complex_type, array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, NULL;
+
+CREATE INDEX is_odd ON stats_import_test(((comp).a % 2 = 1));
+
+ANALYZE stats_import_test;
+
+-- capture snapshot of source stats
+CREATE TABLE stats_export AS
+SELECT e.*
+FROM pg_catalog.pg_statistic_export AS e
+WHERE e.schemaname = 'public'
+AND e.relname IN ('stats_import_test', 'is_odd');
+
+SELECT c.reltuples AS before_tuples, c.relpages AS before_pages
+FROM pg_class AS c
+WHERE oid = 'stats_import_test'::regclass;
+
+-- test settting tuples and pages but no columns
+SELECT pg_import_rel_stats(c.oid,
+                           current_setting('server_version_num')::integer,
+                           1000.0, 200, NULL::jsonb)
+FROM pg_class AS c
+WHERE oid = 'stats_import_test'::regclass;
+
+SELECT c.reltuples AS after_tuples, c.relpages AS after_pages
+FROM pg_class AS c
+WHERE oid = 'stats_import_test'::regclass;
+
+-- create a table just like stats_import_test
+CREATE TABLE stats_import_clone ( LIKE stats_import_test );
+
+-- create an index just like is_odd
+CREATE INDEX is_odd2 ON stats_import_clone(((comp).a % 2 = 0));
+
+-- copy table stats to clone table
+SELECT pg_import_rel_stats(c.oid, e.server_version_num,
+                            e.n_tuples, e.n_pages, e.stats)
+FROM pg_class AS c
+JOIN pg_namespace AS n
+ON n.oid = c.relnamespace
+JOIN stats_export AS e
+ON e.schemaname = 'public'
+AND e.relname = 'stats_import_test'
+WHERE c.oid = 'stats_import_clone'::regclass;
+
+-- copy index stats to clone index
+SELECT pg_import_rel_stats(c.oid, e.server_version_num,
+                            e.n_tuples, e.n_pages, e.stats)
+FROM pg_class AS c
+JOIN pg_namespace AS n
+ON n.oid = c.relnamespace
+JOIN stats_export AS e
+ON e.schemaname = 'public'
+AND e.relname = 'is_odd'
+WHERE c.oid = 'is_odd2'::regclass;
+
+-- table stats must match
+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 AS s
+WHERE s.starelid = 'stats_import_test'::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,
+        stavalues1::text AS sv1, stavalues2::text AS sv2, stavalues3::text AS sv3,
+        stavalues4::text AS sv4, stavalues5::text AS sv5
+FROM pg_statistic AS s
+WHERE s.starelid = 'stats_import_clone'::regclass;
+
+-- index stats must match
+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 AS s
+WHERE s.starelid = '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,
+        stavalues1::text AS sv1, stavalues2::text AS sv2, stavalues3::text AS sv3,
+        stavalues4::text AS sv4, stavalues5::text AS sv5
+FROM pg_statistic AS s
+WHERE s.starelid = 'is_odd2'::regclass;
+
+DROP TABLE stats_export;
+DROP TABLE stats_import_clone;
+DROP TABLE stats_import_test;
+DROP TYPE stats_import_complex_type;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 20da3ed033..ae3d1073e3 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -28151,6 +28151,49 @@ 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>server_version_num</parameter> <type>integer</type>, <parameter>num_tuples</parameter> <type>float4</type>, <parameter>num_pages</parameter> <type>integer</type>, <parameter>column_stats</parameter> <type>jsonb</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>column_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 <program>pg_upgrade</program> and
+        <program>pg_restore</program> to convey the statistics from the old system
+        version into the new one.
+       </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
    <table id="functions-admin-dblocation">
     <title>Database Object Location Functions</title>
     <tgroup cols="1">
-- 
2.43.0

v3-0004-Add-pg_export_stats-pg_import_stats.patchtext/x-patch; charset=US-ASCII; name=v3-0004-Add-pg_export_stats-pg_import_stats.patchDownload
From bedf3ff9be5f26ed435f90e6db8405a5e7e32e67 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Tue, 12 Dec 2023 22:25:51 -0500
Subject: [PATCH v3 4/9] Add pg_export_stats, pg_import_stats.

pg_export_stats is used to export stats from databases as far back as
v10. The output is currently only to stdout and should be redirected to
a file in most use cases.

pg_import_stats is used to import stats to any version that has the
function pg_import_rel_stats().
---
 src/bin/scripts/.gitignore        |   2 +
 src/bin/scripts/Makefile          |   6 +-
 src/bin/scripts/pg_export_stats.c | 298 +++++++++++++++++++++++++++++
 src/bin/scripts/pg_import_stats.c | 303 ++++++++++++++++++++++++++++++
 4 files changed, 608 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 20db40b103..a019894d84 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)
@@ -31,6 +31,8 @@ clusterdb: clusterdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport su
 vacuumdb: vacuumdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
 reindexdb: reindexdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
 pg_isready: pg_isready.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
+pg_export_stats: pg_export_stats.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
+pg_import_stats: pg_import_stats.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
 
 install: all installdirs
 	$(INSTALL_PROGRAM) createdb$(X)   '$(DESTDIR)$(bindir)'/createdb$(X)
@@ -41,6 +43,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..abb3659e20
--- /dev/null
+++ b/src/bin/scripts/pg_export_stats.c
@@ -0,0 +1,298 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_export_stats
+ *
+ * Portions Copyright (c) 1996-2023, 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);
+
+/* view definition introduced in 17 */
+const char *export_query_v17 =
+	"SELECT schemaname, relname, server_version_num, n_tuples, "
+	"n_pages, stats FROM pg_statistic_export ";
+
+/*
+ * Versions 10-16 have the same stats layout, but lack the view definition,
+ * so extracting the view definition ad using it as-is will work.
+ */
+const char *export_query_v10 =
+	"SELECT "
+	"    n.nspname AS schemaname, "
+	"    r.relname AS relname, "
+	"    current_setting('server_version_num')::integer AS server_version_num, "
+	"    r.reltuples::float4 AS n_tuples, "
+	"    r.relpages::integer AS n_pages, "
+	"    ( "
+	"        SELECT "
+	"            jsonb_object_agg( "
+	"                CASE "
+	"                    WHEN a.stainherit THEN 'inherited' "
+	"                    ELSE 'regular' "
+	"                END, "
+	"                a.stats "
+	"            ) "
+	"        FROM "
+	"        ( "
+	"            SELECT "
+	"                s.stainherit, "
+	"                jsonb_object_agg( "
+	"                    a.attname, "
+	"                    jsonb_build_object( "
+	"                        'stanullfrac', s.stanullfrac::text, "
+	"                        'stawidth', s.stawidth::text, "
+	"                        'stadistinct', s.stadistinct::text, "
+	"                        'stakinds', "
+	"                        ( "
+	"                            SELECT "
+	"                                jsonb_agg( "
+	"                                    CASE kind.kind "
+	"                                        WHEN 0 THEN 'TRIVIAL' "
+	"                                        WHEN 1 THEN 'MCV' "
+	"                                        WHEN 2 THEN 'HISTOGRAM' "
+	"                                        WHEN 3 THEN 'CORRELATION' "
+	"                                        WHEN 4 THEN 'MCELEM' "
+	"                                        WHEN 5 THEN 'DECHIST' "
+	"                                        WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM' "
+	"                                        WHEN 7 THEN 'BOUNDS_HISTOGRAM' "
+	"                                    END::text "
+	"                                    ORDER BY kind.ord) "
+	"                            FROM unnest(ARRAY[s.stakind1, s.stakind2, "
+	"                                        s.stakind3, stakind4, "
+	"                                        s.stakind5]) "
+	"                                 WITH ORDINALITY AS kind(kind, ord) "
+	"                        ), "
+	"                        'stanumbers', "
+	"                        jsonb_build_array( "
+	"                            s.stanumbers1::text, "
+	"                            s.stanumbers2::text, "
+	"                            s.stanumbers3::text, "
+	"                            s.stanumbers4::text, "
+	"                            s.stanumbers5::text), "
+	"                        'stavalues', "
+	"                        jsonb_build_array( "
+	"                            s.stavalues1::text, "
+	"                            s.stavalues2::text, "
+	"                            s.stavalues3::text, "
+	"                            s.stavalues4::text, "
+	"                            s.stavalues5::text) "
+	"                    ) "
+	"                ) AS stats "
+	"            FROM pg_attribute AS a "
+	"            JOIN pg_statistic AS s "
+	"                ON s.starelid = a.attrelid "
+	"                AND s.staattnum = a.attnum "
+	"            WHERE a.attrelid = r.oid "
+	"            AND NOT a.attisdropped "
+	"            AND a.attnum > 0 "
+	"            AND has_column_privilege(a.attrelid, a.attnum, 'SELECT') "
+	"            GROUP BY s.stainherit "
+	"        ) AS a "
+	"    ) AS stats "
+	"FROM pg_class AS r "
+	"JOIN pg_namespace AS n "
+	"    ON n.oid = r.relnamespace "
+	"WHERE relkind IN ('r', 'm', 'f', 'p', 'i') "
+	"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;
+
+	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);
+
+	initPQExpBuffer(&sql);
+
+	appendPQExpBufferStr(&sql, "COPY (");
+
+	if (PQserverVersion(conn) >= 170000)
+		appendPQExpBufferStr(&sql, export_query_v17);
+	else if (PQserverVersion(conn) >= 100000)
+		appendPQExpBufferStr(&sql, export_query_v10);
+	else
+		pg_fatal("exporting statistics from databases prior to version 10 not supported");
+
+	appendPQExpBufferStr(&sql, ") TO STDOUT");
+
+	result = PQexec(conn, sql.data);
+	result_status = PQresultStatus(result);
+
+	if (result_status != PGRES_COPY_OUT)
+		pg_fatal("malformed copy command");
+
+	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..122afc0971
--- /dev/null
+++ b/src/bin/scripts/pg_import_stats.c
@@ -0,0 +1,303 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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;
+
+	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 */
+
+
+	result = PQexec(conn,
+		"CREATE TEMPORARY TABLE import_stats ( "
+		"id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, "
+		"schemaname text, relname text, server_version_num integer, "
+		"n_tuples float4, n_pages integer, stats jsonb )");
+
+	if (PQresultStatus(result) != PGRES_COMMAND_OK)
+		pg_fatal("could not create temporary file: %s", PQerrorMessage(conn));
+
+	PQclear(result);
+
+	result = PQexec(conn,
+		"COPY import_stats(schemaname, relname, server_version_num, n_tuples, "
+		"n_pages, 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));
+
+	numtables = atol(PQcmdTuples(result));
+
+	PQclear(result);
+
+	result = PQprepare(conn, "import",
+		"SELECT pg_import_rel_stats(c.oid, s.server_version_num, "
+		"             s.n_tuples, s.n_pages, s.stats) as import_result "
+		"FROM import_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",
+			"SELECT s.schemaname, s.relname "
+			"FROM import_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", 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", 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);
+	}
+
+	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

v3-0005-Add-system-view-pg_statistic_ext_export.patchtext/x-patch; charset=US-ASCII; name=v3-0005-Add-system-view-pg_statistic_ext_export.patchDownload
From 059494d82745258400a145b6aa71ce958f23347a Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 9 Dec 2023 04:59:23 -0500
Subject: [PATCH v3 5/9] Add system view pg_statistic_ext_export.

This view is designed to aid in the export (and re-import) of extended
statistics, mostly for upgrade/restore situations.
---
 src/backend/catalog/system_views.sql | 131 +++++++++++++++++++++++++++
 src/test/regress/expected/rules.out  |  34 +++++++
 doc/src/sgml/system-views.sgml       |   5 +
 3 files changed, 170 insertions(+)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 7655bf7458..8dca87c061 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -356,6 +356,137 @@ CREATE VIEW pg_statistic_export WITH (security_barrier) AS
     WHERE relkind IN ('r', 'm', 'f', 'p', 'i')
     AND n.nspname NOT IN ('pg_catalog', 'pg_toast', 'information_schema');
 
+CREATE VIEW pg_statistic_ext_export WITH (security_barrier) AS
+    SELECT
+        n.nspname AS schemaname,
+        r.relname AS tablename,
+        e.stxname AS ext_stats_name,
+        (current_setting('server_version_num'::text))::integer AS server_version_num,
+        jsonb_object_agg(
+            CASE sd.stxdinherit
+                WHEN true THEN 'inherited'
+                ELSE 'regular'
+            END,
+            jsonb_build_object(
+                'stxkinds',
+                to_jsonb(e.stxkind),
+                'stxdndistinct',
+                (
+                    SELECT
+                        jsonb_agg(
+                            -- att1, [, att2 ...] => attN: degree
+                            jsonb_build_object(
+                                'attnums',
+                                string_to_array(nd.attnums, ', '::text),
+                                'ndistinct',
+                                nd.ndistinct
+                                )
+                            ORDER BY nd.ord
+                        )
+                    -- jsonb does not preserve parsed order so use json
+                    FROM json_each_text(sd.stxdndistinct::text::json)
+                        WITH ORDINALITY AS nd(attnums, ndistinct, ord)
+                    WHERE sd.stxdndistinct IS NOT NULL
+                ),
+                'stxdndependencies',
+                (
+                    SELECT
+                        jsonb_agg(
+                            jsonb_build_object(
+                                'attnums',
+                                string_to_array(
+                                    replace(dep.attrs, ' => ', ', '), ', '
+                                ),
+                                'degree',
+                                dep.degree
+                            )
+                            ORDER BY dep.ord
+                        )
+                    FROM json_each_text(sd.stxddependencies::text::json)
+                        WITH ORDINALITY AS dep(attrs, degree, ord)
+                    WHERE sd.stxddependencies IS NOT NULL
+                ),
+                'stxdmcv',
+                (
+                    SELECT
+                        jsonb_agg(
+                            jsonb_build_object(
+                                'index',
+                                mcvl.index::text,
+                                'frequency',
+                                mcvl.frequency::text,
+                                'base_frequency',
+                                mcvl.base_frequency::text,
+                                'values',
+                                mcvl.values,
+                                'nulls',
+                                mcvl.nulls
+                            )
+                        )
+                    FROM pg_mcv_list_items(sd.stxdmcv) AS mcvl
+                    WHERE sd.stxdmcv IS NOT NULL
+                ),
+                'stxdexprs',
+                (
+                    SELECT
+                        jsonb_agg(
+                            jsonb_build_object(
+                                'stanullfrac',
+                                s.stanullfrac::text,
+                                'stawidth',
+                                s.stawidth::text,
+                                'stadistinct',
+                                s.stadistinct::text,
+                                'stakinds',
+                                (
+                                    SELECT
+                                        jsonb_agg(
+                                            CASE kind.kind
+                                                WHEN 0 THEN 'TRIVIAL'
+                                                WHEN 1 THEN 'MCV'
+                                                WHEN 2 THEN 'HISTOGRAM'
+                                                WHEN 3 THEN 'CORRELATION'
+                                                WHEN 4 THEN 'MCELEM'
+                                                WHEN 5 THEN 'DECHIST'
+                                                WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM'
+                                                WHEN 7 THEN 'BOUNDS_HISTOGRAM'
+                                                ELSE NULL
+                                            END
+                                            ORDER BY kind.ord
+                                        )
+                                    FROM unnest(ARRAY[s.stakind1, s.stakind2,
+                                                      s.stakind3, s.stakind4,
+                                                      s.stakind5])
+                                        WITH ORDINALITY kind(kind, ord)
+                                ),
+                                'stanumbers',
+                                jsonb_build_array(
+                                    s.stanumbers1::text,
+                                    s.stanumbers2::text,
+                                    s.stanumbers3::text,
+                                    s.stanumbers4::text,
+                                    s.stanumbers5::text
+                                ),
+                                'stavalues',
+                                jsonb_build_array(
+                                    s.stavalues1::text,
+                                    s.stavalues2::text,
+                                    s.stavalues3::text,
+                                    s.stavalues4::text,
+                                    s.stavalues5::text)
+                                )
+                                ORDER BY s.ordinality
+                            )
+                    FROM unnest(sd.stxdexpr) WITH ORDINALITY AS s
+                    WHERE sd.stxdexpr IS NOT NULL
+                )
+            )
+        ) AS stats
+    FROM pg_class r
+    JOIN pg_namespace n ON n.oid = r.relnamespace
+    JOIN pg_statistic_ext e ON e.stxrelid = r.oid
+    JOIN pg_statistic_ext_data sd ON sd.stxoid = e.oid
+    GROUP BY schemaname, tablename, ext_stats_name, server_version_num;
 
 
 CREATE VIEW pg_stats_ext WITH (security_barrier) AS
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index f2b059af5e..66ebac6cfd 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2435,6 +2435,40 @@ pg_statistic_export| SELECT n.nspname AS schemaname,
    FROM (pg_class r
      JOIN pg_namespace n ON ((n.oid = r.relnamespace)))
   WHERE ((r.relkind = ANY (ARRAY['r'::"char", 'm'::"char", 'f'::"char", 'p'::"char", 'i'::"char"])) AND (n.nspname <> ALL (ARRAY['pg_catalog'::name, 'pg_toast'::name, 'information_schema'::name])));
+pg_statistic_ext_export| SELECT n.nspname AS schemaname,
+    r.relname AS tablename,
+    e.stxname AS ext_stats_name,
+    (current_setting('server_version_num'::text))::integer AS server_version_num,
+    jsonb_object_agg(
+        CASE sd.stxdinherit
+            WHEN true THEN 'inherited'::text
+            ELSE 'regular'::text
+        END, jsonb_build_object('stxkinds', to_jsonb(e.stxkind), 'stxdndistinct', ( SELECT jsonb_agg(jsonb_build_object('attnums', string_to_array(nd.attnums, ', '::text), 'ndistinct', nd.ndistinct) ORDER BY nd.ord) AS jsonb_agg
+           FROM json_each_text(((sd.stxdndistinct)::text)::json) WITH ORDINALITY nd(attnums, ndistinct, ord)
+          WHERE (sd.stxdndistinct IS NOT NULL)), 'stxdndependencies', ( SELECT jsonb_agg(jsonb_build_object('attnums', string_to_array(replace(dep.attrs, ' => '::text, ', '::text), ', '::text), 'degree', dep.degree) ORDER BY dep.ord) AS jsonb_agg
+           FROM json_each_text(((sd.stxddependencies)::text)::json) WITH ORDINALITY dep(attrs, degree, ord)
+          WHERE (sd.stxddependencies IS NOT NULL)), 'stxdmcv', ( SELECT jsonb_agg(jsonb_build_object('index', (mcvl.index)::text, 'frequency', (mcvl.frequency)::text, 'base_frequency', (mcvl.base_frequency)::text, 'values', mcvl."values", 'nulls', mcvl.nulls)) AS jsonb_agg
+           FROM pg_mcv_list_items(sd.stxdmcv) mcvl(index, "values", nulls, frequency, base_frequency)
+          WHERE (sd.stxdmcv IS NOT NULL)), 'stxdexprs', ( SELECT jsonb_agg(jsonb_build_object('stanullfrac', (s.stanullfrac)::text, 'stawidth', (s.stawidth)::text, 'stadistinct', (s.stadistinct)::text, 'stakinds', ( SELECT jsonb_agg(
+                        CASE kind.kind
+                            WHEN 0 THEN 'TRIVIAL'::text
+                            WHEN 1 THEN 'MCV'::text
+                            WHEN 2 THEN 'HISTOGRAM'::text
+                            WHEN 3 THEN 'CORRELATION'::text
+                            WHEN 4 THEN 'MCELEM'::text
+                            WHEN 5 THEN 'DECHIST'::text
+                            WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM'::text
+                            WHEN 7 THEN 'BOUNDS_HISTOGRAM'::text
+                            ELSE NULL::text
+                        END ORDER BY kind.ord) AS jsonb_agg
+                   FROM unnest(ARRAY[s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5]) WITH ORDINALITY kind(kind, ord)), 'stanumbers', jsonb_build_array((s.stanumbers1)::text, (s.stanumbers2)::text, (s.stanumbers3)::text, (s.stanumbers4)::text, (s.stanumbers5)::text), 'stavalues', jsonb_build_array((s.stavalues1)::text, (s.stavalues2)::text, (s.stavalues3)::text, (s.stavalues4)::text, (s.stavalues5)::text)) ORDER BY s.ordinality) AS jsonb_agg
+           FROM unnest(sd.stxdexpr) WITH ORDINALITY s(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, stavalues2, stavalues3, stavalues4, stavalues5, ordinality)
+          WHERE (sd.stxdexpr IS NOT NULL)))) AS stats
+   FROM (((pg_class r
+     JOIN pg_namespace n ON ((n.oid = r.relnamespace)))
+     JOIN pg_statistic_ext e ON ((e.stxrelid = r.oid)))
+     JOIN pg_statistic_ext_data sd ON ((sd.stxoid = e.oid)))
+  GROUP BY n.nspname, r.relname, e.stxname, (current_setting('server_version_num'::text))::integer;
 pg_stats| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     a.attname,
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 91b3ab22fb..af5dff6a74 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -196,6 +196,11 @@
       <entry>planner statistics for export/upgrade purposes</entry>
      </row>
 
+     <row>
+      <entry><link linkend="view-pg-stats-ext"><structname>pg_stats_ext_export</structname></link></entry>
+      <entry>extended planner statistics for export/upgrade purposes</entry>
+     </row>
+
      <row>
       <entry><link linkend="view-pg-tables"><structname>pg_tables</structname></link></entry>
       <entry>tables</entry>
-- 
2.43.0

v3-0006-Create-create_stat_ext_entry-from-fetch_statentri.patchtext/x-patch; charset=US-ASCII; name=v3-0006-Create-create_stat_ext_entry-from-fetch_statentri.patchDownload
From 0797a698495c21baaef3b633e78425bcd7b16821 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Mon, 11 Dec 2023 03:23:28 -0500
Subject: [PATCH v3 6/9] Create create_stat_ext_entry() from
 fetch_statentries_for_relation().

Refactor fetch_statentries_for_relation() to use create_stat_ext_entry() in
its inner loop.

Later commits will make use of create_stat_ext_entry().

This was made its own commit for code clarity.
---
 src/backend/statistics/extended_stats.c | 146 +++++++++++++-----------
 1 file changed, 78 insertions(+), 68 deletions(-)

diff --git a/src/backend/statistics/extended_stats.c b/src/backend/statistics/extended_stats.c
index 7f014a0cbb..718826ecf1 100644
--- a/src/backend/statistics/extended_stats.c
+++ b/src/backend/statistics/extended_stats.c
@@ -418,6 +418,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 +520,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);
 	}
 
-- 
2.43.0

v3-0008-Allow-explicit-nulls-in-container-lookups.patchtext/x-patch; charset=US-ASCII; name=v3-0008-Allow-explicit-nulls-in-container-lookups.patchDownload
From 0bfed151e366b2e0389356613713ae33d859382e Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 13 Dec 2023 04:00:47 -0500
Subject: [PATCH v3 8/9] Allow explicit nulls in container lookups.

Allow key_lookup_object() and key_lookup_array() to treat explicit JSONB
jbvNull values the same as if the key were omitted entirely.

This allows export functions to create the keyed object via a query
without worrying about removing the key if there is no underlying data.

Also, having a key existing with an affirmative "there is no data here"
is more clear than the key missing.
---
 src/backend/statistics/statistics.c | 14 ++++++++++----
 1 file changed, 10 insertions(+), 4 deletions(-)

diff --git a/src/backend/statistics/statistics.c b/src/backend/statistics/statistics.c
index e54f1c9162..b1f0396d47 100644
--- a/src/backend/statistics/statistics.c
+++ b/src/backend/statistics/statistics.c
@@ -528,16 +528,19 @@ JsonbContainer *key_lookup_object(JsonbContainer *cont, const char *key)
 	if (!getKeyJsonValueFromContainer(cont, key, strlen(key), &j))
 		return NULL;
 
+	if (j.type == jbvNull)
+		return NULL;
+
 	if (j.type != jbvBinary)
 		ereport(ERROR,
 		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-		   errmsg("invalid statistics format, %s must be an object but is type %s",
+		   errmsg("invalid statistics format, %s must be an object or null but is type %s",
 				  key, JsonbTypeName(&j))));
 
 	if (!JsonContainerIsObject(j.val.binary.data))
 		ereport(ERROR,
 		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-		   errmsg("invalid statistics format, %s must be an object but is type %s",
+		   errmsg("invalid statistics format, %s must be an object or null but is type %s",
 				  key, JsonbContainerTypeName(j.val.binary.data))));
 
 	return j.val.binary.data;
@@ -553,16 +556,19 @@ JsonbContainer *key_lookup_array(JsonbContainer *cont, const char *key)
 	if (!getKeyJsonValueFromContainer(cont, key, strlen(key), &j))
 		return NULL;
 
+	if (j.type == jbvNull)
+		return NULL;
+
 	if (j.type != jbvBinary)
 		ereport(ERROR,
 		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-		   errmsg("invalid statistics format, %s must be an array but is type %s",
+		   errmsg("invalid statistics format, %s must be an array or null but is type %s",
 				  key, JsonbTypeName(&j))));
 
 	if (!JsonContainerIsArray(j.val.binary.data))
 		ereport(ERROR,
 		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-		   errmsg("invalid statistics format, %s must be an array but is type %s",
+		   errmsg("invalid statistics format, %s must be an array or null but is type %s",
 				  key, JsonbContainerTypeName(j.val.binary.data))));
 
 	return j.val.binary.data;
-- 
2.43.0

v3-0007-Add-pg_import_ext_stats.patchtext/x-patch; charset=US-ASCII; name=v3-0007-Add-pg_import_ext_stats.patchDownload
From 159763cbf70b11675321fb56a00e5ea1a8e4592b Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 13 Dec 2023 03:55:35 -0500
Subject: [PATCH v3 7/9] Add pg_import_ext_stats()

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 function takes a best-effort approach, skipping statistics that are
expected but omitted, skipping object that are specified but do not
exist on the target system. The goal is to get better-than-empty
statistics into the STATISTICS object quickly, so that business
operations can resume sooner. The statistics generated will replace
existing rows in pg_statistic_ext_data for the same statistics
object, and this is done in an all-or-nothing basis rather than
attempting to modify existing rows.

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

The medium of exchange is jsonb, the format of which is specified in the
view pg_statistic_ext_export. Obviously this view does not exist in older
versions of the database, but the view definition can be extracted and
adapted to older versions.

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      |   8 +-
 src/backend/statistics/dependencies.c         | 111 +++++++
 src/backend/statistics/extended_stats.c       | 291 ++++++++++++++++++
 src/backend/statistics/mcv.c                  | 217 ++++++++++++-
 src/backend/statistics/mvdistinct.c           | 101 ++++++
 src/backend/statistics/statistics.c           |   8 +-
 .../regress/expected/stats_export_import.out  |  20 ++
 src/test/regress/sql/stats_export_import.sql  |  18 ++
 doc/src/sgml/func.sgml                        |  21 ++
 10 files changed, 794 insertions(+), 6 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 4d1e9bde1f..bd674e232e 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5660,6 +5660,11 @@
   proname => 'pg_import_rel_stats', provolatile => 'v', proisstrict => 'f',
   proparallel => 'u', prorettype => 'bool', proargtypes => 'oid int4 float4 int4 jsonb',
   prosrc => 'pg_import_rel_stats' },
+{ oid => '9162',
+  descr => 'statistics: import to extended stats object',
+  proname => 'pg_import_ext_stats', provolatile => 'v', proisstrict => 't',
+  proparallel => 'u', prorettype => 'bool', proargtypes => 'oid int4 jsonb',
+  prosrc => 'pg_import_ext_stats' },
 { oid => '3150', descr => 'statistics: number of temporary files written',
   proname => 'pg_stat_get_db_temp_files', provolatile => 's',
   proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
diff --git a/src/include/statistics/extended_stats_internal.h b/src/include/statistics/extended_stats_internal.h
index 7b55eb8ffa..5fb4523525 100644
--- a/src/include/statistics/extended_stats_internal.h
+++ b/src/include/statistics/extended_stats_internal.h
@@ -68,17 +68,21 @@ typedef struct StatsBuildData
 	bool	  **nulls;
 } StatsBuildData;
 
-
 extern MVNDistinct *statext_ndistinct_build(double totalrows, StatsBuildData *data);
+extern MVNDistinct *statext_ndistinct_import(JsonbContainer *cont);
 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(JsonbContainer *cont);
 extern bytea *statext_dependencies_serialize(MVDependencies *dependencies);
 extern MVDependencies *statext_dependencies_deserialize(bytea *data);
+extern bytea *import_dependencies(JsonbContainer *cont);
 
 extern MCVList *statext_mcv_build(StatsBuildData *data,
 								  double totalrows, int stattarget);
+extern MCVList *statext_mcv_import(JsonbContainer *cont,
+									VacAttrStats **stats, int natts);
 extern bytea *statext_mcv_serialize(MCVList *mcvlist, VacAttrStats **stats);
 extern MCVList *statext_mcv_deserialize(bytea *data);
 
@@ -127,4 +131,6 @@ extern Selectivity mcv_clause_selectivity_or(PlannerInfo *root,
 											 Selectivity *overlap_basesel,
 											 Selectivity *totalsel);
 
+extern Datum pg_import_ext_stats(PG_FUNCTION_ARGS);
+
 #endif							/* EXTENDED_STATS_INTERNAL_H */
diff --git a/src/backend/statistics/dependencies.c b/src/backend/statistics/dependencies.c
index edb2e5347d..ca8b20adab 100644
--- a/src/backend/statistics/dependencies.c
+++ b/src/backend/statistics/dependencies.c
@@ -27,7 +27,9 @@
 #include "parser/parsetree.h"
 #include "statistics/extended_stats_internal.h"
 #include "statistics/statistics.h"
+#include "utils/builtins.h"
 #include "utils/bytea.h"
+#include "utils/float.h"
 #include "utils/fmgroids.h"
 #include "utils/fmgrprotos.h"
 #include "utils/lsyscache.h"
@@ -1829,3 +1831,112 @@ dependencies_clauselist_selectivity(PlannerInfo *root,
 
 	return s1;
 }
+
+/*
+ * statext_dependencies_import
+ *
+ * Like statext_dependencies_build, but import the data
+ * from a JSON object.
+ *
+ * import format:
+ * [
+ *   {
+ *     "attnums": [ intstr, ... ],
+ *     "degree": floatstr
+ *   }
+ * ]
+ *
+ */
+MVDependencies *
+statext_dependencies_import(JsonbContainer *cont)
+{
+	MVDependencies *dependencies = NULL;
+	int				ndeps;
+	int				i;
+
+
+	if (cont == NULL)
+		ndeps = 0;
+	else
+		ndeps = JsonContainerSize(cont);
+
+	if (ndeps == 0)
+		dependencies = (MVDependencies *) palloc0(sizeof(MVDependencies));
+	else
+		dependencies = (MVDependencies *) palloc0(offsetof(MVDependencies, deps)
+												   + (ndeps * sizeof(MVDependency *)));
+
+	dependencies->magic = STATS_DEPS_MAGIC;
+	dependencies->type = STATS_DEPS_TYPE_BASIC;
+	dependencies->ndeps = ndeps;
+
+	/* compute length of output */
+	for (i = 0; i < ndeps; i++)
+	{
+		JsonbValue	   *j;
+		JsonbContainer *elemobj,
+					   *attnumarr;
+		MVDependency   *d;
+		char		   *s;
+		int				a;
+		int				natts;
+
+		j = getIthJsonbValueFromContainer(cont, i);
+
+		if ((j == NULL)
+				|| (j->type != jbvBinary)
+				|| (!JsonContainerIsObject(j->val.binary.data)))
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("invalid statistics format, elements of stxdepndencies "
+					  "must be objects.")));
+
+		elemobj = j->val.binary.data;
+		attnumarr = key_lookup_array(elemobj, "attnums");
+
+		if (attnumarr == NULL)
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("invalid statistics format, elements of stxdependencies "
+					  "must contain an element called attnums which is an array.")));
+
+		natts = JsonContainerSize(attnumarr);
+		d = (MVDependency *) palloc0(offsetof(MVDependency, attributes)
+									 + (natts * sizeof(AttrNumber)));
+		dependencies->deps[i] = d;
+
+		d->nattributes = natts;
+
+		s = key_lookup_cstring(elemobj, "degree");
+		if (s != NULL)
+		{
+			d->degree = float8in_internal(s, NULL, "double", s, NULL);
+			pfree(s);
+		}
+		else
+			d->degree = 0;
+
+		for (a = 0; a < natts; a++)
+		{
+			JsonbValue *aj;
+			char	   *str;
+
+			aj = getIthJsonbValueFromContainer(attnumarr, a);
+
+			if ((aj == NULL) || (aj->type != jbvString))
+				ereport(ERROR,
+				  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				   errmsg("invalid statistics format, elements of attnums "
+						  "must be string representations of integers.")));
+
+			str = JsonbStringValueToCString(aj);
+			d->attributes[a] = pg_strtoint16(str);
+			pfree(str);
+			pfree(aj);
+		}
+
+		pfree(j);
+	}
+
+	return dependencies;
+}
diff --git a/src/backend/statistics/extended_stats.c b/src/backend/statistics/extended_stats.c
index 718826ecf1..69072485fa 100644
--- a/src/backend/statistics/extended_stats.c
+++ b/src/backend/statistics/extended_stats.c
@@ -19,6 +19,7 @@
 #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"
@@ -495,6 +496,38 @@ create_stat_ext_entry(HeapTuple htup)
 	return entry;
 }
 
+/*
+ * Return a list (of StatExtEntry) of statistics objects for the given relation.
+ */
+/* TODO needed????
+static StatExtEntry *
+fetch_statentry(Relation pg_statext, Oid stxid)
+{
+	SysScanDesc scan;
+	ScanKeyData skey;
+	HeapTuple	htup;
+	StatExtEntry *entry = NULL;
+
+	/-*
+	 * Prepare to scan pg_statistic_ext for entries having given oid.
+	 *-/
+	ScanKeyInit(&skey,
+				Anum_pg_statistic_ext_oid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(stxid));
+
+	scan = systable_beginscan(pg_statext, StatisticExtRelidIndexId, true,
+							  NULL, 1, &skey);
+
+	if (HeapTupleIsValid(htup = systable_getnext(scan)))
+		entry = create_stat_ext_entry(htup);
+
+	systable_endscan(scan);
+
+	return entry;
+}
+*/
+
 /*
  * Return a list (of StatExtEntry) of statistics objects for the given relation.
  */
@@ -2421,6 +2454,8 @@ serialize_expr_stats(AnlExprData *exprdata, int nexprs)
 								  false,
 								  typOid,
 								  CurrentMemoryContext);
+
+		heap_freetuple(stup);
 	}
 
 	table_close(sd, RowExclusiveLock);
@@ -2646,3 +2681,259 @@ make_build_data(Relation rel, StatExtEntry *stat, int numrows, HeapTuple *rows,
 
 	return result;
 }
+
+/*
+ * Generate VacAttrStats for a single pg_statistic_ext
+ */
+static VacAttrStats **
+examine_ext_stat_types(StatExtEntry *stxentry, Relation rel)
+{
+	TupleDesc		tupdesc = RelationGetDescr(rel);
+	Bitmapset	   *columns = stxentry->columns;
+	List		   *exprs = stxentry->exprs;
+	int				natts = bms_num_members(columns) + list_length(exprs);
+	int				i = 0;
+	int				m = -1;
+
+	VacAttrStats  **stats;
+	ListCell   *lc;
+
+	stats = (VacAttrStats **) palloc(natts * sizeof(VacAttrStats *));
+
+	/* lookup VacAttrStats info for the requested columns (same attnum) */
+	while ((m = bms_next_member(columns, m)) >= 0)
+	{
+		Form_pg_attribute	attform = TupleDescAttr(tupdesc, m - 1);
+
+		stats[i] = examine_rel_attribute(attform, rel, NULL);
+
+		/* ext expr stats remove the tupattnum */
+		stats[i]->tupattnum = InvalidAttrNumber;
+		i++;
+	}
+
+	/* also add info for expressions */
+	foreach(lc, exprs)
+	{
+		Node	   *expr = (Node *) lfirst(lc);
+
+		stats[i] = examine_attribute(expr);
+		i++;
+	}
+
+	return stats;
+}
+
+/*
+ * Generate expressions from imported data.
+ */
+static Datum
+import_expressions(Relation rel, JsonbContainer *cont,
+					VacAttrStats **expr_stats, int nexprs)
+{
+	int			i;
+	int			nelems;
+	Oid			typOid;
+	Relation	sd;
+
+	ArrayBuildState *astate = NULL;
+
+	/* skip if no stats to import */
+	if (cont == NULL)
+		return (Datum) 0;
+
+	nelems = JsonContainerSize(cont);
+
+	if (nelems == 0)
+		return (Datum) 0;
+
+	sd = table_open(StatisticRelationId, RowExclusiveLock);
+
+	/* lookup OID of composite type for pg_statistic */
+	typOid = get_rel_type_id(StatisticRelationId);
+	if (!OidIsValid(typOid))
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("relation \"%s\" does not have a composite type",
+						"pg_statistic")));
+
+	/*
+	 * The number of elements should not exceed the number of columns in the
+	 * extended statistics object. The elements should follow the same order
+	 * as they do on disk: regular attributes first, followed by expressions.
+	 * TODO make this a warning
+	 */
+	if (nelems > nexprs)
+	{
+		nelems = nexprs;
+	}
+	nelems = Min(nelems, nexprs);
+
+	for (i = 0; i < nelems; i++)
+	{
+		Datum		values[Natts_pg_statistic] = { 0 };
+		bool		nulls[Natts_pg_statistic] = { false };
+		bool		replaces[Natts_pg_statistic] = { false };
+		HeapTuple	stup;
+
+		JsonbValue   *j = getIthJsonbValueFromContainer(cont, i);
+		VacAttrStats *stat = expr_stats[i];
+
+		JsonbContainer *exprobj;
+
+		if ((j == NULL)
+				|| (j->type != jbvBinary)
+				|| (!JsonContainerIsObject(j->val.binary.data)))
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("invalid statistics format, elements of stxdexprs "
+					  "must be objects.")));
+
+		exprobj = j->val.binary.data;
+
+		import_attribute(InvalidOid, stat, exprobj, false, values, nulls, replaces);
+
+		stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
+
+		astate = accumArrayResult(astate,
+								  heap_copy_tuple_as_datum(stup, RelationGetDescr(sd)),
+								  false,
+								  typOid,
+								  CurrentMemoryContext);
+		pfree(j);
+	}
+
+	table_close(sd, RowExclusiveLock);
+
+	return makeArrayResult(astate, CurrentMemoryContext);
+}
+
+/*
+ * import_pg_ext_stats
+ *
+ * Import stats for one aspect (inherited / regular) of an Extended Statistics
+ * object.
+ *
+ * The JSON container should look like this:
+ * {
+ *     "stxkinds": array of single characters (up to 3?),
+ *     "stxdndistinct": [ {ndistinct}, ... ],
+ *     "stxdndependencies": [ {dependency}, ... ]
+ *     "stxdmcv": [ {mcv}, ... ]
+ *     "stxdexprs" : [ {pg_statistic}, ... ]
+ * }
+ */
+static void
+import_pg_statistic_ext_data(StatExtEntry *stxentry, Relation rel,
+							 bool inh, JsonbContainer *cont,
+							 VacAttrStats **stats)
+{
+	int				ncols;
+	int				nexprs;
+	int				natts;
+	VacAttrStats  **expr_stats;
+
+	JsonbContainer *arraycont;
+	MCVList		   *mcvlist;
+	MVDependencies *dependencies;
+	MVNDistinct	   *ndistinct;
+	Datum			exprs;
+
+	/* skip if no stats to import */
+	if (cont == NULL)
+		return;
+
+	ncols = bms_num_members(stxentry->columns);
+	nexprs = list_length(stxentry->exprs);
+	natts = ncols + nexprs;
+	expr_stats = &stats[ncols];
+
+	arraycont = key_lookup_array(cont, "stxdndistinct");
+	ndistinct = statext_ndistinct_import(arraycont);
+
+	arraycont = key_lookup_array(cont, "stxdndependencies");
+	dependencies = statext_dependencies_import(arraycont);
+
+	arraycont = key_lookup_array(cont, "stxdmcv");
+	mcvlist = statext_mcv_import(arraycont, stats, natts);
+
+	arraycont = key_lookup_array(cont, "stxdexprs");
+	exprs = import_expressions(rel, arraycont, expr_stats, nexprs);
+
+	statext_store(stxentry->statOid, inh, ndistinct, dependencies, mcvlist,
+				  exprs, stats);
+}
+
+/*
+ * Import JSON-serialized stats to an Extended Statistics object.
+ */
+Datum
+pg_import_ext_stats(PG_FUNCTION_ARGS)
+{
+	Oid				stxid;
+	int32			stats_version_num;
+	Jsonb		   *jb;
+	JsonbContainer *cont;
+	Relation		rel;
+	HeapTuple		etup;
+	Relation		sd;
+	StatExtEntry   *stxentry;
+	VacAttrStats  **stats;
+
+	Form_pg_statistic_ext stxform;
+
+	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))
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("server_version_number cannot be NULL")));
+	stats_version_num = PG_GETARG_INT32(1);
+
+	if (stats_version_num < 100000)
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("invalid statistics version: %d is earlier than earliest supported version",
+				  stats_version_num)));
+
+	if (PG_ARGISNULL(2))
+		PG_RETURN_BOOL(false);
+
+	jb = PG_GETARG_JSONB_P(2);
+	if (!JB_ROOT_IS_OBJECT(jb))
+		ereport(ERROR,
+		  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+		   errmsg("extended_stats must be jsonb object at root")));
+
+	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);
+
+	stxentry = create_stat_ext_entry(etup);
+
+	stats = examine_ext_stat_types(stxentry, rel);
+
+	cont = key_lookup_object(&jb->root, "regular");
+	import_pg_statistic_ext_data(stxentry, rel, false, cont, stats);
+
+	if (rel->rd_rel->relhassubclass)
+	{
+		cont = key_lookup_object(&jb->root, "inherited");
+		import_pg_statistic_ext_data(stxentry, rel, true, cont, stats);
+	}
+
+	relation_close(rel, NoLock);
+	table_close(sd, RowExclusiveLock);
+
+	PG_RETURN_BOOL(true);
+}
diff --git a/src/backend/statistics/mcv.c b/src/backend/statistics/mcv.c
index 03b9f04bb5..f1de17847a 100644
--- a/src/backend/statistics/mcv.c
+++ b/src/backend/statistics/mcv.c
@@ -29,6 +29,7 @@
 #include "utils/array.h"
 #include "utils/builtins.h"
 #include "utils/bytea.h"
+#include "utils/float.h"
 #include "utils/fmgroids.h"
 #include "utils/fmgrprotos.h"
 #include "utils/lsyscache.h"
@@ -679,7 +680,6 @@ statext_mcv_serialize(MCVList *mcvlist, VacAttrStats **stats)
 			/* skip NULL values - we don't need to deduplicate those */
 			if (mcvlist->items[i].isnull[dim])
 				continue;
-
 			/* append the value at the end */
 			values[dim][counts[dim]] = mcvlist->items[i].values[dim];
 			counts[dim] += 1;
@@ -2177,3 +2177,218 @@ mcv_clause_selectivity_or(PlannerInfo *root, StatisticExtInfo *stat,
 
 	return s;
 }
+
+/*
+ * statext_mcv_import
+ *      like statext_mcv_build, but import from JSON.
+ *
+ * import format:
+ * [
+ *   {
+ *     "index": intstr,
+ *     "values": '{va1ue, ...}',
+ *     "nulls": '{t,f,...}',
+ *     "frequency": floatstr,
+ *     "base_frequency": floatstr
+ *   }
+ * ]
+ *
+ */
+MCVList *
+statext_mcv_import(JsonbContainer *cont, VacAttrStats **stats, int ndims)
+{
+	int			nitems;
+	int			i;
+	MCVList	   *mcvlist;
+	Oid		    ioparams[STATS_MAX_DIMENSIONS];
+	FmgrInfo	finfos[STATS_MAX_DIMENSIONS];
+
+	if (cont != NULL)
+		nitems = JsonContainerSize(cont);
+	else
+		nitems = 0;
+
+	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 = stats[i]->attrtypid;
+		Oid		infunc;
+
+		mcvlist->types[i] = typid;
+		getTypeInputInfo(typid, &infunc, &ioparams[i]);
+		fmgr_info(infunc, &finfos[i]);
+	}
+
+	for (i = 0; i < nitems; i++)
+	{
+		JsonbValue	   *j;
+		JsonbContainer *itemobj,
+					   *valuesarr,
+					   *nullsarr;
+		int				numvalues,
+						numnulls;
+		int				k;
+		MCVItem		   *item = &mcvlist->items[i];
+		char		   *s;
+
+		item->values = (Datum *) palloc0(sizeof(Datum) * ndims);
+		item->isnull = (bool *) palloc0(sizeof(bool) * ndims);
+
+		j = getIthJsonbValueFromContainer(cont, i);
+
+		if ((j == NULL)
+				|| (j->type != jbvBinary)
+				|| (!JsonContainerIsObject(j->val.binary.data)))
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("invalid statistics format, elements of stxdmcv "
+					  "must be objects.")));
+
+		itemobj = j->val.binary.data;
+
+		s = key_lookup_cstring(itemobj, "frequency");
+		if (s != NULL)
+		{
+			item->frequency = float8in_internal(s, NULL, "double", s, NULL);
+			pfree(s);
+		}
+		else
+			item->frequency = 0.0;
+
+		s = key_lookup_cstring(itemobj, "base_frequency");
+		if (s != NULL)
+		{
+			item->base_frequency = float8in_internal(s, NULL, "double", s, NULL);
+			pfree(s);
+		}
+		else
+			item->base_frequency = 0.0;
+
+		/*
+		 * Import the nulls array first, because that tells us which elements
+		 * of the values array we can skip.
+		 */
+		nullsarr = key_lookup_array(itemobj, "nulls");
+
+		if (nullsarr == NULL)
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("invalid statistics format, elements of stxdmcv "
+					  "must contain an element called nulls which is an array.")));
+
+		numnulls = JsonContainerSize(nullsarr);
+
+		/* having more nulls than dimensions is concerning. */
+		if (numnulls > ndims)
+		{
+			ereport(WARNING,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("statistics import has %d mcv dimensions, "
+					  "but the expects %d. Skipping excess dimensions.",
+						numnulls, ndims)));
+			numnulls = ndims;
+		}
+
+		for (k = 0; k < numnulls; k++)
+		{
+			JsonbValue  *nj = getIthJsonbValueFromContainer(nullsarr, k);
+
+			if ((nj == NULL) || (nj->type != jbvBool))
+				ereport(ERROR,
+				  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				   errmsg("invalid statistics format, elements of nulls "
+						  "must be boolens.")));
+
+			item->isnull[k] = nj->val.boolean;
+
+			pfree(nj);
+		}
+
+		/* Any remaining slots are marked null */
+		for (k = numnulls; k < ndims; k++)
+			item->isnull[k] = true;
+
+		valuesarr = key_lookup_array(itemobj, "values");
+
+		if (valuesarr == NULL)
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("invalid statistics format, elements of stxdmcv "
+					  "must contain an element called values which is an array.")));
+
+		numvalues = JsonContainerSize(valuesarr);
+
+		/* having more values than dimensions is concerning. */
+		if (numvalues > ndims)
+		{
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("statistics import has %d mcv dimensions, "
+					  "but the expects %d. Skipping excess dimensions.",
+						numvalues, ndims)));
+			numvalues = ndims;
+		}
+
+		for (k = 0; k < numvalues; k++)
+		{
+			JsonbValue *vj;
+			bool		import_error = true;
+
+			/* if the element was null flagged, don't bother */
+			if (item->isnull[k])
+			{
+				item->values[k] = (Datum) 0;
+				continue;
+			}
+
+			vj = getIthJsonbValueFromContainer(valuesarr, k);
+
+			if (vj != NULL)
+			{
+				if (vj->type == jbvString)
+				{
+					char   *str = JsonbStringValueToCString(vj);
+
+					item->values[k] = InputFunctionCall(&finfos[k],
+														str,
+														ioparams[k],
+														stats[k]->attrtypmod);
+
+					import_error = false;
+					pfree(str);
+				}
+				else if (vj->type == jbvNull)
+				{
+					item->values[k] = (Datum) 0;
+					item->isnull[k] = true; /* mark just in case */
+					import_error = false;
+				}
+				pfree(vj);
+			}
+
+			if (import_error)
+				ereport(ERROR,
+				  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				   errmsg("invalid statistics format, elements of values "
+						  "must be strings or null.")));
+
+		}
+
+		/* Any remaining slots are marked null */
+		for (k = numvalues; k < ndims; k++)
+		{
+			item->values[k] = (Datum) 0;
+			item->isnull[k] = true;
+		}
+	}
+
+	return mcvlist;
+}
diff --git a/src/backend/statistics/mvdistinct.c b/src/backend/statistics/mvdistinct.c
index 6d25c14644..e870ae02a4 100644
--- a/src/backend/statistics/mvdistinct.c
+++ b/src/backend/statistics/mvdistinct.c
@@ -31,6 +31,8 @@
 #include "lib/stringinfo.h"
 #include "statistics/extended_stats_internal.h"
 #include "statistics/statistics.h"
+#include "utils/builtins.h"
+#include "utils/float.h"
 #include "utils/fmgrprotos.h"
 #include "utils/lsyscache.h"
 #include "utils/syscache.h"
@@ -698,3 +700,102 @@ generate_combinations(CombinationGenerator *state)
 
 	pfree(current);
 }
+
+
+/*
+ * statext_ndistinct_import
+ *
+ * Like statext_ndistinct_build, but import the data
+ * from a JSON container.
+ *
+ * import format:
+ * [
+ *   {
+ *     "attnums": [ intstr, ... ],
+ *     "ndistinct": floatstr
+ *   }
+ * ]
+ *
+ */
+MVNDistinct *
+statext_ndistinct_import(JsonbContainer *cont)
+{
+	MVNDistinct	   *result;
+	int				nitems;
+	int				i;
+
+	if (cont == NULL)
+		return NULL;
+
+	nitems = JsonContainerSize(cont);
+
+	if (nitems == 0)
+		return NULL;
+
+	result = palloc(offsetof(MVNDistinct, items) +
+					(nitems * sizeof(MVNDistinctItem)));
+	result->magic = STATS_NDISTINCT_MAGIC;
+	result->type = STATS_NDISTINCT_TYPE_BASIC;
+	result->nitems = nitems;
+
+	for (i = 0; i < nitems; i++)
+	{
+		JsonbValue	   *j;
+		JsonbContainer *elemobj,
+					   *attnumarr;
+		int				a;
+		int				natts;
+		char		   *s;
+
+		MVNDistinctItem *item = &result->items[i];
+
+		j = getIthJsonbValueFromContainer(cont, i);
+
+		if ((j == NULL)
+				|| (j->type != jbvBinary)
+				|| (!JsonContainerIsObject(j->val.binary.data)))
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("invalid statistics format, elements of stxdndistinct "
+					  "must be objects.")));
+
+		elemobj = j->val.binary.data;
+
+		s = key_lookup_cstring(elemobj, "ndistinct");
+		item->ndistinct = float8in_internal(s, NULL, "double", s, NULL);
+		pfree(s);
+
+		attnumarr = key_lookup_array(elemobj, "attnums");
+
+		if (attnumarr == NULL)
+			ereport(ERROR,
+			  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+			   errmsg("invalid statistics format, elements of stxdndistinct "
+					  "must contain an element called attnums which is an array.")));
+
+		natts = JsonContainerSize(attnumarr);
+		item->nattributes = natts;
+		item->attributes = palloc(sizeof(AttrNumber) * natts);
+
+		for (a = 0; a < natts; a++)
+		{
+			JsonbValue *aj;
+
+			aj = getIthJsonbValueFromContainer(attnumarr, a);
+
+			if ((aj == NULL) || (aj->type != jbvString))
+				ereport(ERROR,
+				  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				   errmsg("invalid statistics format, elements of attnums "
+						  "must be string representations of integers.")));
+			s = JsonbStringValueToCString(aj);
+			item->attributes[a] = pg_strtoint16(s);
+			pfree(s);
+			pfree(aj);
+		}
+
+		pfree(j);
+	}
+
+	return result;
+}
diff --git a/src/backend/statistics/statistics.c b/src/backend/statistics/statistics.c
index 968ccfaaf2..e54f1c9162 100644
--- a/src/backend/statistics/statistics.c
+++ b/src/backend/statistics/statistics.c
@@ -364,7 +364,7 @@ void import_stanumbers(const VacAttrStats *stat, JsonbContainer *cont,
 		if (numnumbers > STATISTIC_NUM_SLOTS)
 			ereport(ERROR,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("invalid format: number of stanumbers %d is greater than available slots %d",
+				errmsg("invalid format: number of stanumbers arrays (%d) is greater than available slots %d",
 						numnumbers, STATISTIC_NUM_SLOTS)));
 
 		fmgr_info(F_ARRAY_IN, &finfo);
@@ -474,8 +474,8 @@ void import_stavalues(const VacAttrStats *stat, JsonbContainer *cont,
 						typoid = typentry->typelem;
 				}
 				valvalues[k] = FunctionCall3(&finfo, CStringGetDatum(s),
-								 ObjectIdGetDatum(typoid),
-								 Int32GetDatum(typmod));
+											 ObjectIdGetDatum(typoid),
+											 Int32GetDatum(typmod));
 				valreplaces[k] = true;
 				pfree(s);
 				pfree(j);
@@ -664,7 +664,7 @@ examine_rel_attribute(Form_pg_attribute attr, Relation onerel, Node *index_expr)
 	if (!HeapTupleIsValid(typtuple))
 		elog(ERROR, "cache lookup failed for type %u", stats->attrtypid);
 	stats->attrtype = (Form_pg_type) GETSTRUCT(typtuple);
-	stats->anl_context = CurrentMemoryContext;
+	stats->anl_context = NULL; /*DEBUG CurrentMemoryContext; */
 	stats->tupattnum = attr->attnum;
 
 	/*
diff --git a/src/test/regress/expected/stats_export_import.out b/src/test/regress/expected/stats_export_import.out
index 2490472198..7c14f292e4 100644
--- a/src/test/regress/expected/stats_export_import.out
+++ b/src/test/regress/expected/stats_export_import.out
@@ -19,6 +19,7 @@ SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import_complex_type,
 UNION ALL
 SELECT 4, 'four', NULL, NULL;
 CREATE INDEX is_odd ON stats_import_test(((comp).a % 2 = 1));
+CREATE STATISTICS evens_test ON name, ((comp).a % 2 = 0) FROM stats_import_test;
 ANALYZE stats_import_test;
 -- capture snapshot of source stats
 CREATE TABLE stats_export AS
@@ -57,6 +58,8 @@ WHERE oid = 'stats_import_test'::regclass;
 CREATE TABLE stats_import_clone ( LIKE stats_import_test );
 -- create an index just like is_odd
 CREATE INDEX is_odd2 ON stats_import_clone(((comp).a % 2 = 0));
+-- create a statistics object like evens_test
+CREATE STATISTICS evens_clone ON name, ((comp).a % 2 = 0) FROM stats_import_clone;
 -- copy table stats to clone table
 SELECT pg_import_rel_stats(c.oid, e.server_version_num,
                             e.n_tuples, e.n_pages, e.stats)
@@ -131,6 +134,23 @@ WHERE s.starelid = 'is_odd2'::regclass;
 -----------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----
 (0 rows)
 
+-- copy extended stats to clone table
+SELECT pg_import_ext_stats(
+        (
+            SELECT e.oid as ext_clone_oid
+            FROM pg_statistic_ext AS e
+            WHERE e.stxname = 'evens_clone'
+        ),
+        e.server_version_num, e.stats)
+FROM pg_catalog.pg_statistic_ext_export AS e
+WHERE e.schemaname = 'public'
+AND e.tablename = 'stats_import_test'
+AND e.ext_stats_name = 'evens_test';
+ pg_import_ext_stats 
+---------------------
+ t
+(1 row)
+
 DROP TABLE stats_export;
 DROP TABLE stats_import_clone;
 DROP TABLE stats_import_test;
diff --git a/src/test/regress/sql/stats_export_import.sql b/src/test/regress/sql/stats_export_import.sql
index e97b9d1064..3bc8e3d3a3 100644
--- a/src/test/regress/sql/stats_export_import.sql
+++ b/src/test/regress/sql/stats_export_import.sql
@@ -23,6 +23,8 @@ SELECT 4, 'four', NULL, NULL;
 
 CREATE INDEX is_odd ON stats_import_test(((comp).a % 2 = 1));
 
+CREATE STATISTICS evens_test ON name, ((comp).a % 2 = 0) FROM stats_import_test;
+
 ANALYZE stats_import_test;
 
 -- capture snapshot of source stats
@@ -53,6 +55,9 @@ CREATE TABLE stats_import_clone ( LIKE stats_import_test );
 -- create an index just like is_odd
 CREATE INDEX is_odd2 ON stats_import_clone(((comp).a % 2 = 0));
 
+-- create a statistics object like evens_test
+CREATE STATISTICS evens_clone ON name, ((comp).a % 2 = 0) FROM stats_import_clone;
+
 -- copy table stats to clone table
 SELECT pg_import_rel_stats(c.oid, e.server_version_num,
                             e.n_tuples, e.n_pages, e.stats)
@@ -113,6 +118,19 @@ SELECT staattnum, stainherit, stanullfrac, stawidth, stadistinct,
 FROM pg_statistic AS s
 WHERE s.starelid = 'is_odd2'::regclass;
 
+-- copy extended stats to clone table
+SELECT pg_import_ext_stats(
+        (
+            SELECT e.oid as ext_clone_oid
+            FROM pg_statistic_ext AS e
+            WHERE e.stxname = 'evens_clone'
+        ),
+        e.server_version_num, e.stats)
+FROM pg_catalog.pg_statistic_ext_export AS e
+WHERE e.schemaname = 'public'
+AND e.tablename = 'stats_import_test'
+AND e.ext_stats_name = 'evens_test';
+
 DROP TABLE stats_export;
 DROP TABLE stats_import_clone;
 DROP TABLE stats_import_test;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index ae3d1073e3..d9029fd29d 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -28191,6 +28191,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para></entry>
       </row>
      </tbody>
+      <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 stats object</parameter> <type>oid</type>, <parameter>server_version_num</parameter> <type>integer</type>, <parameter>extended_stats</parameter> <type>jsonb</type> )
+        <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
+        <program>pg_upgrade</program> and <program>pg_restore</program> to
+        convey the statistics from the old system version into the new one.
+       </para></entry>
+      </row>
+     </tbody>
     </tgroup>
    </table>
 
-- 
2.43.0

v3-0009-Enable-pg_export_stats-pg_import_stats-to-use-ext.patchtext/x-patch; charset=US-ASCII; name=v3-0009-Enable-pg_export_stats-pg_import_stats-to-use-ext.patchDownload
From aeffb52e6c744f624ea32b3ef587f5e9d0b5b85b Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 13 Dec 2023 05:21:00 -0500
Subject: [PATCH v3 9/9] Enable pg_export_stats, pg_import_stats to use
 extended statistics.

Both programs still use a single data stream. As a result, the columns
COPY-ed are a superset of both export tables, and on import the data
must first be loaded into a superset temporary table before being
separated into rel-specific and extended-specific tables.

While relation stats format is stable back to v10, the formats of
pg_statistic_ext and pg_statistic_ext_data change every version or two.
Extraction queries are provided back to v10, but currently only tested
back to v15. That's probably ok given that these programs primarily
serve as a reference and their functionality will most likely be moved
to pg_upgrade and pg_dump+pg_restore.
---
 src/bin/scripts/pg_export_stats.c | 448 +++++++++++++++++++++++++++++-
 src/bin/scripts/pg_import_stats.c | 289 +++++++++++++++----
 2 files changed, 672 insertions(+), 65 deletions(-)

diff --git a/src/bin/scripts/pg_export_stats.c b/src/bin/scripts/pg_export_stats.c
index abb3659e20..4aaafc729e 100644
--- a/src/bin/scripts/pg_export_stats.c
+++ b/src/bin/scripts/pg_export_stats.c
@@ -22,18 +22,20 @@
 static void help(const char *progname);
 
 /* view definition introduced in 17 */
-const char *export_query_v17 =
-	"SELECT schemaname, relname, server_version_num, n_tuples, "
-	"n_pages, stats FROM pg_statistic_export ";
+const char *export_rel_query_v17 =
+	"SELECT schemaname, relname, NULL::text AS ext_stats_name, "
+	"server_version_num, n_tuples, n_pages, stats "
+	"FROM pg_statistic_export ";
 
 /*
- * Versions 10-16 have the same stats layout, but lack the view definition,
- * so extracting the view definition ad using it as-is will work.
+ * Versions 10-16 have the same rel stats layout, but lack the view
+ * definition, so extracting the view definition ad using it as-is will work.
  */
-const char *export_query_v10 =
+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, "
 	"    r.reltuples::float4 AS n_tuples, "
 	"    r.relpages::integer AS n_pages, "
@@ -109,6 +111,412 @@ const char *export_query_v10 =
 	"WHERE relkind IN ('r', 'm', 'f', 'p', 'i') "
 	"AND n.nspname NOT IN ('pg_catalog', 'pg_toast', 'information_schema') ";
 
+/* view definition introduced in 17 */
+const char *export_ext_query_v17 =
+	"SELECT schemaname, relname, ext_stats_name, server_version_num, "
+	"NULL::float4 AS n_tuples, NULL::integer AS n_pages, stats "
+	"FROM pg_statistic_ext_export ";
+
+/* v15-v16 have the same extended stats layout, but lack the view definition */
+const char *export_ext_query_v15 =
+	"SELECT "
+	"    n.nspname AS schemaname, "
+	"    r.relname AS tablename, "
+	"    e.stxname AS ext_stats_name, "
+	"    (current_setting('server_version_num'::text))::integer AS server_version_num, "
+	"    NULL::float4 AS n_tuples, "
+	"    NULL::integer AS n_pages, "
+	"    jsonb_object_agg( "
+	"        CASE sd.stxdinherit "
+	"            WHEN true THEN 'inherited' "
+	"            ELSE 'regular' "
+	"        END, "
+	"        jsonb_build_object( "
+	"            'stxkinds', "
+	"            to_jsonb(e.stxkind), "
+	"            'stxdndistinct', "
+	"            ( "
+	"                SELECT "
+	"                    jsonb_agg( "
+	"                        jsonb_build_object( "
+	"                            'attnums', "
+	"                            string_to_array(nd.attnums, ', '::text), "
+	"                            'ndistinct', "
+	"                            nd.ndistinct "
+	"                            ) "
+	"                        ORDER BY nd.ord "
+	"                    ) "
+	"                FROM json_each_text(sd.stxdndistinct::text::json) "
+	"                    WITH ORDINALITY AS nd(attnums, ndistinct, ord) "
+	"                WHERE sd.stxdndistinct IS NOT NULL "
+	"            ), "
+	"            'stxdndependencies', "
+	"            ( "
+	"                SELECT "
+	"                    jsonb_agg( "
+	"                        jsonb_build_object( "
+	"                            'attnums', "
+	"                            string_to_array( "
+	"                                replace(dep.attrs, ' => ', ', '), ', ' "
+	"                            ), "
+	"                            'degree', "
+	"                            dep.degree "
+	"                        ) "
+	"                        ORDER BY dep.ord "
+	"                    ) "
+	"                FROM json_each_text(sd.stxddependencies::text::json) "
+	"                    WITH ORDINALITY AS dep(attrs, degree, ord) "
+	"                WHERE sd.stxddependencies IS NOT NULL "
+	"            ), "
+	"            'stxdmcv', "
+	"            ( "
+	"                SELECT "
+	"                    jsonb_agg( "
+	"                        jsonb_build_object( "
+	"                            'index', "
+	"                            mcvl.index::text, "
+	"                            'frequency', "
+	"                            mcvl.frequency::text, "
+	"                            'base_frequency', "
+	"                            mcvl.base_frequency::text, "
+	"                            'values', "
+	"                            mcvl.values, "
+	"                            'nulls', "
+	"                            mcvl.nulls "
+	"                        ) "
+	"                    ) "
+	"                FROM pg_mcv_list_items(sd.stxdmcv) AS mcvl "
+	"                WHERE sd.stxdmcv IS NOT NULL "
+	"            ), "
+	"            'stxdexprs', "
+	"            ( "
+	"                SELECT "
+	"                    jsonb_agg( "
+	"                        jsonb_build_object( "
+	"                            'stanullfrac', "
+	"                            s.stanullfrac::text, "
+	"                            'stawidth', "
+	"                            s.stawidth::text, "
+	"                            'stadistinct', "
+	"                            s.stadistinct::text, "
+	"                            'stakinds', "
+	"                            ( "
+	"                                SELECT "
+	"                                    jsonb_agg( "
+	"                                        CASE kind.kind "
+	"                                            WHEN 0 THEN 'TRIVIAL' "
+	"                                            WHEN 1 THEN 'MCV' "
+	"                                            WHEN 2 THEN 'HISTOGRAM' "
+	"                                            WHEN 3 THEN 'CORRELATION' "
+	"                                            WHEN 4 THEN 'MCELEM' "
+	"                                            WHEN 5 THEN 'DECHIST' "
+	"                                            WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM' "
+	"                                            WHEN 7 THEN 'BOUNDS_HISTOGRAM' "
+	"                                            ELSE NULL "
+	"                                        END "
+	"                                        ORDER BY kind.ord "
+	"                                    ) "
+	"                                FROM unnest(ARRAY[s.stakind1, s.stakind2, "
+	"                                                  s.stakind3, s.stakind4, "
+	"                                                  s.stakind5]) "
+	"                                    WITH ORDINALITY kind(kind, ord) "
+	"                            ), "
+	"                            'stanumbers', "
+	"                            jsonb_build_array( "
+	"                                s.stanumbers1::text, "
+	"                                s.stanumbers2::text, "
+	"                                s.stanumbers3::text, "
+	"                                s.stanumbers4::text, "
+	"                                s.stanumbers5::text "
+	"                            ), "
+	"                            'stavalues', "
+	"                            jsonb_build_array( "
+	"                                s.stavalues1::text, "
+	"                                s.stavalues2::text, "
+	"                                s.stavalues3::text, "
+	"                                s.stavalues4::text, "
+	"                                s.stavalues5::text) "
+	"                            ) "
+	"                            ORDER BY s.ordinality "
+	"                        ) "
+	"                FROM unnest(sd.stxdexpr) WITH ORDINALITY AS s "
+	"                WHERE sd.stxdexpr IS NOT NULL "
+	"            ) "
+	"        ) "
+	"    ) AS stats "
+	"FROM pg_class r "
+	"JOIN pg_namespace n ON n.oid = r.relnamespace "
+	"JOIN pg_statistic_ext e ON e.stxrelid = r.oid "
+	"JOIN pg_statistic_ext_data sd ON sd.stxoid = e.oid "
+	"GROUP BY schemaname, tablename, ext_stats_name, server_version_num ";
+
+/* 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 tablename, "
+	"    e.stxname AS ext_stats_name, "
+	"    (current_setting('server_version_num'::text))::integer AS server_version_num, "
+	"    NULL::float4 AS n_tuples, "
+	"    NULL::integer AS n_pages, "
+	"    jsonb_object_agg( "
+	"        'regular', "
+	"        jsonb_build_object( "
+	"            'stxkinds', "
+	"            to_jsonb(e.stxkind), "
+	"            'stxdndistinct', "
+	"            ( "
+	"                SELECT "
+	"                    jsonb_agg( "
+	"                        jsonb_build_object( "
+	"                            'attnums', "
+	"                            string_to_array(nd.attnums, ', '::text), "
+	"                            'ndistinct', "
+	"                            nd.ndistinct "
+	"                            ) "
+	"                        ORDER BY nd.ord "
+	"                    ) "
+	"                FROM json_each_text(sd.stxdndistinct::text::json) "
+	"                    WITH ORDINALITY AS nd(attnums, ndistinct, ord) "
+	"                WHERE sd.stxdndistinct IS NOT NULL "
+	"            ), "
+	"            'stxdndependencies', "
+	"            ( "
+	"                SELECT "
+	"                    jsonb_agg( "
+	"                        jsonb_build_object( "
+	"                            'attnums', "
+	"                            string_to_array( "
+	"                                replace(dep.attrs, ' => ', ', '), ', ' "
+	"                            ), "
+	"                            'degree', "
+	"                            dep.degree "
+	"                        ) "
+	"                        ORDER BY dep.ord "
+	"                    ) "
+	"                FROM json_each_text(sd.stxddependencies::text::json) "
+	"                    WITH ORDINALITY AS dep(attrs, degree, ord) "
+	"                WHERE sd.stxddependencies IS NOT NULL "
+	"            ), "
+	"            'stxdmcv', "
+	"            ( "
+	"                SELECT "
+	"                    jsonb_agg( "
+	"                        jsonb_build_object( "
+	"                            'index', "
+	"                            mcvl.index::text, "
+	"                            'frequency', "
+	"                            mcvl.frequency::text, "
+	"                            'base_frequency', "
+	"                            mcvl.base_frequency::text, "
+	"                            'values', "
+	"                            mcvl.values, "
+	"                            'nulls', "
+	"                            mcvl.nulls "
+	"                        ) "
+	"                    ) "
+	"                FROM pg_mcv_list_items(sd.stxdmcv) AS mcvl "
+	"                WHERE sd.stxdmcv IS NOT NULL "
+	"            ), "
+	"            'stxdexprs', "
+	"            ( "
+	"                SELECT "
+	"                    jsonb_agg( "
+	"                        jsonb_build_object( "
+	"                            'stanullfrac', "
+	"                            s.stanullfrac::text, "
+	"                            'stawidth', "
+	"                            s.stawidth::text, "
+	"                            'stadistinct', "
+	"                            s.stadistinct::text, "
+	"                            'stakinds', "
+	"                            ( "
+	"                                SELECT "
+	"                                    jsonb_agg( "
+	"                                        CASE kind.kind "
+	"                                            WHEN 0 THEN 'TRIVIAL' "
+	"                                            WHEN 1 THEN 'MCV' "
+	"                                            WHEN 2 THEN 'HISTOGRAM' "
+	"                                            WHEN 3 THEN 'CORRELATION' "
+	"                                            WHEN 4 THEN 'MCELEM' "
+	"                                            WHEN 5 THEN 'DECHIST' "
+	"                                            WHEN 6 THEN 'RANGE_LENGTH_HISTOGRAM' "
+	"                                            WHEN 7 THEN 'BOUNDS_HISTOGRAM' "
+	"                                            ELSE NULL "
+	"                                        END "
+	"                                        ORDER BY kind.ord "
+	"                                    ) "
+	"                                FROM unnest(ARRAY[s.stakind1, s.stakind2, "
+	"                                                  s.stakind3, s.stakind4, "
+	"                                                  s.stakind5]) "
+	"                                    WITH ORDINALITY kind(kind, ord) "
+	"                            ), "
+	"                            'stanumbers', "
+	"                            jsonb_build_array( "
+	"                                s.stanumbers1::text, "
+	"                                s.stanumbers2::text, "
+	"                                s.stanumbers3::text, "
+	"                                s.stanumbers4::text, "
+	"                                s.stanumbers5::text "
+	"                            ), "
+	"                            'stavalues', "
+	"                            jsonb_build_array( "
+	"                                s.stavalues1::text, "
+	"                                s.stavalues2::text, "
+	"                                s.stavalues3::text, "
+	"                                s.stavalues4::text, "
+	"                                s.stavalues5::text) "
+	"                            ) "
+	"                            ORDER BY s.ordinality "
+	"                        ) "
+	"                FROM unnest(sd.stxdexpr) WITH ORDINALITY AS s "
+	"                WHERE sd.stxdexpr IS NOT NULL "
+	"            ) "
+	"        ) "
+	"    ) AS stats "
+	"FROM pg_class r "
+	"JOIN pg_namespace n ON n.oid = r.relnamespace "
+	"JOIN pg_statistic_ext e ON e.stxrelid = r.oid "
+	"JOIN pg_statistic_ext_data sd ON sd.stxoid = e.oid "
+	"GROUP BY schemaname, tablename, ext_stats_name, server_version_num ";
+
+/* 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 tablename, "
+	"    e.stxname AS ext_stats_name, "
+	"    (current_setting('server_version_num'::text))::integer AS server_version_num, "
+	"    NULL::float4 AS n_tuples, "
+	"    NULL::integer AS n_pages, "
+	"    jsonb_object_agg( "
+	"        'regular', "
+	"        jsonb_build_object( "
+	"            'stxkinds', "
+	"            to_jsonb(e.stxkind), "
+	"            'stxdndistinct', "
+	"            ( "
+	"                SELECT "
+	"                    jsonb_agg( "
+	"                        jsonb_build_object( "
+	"                            'attnums', "
+	"                            string_to_array(nd.attnums, ', '::text), "
+	"                            'ndistinct', "
+	"                            nd.ndistinct "
+	"                            ) "
+	"                        ORDER BY nd.ord "
+	"                    ) "
+	"                FROM json_each_text(sd.stxdndistinct::text::json) "
+	"                    WITH ORDINALITY AS nd(attnums, ndistinct, ord) "
+	"                WHERE sd.stxdndistinct IS NOT NULL "
+	"            ), "
+	"            'stxdndependencies', "
+	"            ( "
+	"                SELECT "
+	"                    jsonb_agg( "
+	"                        jsonb_build_object( "
+	"                            'attnums', "
+	"                            string_to_array( "
+	"                                replace(dep.attrs, ' => ', ', '), ', ' "
+	"                            ), "
+	"                            'degree', "
+	"                            dep.degree "
+	"                        ) "
+	"                        ORDER BY dep.ord "
+	"                    ) "
+	"                FROM json_each_text(sd.stxddependencies::text::json) "
+	"                    WITH ORDINALITY AS dep(attrs, degree, ord) "
+	"                WHERE sd.stxddependencies IS NOT NULL "
+	"            ), "
+	"            'stxdmcv', "
+	"            ( "
+	"                SELECT "
+	"                    jsonb_agg( "
+	"                        jsonb_build_object( "
+	"                            'index', "
+	"                            mcvl.index::text, "
+	"                            'frequency', "
+	"                            mcvl.frequency::text, "
+	"                            'base_frequency', "
+	"                            mcvl.base_frequency::text, "
+	"                            'values', "
+	"                            mcvl.values, "
+	"                            'nulls', "
+	"                            mcvl.nulls "
+	"                        ) "
+	"                    ) "
+	"                FROM pg_mcv_list_items(sd.stxdmcv) AS mcvl "
+	"                WHERE sd.stxdmcv IS NOT NULL "
+	"            ) "
+	"        ) "
+	"    ) AS stats "
+	"FROM pg_class r "
+	"JOIN pg_namespace n ON n.oid = r.relnamespace "
+	"JOIN pg_statistic_ext e ON e.stxrelid = r.oid "
+	"JOIN pg_statistic_ext_data sd ON sd.stxoid = e.oid "
+	"GROUP BY schemaname, tablename, ext_stats_name, server_version_num ";
+
+/*
+ * 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 tablename, "
+	"    e.stxname AS ext_stats_name, "
+	"    (current_setting('server_version_num'::text))::integer AS server_version_num, "
+	"    NULL::float4 AS n_tuples, "
+	"    NULL::integer AS n_pages, "
+	"    jsonb_object_agg( "
+	"        'regular', "
+	"        jsonb_build_object( "
+	"            'stxkinds', "
+	"            to_jsonb(e.stxkind), "
+	"            'stxdndistinct', "
+	"            ( "
+	"                SELECT "
+	"                    jsonb_agg( "
+	"                        jsonb_build_object( "
+	"                            'attnums', "
+	"                            string_to_array(nd.attnums, ', '::text), "
+	"                            'ndistinct', "
+	"                            nd.ndistinct "
+	"                            ) "
+	"                        ORDER BY nd.ord "
+	"                    ) "
+	"                FROM json_each_text(e.stxndistinct::text::json) "
+	"                    WITH ORDINALITY AS nd(attnums, ndistinct, ord) "
+	"                WHERE e.stxndistinct IS NOT NULL "
+	"            ), "
+	"            'stxdndependencies', "
+	"            ( "
+	"                SELECT "
+	"                    jsonb_agg( "
+	"                        jsonb_build_object( "
+	"                            'attnums', "
+	"                            string_to_array( "
+	"                                replace(dep.attrs, ' => ', ', '), ', ' "
+	"                            ), "
+	"                            'degree', "
+	"                            dep.degree "
+	"                        ) "
+	"                        ORDER BY dep.ord "
+	"                    ) "
+	"                FROM json_each_text(e.stxdependencies::text::json) "
+	"                    WITH ORDINALITY AS dep(attrs, degree, ord) "
+	"                WHERE e.stxdependencies IS NOT NULL "
+	"            ) "
+	"        ) "
+	"    ) AS stats "
+	"FROM pg_class r "
+	"JOIN pg_namespace n ON n.oid = r.relnamespace "
+	"JOIN pg_statistic_ext e ON e.stxrelid = r.oid "
+	"GROUP BY schemaname, tablename, ext_stats_name, server_version_num ";
+
 int
 main(int argc, char *argv[])
 {
@@ -138,6 +546,7 @@ main(int argc, char *argv[])
 	PQExpBufferData sql;
 
 	PGconn	   *conn;
+	int			server_version_num;
 
 	FILE	   *copystream = stdout;
 
@@ -227,14 +636,31 @@ main(int argc, char *argv[])
 
 	conn = connectDatabase(&cparams, progname, echo, false, true);
 
+	server_version_num = PQserverVersion(conn);
+
 	initPQExpBuffer(&sql);
 
 	appendPQExpBufferStr(&sql, "COPY (");
 
-	if (PQserverVersion(conn) >= 170000)
-		appendPQExpBufferStr(&sql, export_query_v17);
-	else if (PQserverVersion(conn) >= 100000)
-		appendPQExpBufferStr(&sql, export_query_v10);
+	if (server_version_num >= 170000)
+		appendPQExpBufferStr(&sql, export_rel_query_v17);
+	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 >= 170000)
+		appendPQExpBufferStr(&sql, export_ext_query_v17);
+	else 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");
 
@@ -244,7 +670,7 @@ main(int argc, char *argv[])
 	result_status = PQresultStatus(result);
 
 	if (result_status != PGRES_COPY_OUT)
-		pg_fatal("malformed copy command");
+		pg_fatal("malformed copy command: %s", PQerrorMessage(conn));
 
 	for (;;)
 	{
diff --git a/src/bin/scripts/pg_import_stats.c b/src/bin/scripts/pg_import_stats.c
index 122afc0971..a8b6f9c701 100644
--- a/src/bin/scripts/pg_import_stats.c
+++ b/src/bin/scripts/pg_import_stats.c
@@ -57,6 +57,7 @@ main(int argc, char *argv[])
 
 	int		i;
 	int		numtables;
+	int		numextstats;
 
 	pg_logging_init(argv[0]);
 	progname = get_progname(argv[0]);
@@ -141,21 +142,69 @@ main(int argc, char *argv[])
 
 	/* 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, "
+		"n_tuples float4, "
+		"n_pages 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, "
+		"n_tuples float4, "
+		"n_pages 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, server_version_num integer, "
-		"n_tuples float4, n_pages integer, stats jsonb )");
+		"schemaname text, "
+		"relname text, "
+		"ext_stats_name text, "
+		"server_version_num integer, "
+		"n_tuples float4, "
+		"n_pages integer, "
+		"stats jsonb )");
 
 	if (PQresultStatus(result) != PGRES_COMMAND_OK)
-		pg_fatal("could not create temporary file: %s", PQerrorMessage(conn));
+		pg_fatal("could not create temporary table: %s", PQerrorMessage(conn));
 
 	PQclear(result);
 
+	/*
+	 * Copy input data into combined table.
+	 */
 	result = PQexec(conn,
-		"COPY import_stats(schemaname, relname, server_version_num, n_tuples, "
-		"n_pages, stats) FROM STDIN");
+		"COPY import_stats(schemaname, relname, ext_stats_name, "
+		"server_version_num, n_tuples, n_pages, stats) FROM STDIN");
 
 	if (PQresultStatus(result) != PGRES_COPY_IN)
 		pg_fatal("error copying data to import_stats: %s", PQerrorMessage(conn));
@@ -185,30 +234,52 @@ main(int argc, char *argv[])
 	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, n_tuples, n_pages, stats) "
+		"SELECT schemaname, relname, server_version_num, "
+		"n_tuples, n_pages, 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);
 
-	result = PQprepare(conn, "import",
-		"SELECT pg_import_rel_stats(c.oid, s.server_version_num, "
-		"             s.n_tuples, s.n_pages, s.stats) as import_result "
-		"FROM import_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);
+	/*
+	 * 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("error in PREPARE: %s", PQerrorMessage(conn));
+		pg_fatal("relation stats insert error: %s", PQerrorMessage(conn));
+
+	numextstats = atol(PQcmdTuples(result));
 
 	PQclear(result);
 
-	if (!quiet)
+	if (numtables > 0)
 	{
-		result = PQprepare(conn, "echo",
-			"SELECT s.schemaname, s.relname "
-			"FROM import_stats AS s "
+
+		result = PQprepare(conn, "import_rel",
+			"SELECT pg_import_rel_stats(c.oid, s.server_version_num, "
+			"             s.n_tuples, s.n_pages, s.stats) 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);
 
@@ -216,62 +287,172 @@ main(int argc, char *argv[])
 			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;
+		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);
 
-		const char *const values[] = {istr};
+			if (PQresultStatus(result) != PGRES_COMMAND_OK)
+				pg_fatal("error in PREPARE: %s", PQerrorMessage(conn));
 
-		snprintf(istr, 32, "%d", i);
+			PQclear(result);
+		}
 
-		if (!quiet)
+		for (i = 1; i <= numtables; i++)
 		{
-			result = PQexecPrepared(conn, "echo", 1, values, NULL, NULL, 0);
-			schema = pg_strdup(PQgetvalue(result, 0, 0));
-			table = pg_strdup(PQgetvalue(result, 0, 1));
+			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);
 		}
+	}
 
-		PQclear(result);
+	if (numextstats > 0)
+	{
+
+	result = PQprepare(conn, "import_ext",
+		"SELECT pg_import_ext_stats(e.oid, s.server_version_num, "
+		"             s.stats) 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 ",
+		1, NULL);
 
-		result = PQexecPrepared(conn, "import", 1, values, NULL, NULL, 0);
+		if (PQresultStatus(result) != PGRES_COMMAND_OK)
+			pg_fatal("error in PREPARE: %s", PQerrorMessage(conn));
 
-		if (quiet)
+		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);
-			continue;
 		}
 
-		if (PQresultStatus(result) == PGRES_TUPLES_OK)
+		for (i = 1; i <= numextstats; i++)
 		{
-			int 	rows = PQntuples(result);
+			char	istr[32];
+			char   *schema = NULL;
+			char   *table = NULL;
+			char   *stat = NULL;
+
+			const char *const values[] = {istr};
+
+			snprintf(istr, 32, "%d", i);
 
-			if (rows == 1)
+			if (!quiet)
 			{
-				char   *retval = PQgetvalue(result, 0, 0);
-				if (*retval == 't')
-					printf("%s.%s: imported\n", schema, table);
+				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
-					printf("%s.%s: failed\n", schema, table);
+					pg_fatal("import function must return 0 or 1 rows");
 			}
-			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));
+				printf("%s on %s.%s: error: %s\n", stat, schema, table, PQerrorMessage(conn));
 
-		if (schema != NULL)
-			pfree(schema);
+			if (schema != NULL)
+				pfree(schema);
 
-		if (table != NULL)
-			pfree(table);
+			if (table != NULL)
+				pfree(table);
 
-		PQclear(result);
+			if (stat != NULL)
+				pfree(stat);
+
+			PQclear(result);
+		}
 	}
 
 	exit(0);
-- 
2.43.0

#11Andrei Lepikhov
a.lepikhov@postgrespro.ru
In reply to: Corey Huinker (#10)
Re: Statistics Import and Export

On 13/12/2023 17:26, Corey Huinker wrote:> 4. I don't yet have a
complete vision for how these tools will be used

by pg_upgrade and pg_dump/restore, the places where these will provide
the biggest win for users.

Some issues here with docs:

func.sgml:28465: parser error : Opening and ending tag mismatch: sect1
line 26479 and sect2
</sect2>
^

Also, as I remember, we already had some attempts to invent dump/restore
statistics [1,2]. They were stopped with the problem of type
verification. What if the definition of the type has changed between the
dump and restore? As I see in the code, Importing statistics you just
check the column name and don't see into the type.

[1]: Backup and recovery of pg_statistic /messages/by-id/724322880.K8vzik8zPz@abook
/messages/by-id/724322880.K8vzik8zPz@abook
[2]: Re: Ideas about a better API for postgres_fdw remote estimates /messages/by-id/7a40707d-1758-85a2-7bb1-6e5775518e64@postgrespro.ru
/messages/by-id/7a40707d-1758-85a2-7bb1-6e5775518e64@postgrespro.ru

--
regards,
Andrei Lepikhov
Postgres Professional

#12Corey Huinker
corey.huinker@gmail.com
In reply to: Andrei Lepikhov (#11)
Re: Statistics Import and Export

On Fri, Dec 15, 2023 at 3:36 AM Andrei Lepikhov <a.lepikhov@postgrespro.ru>
wrote:

On 13/12/2023 17:26, Corey Huinker wrote:> 4. I don't yet have a
complete vision for how these tools will be used

by pg_upgrade and pg_dump/restore, the places where these will provide
the biggest win for users.

Some issues here with docs:

func.sgml:28465: parser error : Opening and ending tag mismatch: sect1
line 26479 and sect2
</sect2>
^

Apologies, will fix.

Also, as I remember, we already had some attempts to invent dump/restore
statistics [1,2]. They were stopped with the problem of type
verification. What if the definition of the type has changed between the
dump and restore? As I see in the code, Importing statistics you just
check the column name and don't see into the type.

We look up the imported statistics via column name, that is correct.

However, the values in stavalues and mcv and such are stored purely as
text, so they must be casted using the input functions for that particular
datatype. If that column definition changed, or the underlying input
function changed, the stats import of that particular table would fail. It
should be noted, however, that those same input functions were used to
bring the data into the table via restore, so it would have already failed
on that step. Either way, the structure of the table has effectively
changed, so failure to import those statistics would be a good thing.

[1] Backup and recovery of pg_statistic
/messages/by-id/724322880.K8vzik8zPz@abook

That proposal sought to serialize enough information on the old server such
that rows could be directly inserted into pg_statistic on the new server.
As was pointed out at the time, version N of a server cannot know what the
format of pg_statistic will be in version N+1.

This patch avoids that problem by inspecting the structure of the object to
be faux-analyzed, and using that to determine what parts of the JSON to
fetch, and what datatype to cast values to in cases like mcv and
stavaluesN. The exported JSON has no oids in it whatseover, all elements
subject to casting on import have already been cast to text, and the record
returned has the server version number of the producing system, and the
import function can use that to determine how it interprets the data it
finds.

[2] Re: Ideas about a better API for postgres_fdw remote estimates

/messages/by-id/7a40707d-1758-85a2-7bb1-6e5775518e64@postgrespro.ru

This one seems to be pulling oids from the remote server, and we can't
guarantee their stability across systems, especially for objects and
operators from extensions. I tried to go the route of extracting the full
text name of an operator, but discovered that the qualified names, in
addition to being unsightly, were irrelevant because we can't insert stats
that disagree about type with the attribute/expression. So it didn't matter
what type the remote system thought it had, the local system was going to
coerce it into the expected data type or ereport() trying.

I think there is hope for having do_analyze() run a remote query fetching
the remote table's exported stats and then storing them locally, possibly
after some modification, and that would save us from having to sample a
remote table.

#13Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Corey Huinker (#10)
Re: Statistics Import and Export

Hi,

I finally had time to look at the last version of the patch, so here's a
couple thoughts and questions in somewhat random order. Please take this
as a bit of a brainstorming and push back if you disagree some of my
comments.

In general, I like the goal of this patch - not having statistics is a
common issue after an upgrade, and people sometimes don't even realize
they need to run analyze. So, it's definitely worth improving.

I'm not entirely sure about the other use case - allowing people to
tweak optimizer statistics on a running cluster, to see what would be
the plan in that case. Or more precisely - I agree that would be an
interesting and useful feature, but maybe the interface should not be
the same as for the binary upgrade use case?

interfaces
----------

When I thought about the ability to dump/load statistics in the past, I
usually envisioned some sort of DDL that would do the export and import.
So for example we'd have EXPORT STATISTICS / IMPORT STATISTICS commands,
or something like that, and that'd do all the work. This would mean
stats are "first-class citizens" and it'd be fairly straightforward to
add this into pg_dump, for example. Or at least I think so ...

Alternatively we could have the usual "functional" interface, with a
functions to export/import statistics, replacing the DDL commands.

Unfortunately, none of this works for the pg_upgrade use case, because
existing cluster versions would not support this new interface, of
course. That's a significant flaw, as it'd make this useful only for
upgrades of future versions.

So I think for the pg_upgrade use case, we don't have much choice other
than using "custom" export through a view, which is what the patch does.

However, for the other use case (tweaking optimizer stats) this is not
really an issue - that always happens on the same instance, so no issue
with not having the "export" function and so on. I'd bet there are more
convenient ways to do this than using the export view. I'm sure it could
share a lot of the infrastructure, ofc.

I suggest we focus on the pg_upgrade use case for now. In particular, I
think we really need to find a good way to integrate this into
pg_upgrade. I'm not against having custom CLI commands, but it's still a
manual thing - I wonder if we could extend pg_dump to dump stats, or
make it built-in into pg_upgrade in some way (possibly disabled by
default, or something like that).

JSON format
-----------

As for the JSON format, I wonder if we need that at all? Isn't that an
unnecessary layer of indirection? Couldn't we simply dump pg_statistic
and pg_statistic_ext_data in CSV, or something like that? The amount of
new JSONB code seems to be very small, so it's OK I guess.

I'm still a bit unsure about the "right" JSON schema. I find it a bit
inconvenient that the JSON objects mimic the pg_statistic schema very
closely. In particular, it has one array for stakind values, another
array for stavalues, array for stanumbers etc. I understand generating
this JSON in SQL is fairly straightforward, and for the pg_upgrade use
case it's probably OK. But my concern is it's not very convenient for
the "manual tweaking" use case, because the "related" fields are
scattered in different parts of the JSON.

That's pretty much why I envisioned a format "grouping" the arrays for a
particular type of statistics (MCV, histogram) into the same object, as
for example in

{
"mcv" : {"values" : [...], "frequencies" : [...]}
"histogram" : {"bounds" : [...]}
}

But that's probably much harder to generate from plain SQL (at least I
think so, I haven't tried).

data missing in the export
--------------------------

I think the data needs to include more information. Maybe not for the
pg_upgrade use case, where it's mostly guaranteed not to change, but for
the "manual tweak" use case it can change. And I don't think we want two
different formats - we want one, working for everything.

Consider for example about the staopN and stacollN fields - if we clone
the stats from one table to the other, and the table uses different
collations, will that still work? Similarly, I think we should include
the type of each column, because it's absolutely not guaranteed the
import function will fail if the type changes. For example, if the type
changes from integer to text, that will work, but the ordering will
absolutely not be the same. And so on.

For the extended statistics export, I think we need to include also the
attribute names and expressions, because these can be different between
the statistics. And not only that - the statistics values reference the
attributes by positions, but if the two tables have the attributes in a
different order (when ordered by attnum), that will break stuff.

more strict checks
------------------

I think the code should be a bit more "defensive" when importing stuff,
and do at least some sanity checks. For the pg_upgrade use case this
should be mostly non-issue (except for maybe helping to detect bugs
earlier), but for the "manual tweak" use case it's much more important.

By this I mean checks like:

* making sure the frequencies in MCV lists are not obviously wrong
(outside [0,1], sum exceeding > 1.0, etc.)

* cross-checking that stanumbers/stavalues make sense (e.g. that MCV has
both arrays while histogram has only stavalues, that the arrays have
the same length for MCV, etc.)

* checking there are no duplicate stakind values (e.g. two MCV lists)

This is another reason why I was thinking the current JSON format may be
a bit inconvenient, because it loads the fields separately, making the
checks harder. But I guess it could be done after loading everything, as
a separate phase.

Not sure if all the checks need to be regular elog(ERROR), perhaps some
could/should be just asserts.

minor questions
---------------

1) Should the views be called pg_statistic_export or pg_stats_export?
Perhaps pg_stats_export is better, because the format is meant to be
human-readable (rather than 100% internal).

2) It's not very clear what "non-transactional update" of pg_class
fields actually means. Does that mean we update the fields in-place,
can't be rolled back, is not subject to MVCC or what? I suspect users
won't know unless the docs say that explicitly.

3) The "statistics.c" code should really document the JSON structure. Or
maybe if we plan to use this for other purposes, it should be documented
in the SGML?

Actually, this means that the use supported cases determine if the
expected JSON structure is part of the API. For pg_upgrade we could keep
it as "internal" and maybe change it as needed, but for "manual tweak"
it'd become part of the public API.

4) Why do we need the separate "replaced" flags in import_stakinds? Can
it happen that collreplaces/opreplaces differ from kindreplaces?

5) What happens in we import statistics for a table that already has
some statistics? Will this discard the existing statistics, or will this
merge them somehow? (I think we should always discard the existing
stats, and keep only the new version.)

6) What happens if we import extended stats with mismatching definition?
For example, what if the "new" statistics object does not have "mcv"
enabled, but the imported data do include MCV? What if the statistics do
have the same number of "dimensions" but not the same number of columns
and expressions?

7) The func.sgml additions in 0007 seems a bit strange, particularly the
first sentence of the paragraph.

8) While experimenting with the patch, I noticed this:

create table t (a int, b int, c text);
create statistics s on a, b, c, (a+b), (a-b) from t;

create table t2 (a text, b text, c text);
create statistics s2 on a, c from t2;

select pg_import_ext_stats(
(select oid from pg_statistic_ext where stxname = 's2'),
(select server_version_num from pg_statistic_ext_export
where ext_stats_name = 's'),
(select stats from pg_statistic_ext_export
where ext_stats_name = 's'));

WARNING: statistics import has 5 mcv dimensions, but the expects 2.
Skipping excess dimensions.
ERROR: statistics import has 5 mcv dimensions, but the expects 2.
Skipping excess dimensions.

I guess we should not trigger WARNING+ERROR with the same message.

regards

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

#14Bruce Momjian
bruce@momjian.us
In reply to: Tomas Vondra (#13)
Re: Statistics Import and Export

On Tue, Dec 26, 2023 at 02:18:56AM +0100, Tomas Vondra wrote:

interfaces
----------

When I thought about the ability to dump/load statistics in the past, I
usually envisioned some sort of DDL that would do the export and import.
So for example we'd have EXPORT STATISTICS / IMPORT STATISTICS commands,
or something like that, and that'd do all the work. This would mean
stats are "first-class citizens" and it'd be fairly straightforward to
add this into pg_dump, for example. Or at least I think so ...

Alternatively we could have the usual "functional" interface, with a
functions to export/import statistics, replacing the DDL commands.

Unfortunately, none of this works for the pg_upgrade use case, because
existing cluster versions would not support this new interface, of
course. That's a significant flaw, as it'd make this useful only for
upgrades of future versions.

So I think for the pg_upgrade use case, we don't have much choice other
than using "custom" export through a view, which is what the patch does.

However, for the other use case (tweaking optimizer stats) this is not
really an issue - that always happens on the same instance, so no issue
with not having the "export" function and so on. I'd bet there are more
convenient ways to do this than using the export view. I'm sure it could
share a lot of the infrastructure, ofc.

I suggest we focus on the pg_upgrade use case for now. In particular, I
think we really need to find a good way to integrate this into
pg_upgrade. I'm not against having custom CLI commands, but it's still a
manual thing - I wonder if we could extend pg_dump to dump stats, or
make it built-in into pg_upgrade in some way (possibly disabled by
default, or something like that).

I have some thoughts on this too. I understand the desire to add
something that can be used for upgrades _to_ PG 17, but I am concerned
that this will give us a cumbersome API that will hamper future
development. I think we should develop the API we want, regardless of
how useful it is for upgrades _to_ PG 17, and then figure out what
short-term hacks we can add to get it working for upgrades _to_ PG 17;
these hacks can eventually be removed. Even if they can't be removed,
they are export-only and we can continue developing the import SQL
command cleanly, and I think import is going to need the most long-term
maintenance.

I think we need a robust API to handle two cases:

* changes in how we store statistics
* changes in how how data type values are represented in the statistics

We have had such changes in the past, and I think these two issues are
what have prevented import/export of statistics up to this point.
Developing an API that doesn't cleanly handle these will cause long-term
pain.

In summary, I think we need an SQL-level command for this. I think we
need to embed the Postgres export version number into the statistics
export file (maybe in the COPY header), and then load the file via COPY
internally (not JSON) into a temporary table that we know matches the
exported Postgres version. We then need to use SQL to make any
adjustments to it before loading it into pg_statistic. Doing that
internally in JSON just isn't efficient. If people want JSON for such
cases, I suggest we add a JSON format to COPY.

I think we can then look at pg_upgrade to see if we can simulate the
export action which can use the statistics import SQL command.

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

Only you can decide what is important to you.

#15Tom Lane
tgl@sss.pgh.pa.us
In reply to: Bruce Momjian (#14)
Re: Statistics Import and Export

Bruce Momjian <bruce@momjian.us> writes:

I think we need a robust API to handle two cases:

* changes in how we store statistics
* changes in how how data type values are represented in the statistics

We have had such changes in the past, and I think these two issues are
what have prevented import/export of statistics up to this point.
Developing an API that doesn't cleanly handle these will cause long-term
pain.

Agreed.

In summary, I think we need an SQL-level command for this.

I think a SQL command is an actively bad idea. It'll just add development
and maintenance overhead that we don't need. When I worked on this topic
years ago at Salesforce, I had things set up with simple functions, which
pg_dump would invoke by writing more or less

SELECT pg_catalog.load_statistics(....);

This has a number of advantages, not least of which is that an extension
could plausibly add compatible functions to older versions. The trick,
as you say, is to figure out what the argument lists ought to be.
Unfortunately I recall few details of what I wrote for Salesforce,
but I think I had it broken down in a way where there was a separate
function call occurring for each pg_statistic "slot", thus roughly

load_statistics(table regclass, attname text, stakind int, stavalue ...);

I might have had a separate load_statistics_xxx function for each
stakind, which would ease the issue of deciding what the datatype
of "stavalue" is. As mentioned already, we'd also need some sort of
version identifier, and we'd expect the load_statistics() functions
to be able to transform the data if the old version used a different
representation. I agree with the idea that an explicit representation
of the source table attribute's type would be wise, too.

regards, tom lane

#16Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Tom Lane (#15)
Re: Statistics Import and Export

On 12/26/23 20:19, Tom Lane wrote:

Bruce Momjian <bruce@momjian.us> writes:

I think we need a robust API to handle two cases:

* changes in how we store statistics
* changes in how how data type values are represented in the statistics

We have had such changes in the past, and I think these two issues are
what have prevented import/export of statistics up to this point.
Developing an API that doesn't cleanly handle these will cause long-term
pain.

Agreed.

I agree the format is important - we don't want to end up with a format
that's cumbersome or inconvenient to use. But I don't think the proposed
format is somewhat bad in those respects - it mostly reflects how we
store statistics and if I was designing a format for humans, it might
look a bit differently. But that's not the goal here, IMHO.

I don't quite understand the two cases above. Why should this affect how
we store statistics? Surely, making the statistics easy to use for the
optimizer is much more important than occasional export/import.

In summary, I think we need an SQL-level command for this.

I think a SQL command is an actively bad idea. It'll just add development
and maintenance overhead that we don't need. When I worked on this topic
years ago at Salesforce, I had things set up with simple functions, which
pg_dump would invoke by writing more or less

SELECT pg_catalog.load_statistics(....);

This has a number of advantages, not least of which is that an extension
could plausibly add compatible functions to older versions. The trick,
as you say, is to figure out what the argument lists ought to be.
Unfortunately I recall few details of what I wrote for Salesforce,
but I think I had it broken down in a way where there was a separate
function call occurring for each pg_statistic "slot", thus roughly

load_statistics(table regclass, attname text, stakind int, stavalue ...);

I might have had a separate load_statistics_xxx function for each
stakind, which would ease the issue of deciding what the datatype
of "stavalue" is. As mentioned already, we'd also need some sort of
version identifier, and we'd expect the load_statistics() functions
to be able to transform the data if the old version used a different
representation. I agree with the idea that an explicit representation
of the source table attribute's type would be wise, too.

Yeah, this is pretty much what I meant by "functional" interface. But if
I said maybe the format implemented by the patch is maybe too close to
how we store the statistics, then this has exactly the same issue. And
it has other issues too, I think - it breaks down the stats into
multiple function calls, so ensuring the sanity/correctness of whole
sets of statistics gets much harder, I think.

I'm not sure about the extension idea. Yes, we could have an extension
providing such functions, but do we have any precedent of making
pg_upgrade dependent on an external extension? I'd much rather have
something built-in that just works, especially if we intend to make it
the default behavior (which I think should be our aim here).

regards

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

#17Bruce Momjian
bruce@momjian.us
In reply to: Tomas Vondra (#16)
Re: Statistics Import and Export

On Wed, Dec 27, 2023 at 01:08:47PM +0100, Tomas Vondra wrote:

On 12/26/23 20:19, Tom Lane wrote:

Bruce Momjian <bruce@momjian.us> writes:

I think we need a robust API to handle two cases:

* changes in how we store statistics
* changes in how how data type values are represented in the statistics

We have had such changes in the past, and I think these two issues are
what have prevented import/export of statistics up to this point.
Developing an API that doesn't cleanly handle these will cause long-term
pain.

Agreed.

I agree the format is important - we don't want to end up with a format
that's cumbersome or inconvenient to use. But I don't think the proposed
format is somewhat bad in those respects - it mostly reflects how we
store statistics and if I was designing a format for humans, it might
look a bit differently. But that's not the goal here, IMHO.

I don't quite understand the two cases above. Why should this affect how
we store statistics? Surely, making the statistics easy to use for the
optimizer is much more important than occasional export/import.

The two items above were to focus on getting a solution that can easily
handle future statistics storage changes. I figured we would want to
manipulate the data as a table internally so I am confused why we would
export JSON instead of a COPY format. I didn't think we were changing
how we internall store or use the statistics.

In summary, I think we need an SQL-level command for this.

I think a SQL command is an actively bad idea. It'll just add development
and maintenance overhead that we don't need. When I worked on this topic
years ago at Salesforce, I had things set up with simple functions, which
pg_dump would invoke by writing more or less

SELECT pg_catalog.load_statistics(....);

This has a number of advantages, not least of which is that an extension
could plausibly add compatible functions to older versions. The trick,
as you say, is to figure out what the argument lists ought to be.
Unfortunately I recall few details of what I wrote for Salesforce,
but I think I had it broken down in a way where there was a separate
function call occurring for each pg_statistic "slot", thus roughly

load_statistics(table regclass, attname text, stakind int, stavalue ...);

I might have had a separate load_statistics_xxx function for each
stakind, which would ease the issue of deciding what the datatype
of "stavalue" is. As mentioned already, we'd also need some sort of
version identifier, and we'd expect the load_statistics() functions
to be able to transform the data if the old version used a different
representation. I agree with the idea that an explicit representation
of the source table attribute's type would be wise, too.

Yeah, this is pretty much what I meant by "functional" interface. But if
I said maybe the format implemented by the patch is maybe too close to
how we store the statistics, then this has exactly the same issue. And
it has other issues too, I think - it breaks down the stats into
multiple function calls, so ensuring the sanity/correctness of whole
sets of statistics gets much harder, I think.

I was suggesting an SQL command because this feature is going to need a
lot of options and do a lot of different things, I am afraid, and a
single function might be too complex to manage.

I'm not sure about the extension idea. Yes, we could have an extension
providing such functions, but do we have any precedent of making
pg_upgrade dependent on an external extension? I'd much rather have
something built-in that just works, especially if we intend to make it
the default behavior (which I think should be our aim here).

Uh, an extension seems nice to allow people in back branches to install
it, but not for normal usage.

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

Only you can decide what is important to you.

#18Corey Huinker
corey.huinker@gmail.com
In reply to: Tomas Vondra (#13)
Re: Statistics Import and Export

On Mon, Dec 25, 2023 at 8:18 PM Tomas Vondra <tomas.vondra@enterprisedb.com>
wrote:

Hi,

I finally had time to look at the last version of the patch, so here's a
couple thoughts and questions in somewhat random order. Please take this
as a bit of a brainstorming and push back if you disagree some of my
comments.

In general, I like the goal of this patch - not having statistics is a
common issue after an upgrade, and people sometimes don't even realize
they need to run analyze. So, it's definitely worth improving.

I'm not entirely sure about the other use case - allowing people to
tweak optimizer statistics on a running cluster, to see what would be
the plan in that case. Or more precisely - I agree that would be an
interesting and useful feature, but maybe the interface should not be
the same as for the binary upgrade use case?

interfaces
----------

When I thought about the ability to dump/load statistics in the past, I
usually envisioned some sort of DDL that would do the export and import.
So for example we'd have EXPORT STATISTICS / IMPORT STATISTICS commands,
or something like that, and that'd do all the work. This would mean
stats are "first-class citizens" and it'd be fairly straightforward to
add this into pg_dump, for example. Or at least I think so ...

Alternatively we could have the usual "functional" interface, with a
functions to export/import statistics, replacing the DDL commands.

Unfortunately, none of this works for the pg_upgrade use case, because
existing cluster versions would not support this new interface, of
course. That's a significant flaw, as it'd make this useful only for
upgrades of future versions.

This was the reason I settled on the interface that I did: while we can
create whatever interface we want for importing the statistics, we would
need to be able to extract stats from databases using only the facilities
available in those same databases, and then store that in a medium that
could be conveyed across databases, either by text files or by saving them
off in a side table prior to upgrade. JSONB met the criteria.

So I think for the pg_upgrade use case, we don't have much choice other
than using "custom" export through a view, which is what the patch does.

However, for the other use case (tweaking optimizer stats) this is not
really an issue - that always happens on the same instance, so no issue
with not having the "export" function and so on. I'd bet there are more
convenient ways to do this than using the export view. I'm sure it could
share a lot of the infrastructure, ofc.

So, there is a third use case - foreign data wrappers. When analyzing a
foreign table, at least one in the postgresql_fdw family of foreign
servers, we should be able to send a query specific to the version and
dialect of that server, get back the JSONB, and import those results. That
use case may be more tangible to you than the tweak/tuning case.

JSON format
-----------

As for the JSON format, I wonder if we need that at all? Isn't that an
unnecessary layer of indirection? Couldn't we simply dump pg_statistic
and pg_statistic_ext_data in CSV, or something like that? The amount of
new JSONB code seems to be very small, so it's OK I guess.

I see a few problems with dumping pg_statistic[_ext_data]. The first is
that the importer now has to understand all of the past formats of those
two tables. The next is that the tables are chock full of Oids that don't
necessarily carry forward. I could see us having a text-ified version of
those two tables, but we'd need that for all previous iterations of those
table formats. Instead, I put the burden on the stats export to de-oid the
data and make it *_in() function friendly.

That's pretty much why I envisioned a format "grouping" the arrays for a
particular type of statistics (MCV, histogram) into the same object, as
for example in

{
"mcv" : {"values" : [...], "frequencies" : [...]}
"histogram" : {"bounds" : [...]}
}

I agree that would be a lot more readable, and probably a lot more
debuggable. But I went into this unsure if there could be more than one
stats slot of a given kind per table. Knowing that they must be unique
helps.

But that's probably much harder to generate from plain SQL (at least I
think so, I haven't tried).

I think it would be harder, but far from impossible.

data missing in the export
--------------------------

I think the data needs to include more information. Maybe not for the
pg_upgrade use case, where it's mostly guaranteed not to change, but for
the "manual tweak" use case it can change. And I don't think we want two
different formats - we want one, working for everything.

I"m not against this at all, and I started out doing that, but the
qualified names of operators got _ugly_, and I quickly realized that what I
was generating wouldn't matter, either the input data would make sense for
the attribute's stats or it would fail trying.

Consider for example about the staopN and stacollN fields - if we clone
the stats from one table to the other, and the table uses different
collations, will that still work? Similarly, I think we should include
the type of each column, because it's absolutely not guaranteed the
import function will fail if the type changes. For example, if the type
changes from integer to text, that will work, but the ordering will
absolutely not be the same. And so on.

I can see including the type of the column, that's a lot cleaner than the
operator names for sure, and I can see us rejecting stats or sections of
stats in certain situations. Like in your example, if the collation
changed, then reject all "<" op stats but keep the "=" ones.

For the extended statistics export, I think we need to include also the
attribute names and expressions, because these can be different between
the statistics. And not only that - the statistics values reference the
attributes by positions, but if the two tables have the attributes in a
different order (when ordered by attnum), that will break stuff.

Correct me if I'm wrong, but I thought expression parse trees change _a
lot_ from version to version?

Attribute reordering is a definite vulnerability of the current
implementation, so an attribute name export might be a way to mitigate that.

* making sure the frequencies in MCV lists are not obviously wrong
(outside [0,1], sum exceeding > 1.0, etc.)

+1

* cross-checking that stanumbers/stavalues make sense (e.g. that MCV has
both arrays while histogram has only stavalues, that the arrays have
the same length for MCV, etc.)

To this end, there's an edge-case hack in the code where I have to derive
the array elemtype. I had thought that examine_attribute() or
std_typanalyze() was going to do that for me, but it didn't. Very much want
your input there.

* checking there are no duplicate stakind values (e.g. two MCV lists)

Per previous comment, it's good to learn these restrictions.

Not sure if all the checks need to be regular elog(ERROR), perhaps some
could/should be just asserts.

For this first pass, all errors were one-size fits all, safe for the
WARNING vs ERROR.

minor questions
---------------

1) Should the views be called pg_statistic_export or pg_stats_export?
Perhaps pg_stats_export is better, because the format is meant to be
human-readable (rather than 100% internal).

I have no opinion on what the best name would be, and will go with
consensus.

2) It's not very clear what "non-transactional update" of pg_class
fields actually means. Does that mean we update the fields in-place,
can't be rolled back, is not subject to MVCC or what? I suspect users
won't know unless the docs say that explicitly.

Correct. Cannot be rolled back, not subject to MVCC.

3) The "statistics.c" code should really document the JSON structure. Or
maybe if we plan to use this for other purposes, it should be documented
in the SGML?

I agree, but I also didn't expect the format to survive first contact with
reviewers, so I held back.

4) Why do we need the separate "replaced" flags in import_stakinds? Can
it happen that collreplaces/opreplaces differ from kindreplaces?

That was initially done to maximize the amount of code that could be copied
from do_analyze(). In retrospect, I like how extended statistics just
deletes all the pg_statistic_ext_data rows and replaces them and I would
like to do the same for pg_statistic before this is all done.

5) What happens in we import statistics for a table that already has
some statistics? Will this discard the existing statistics, or will this
merge them somehow? (I think we should always discard the existing
stats, and keep only the new version.)

In the case of pg_statistic_ext_data, the stats are thrown out and replaced
by the imported ones.

In the case of pg_statistic, it's basically an upsert, and any values that
were missing in the JSON are not updated on the existing row. That's
appealing in a tweak situation where you want to only alter one or two bits
of a stat, but not really useful in other situations. Per previous comment,
I'd prefer a clean slate and forcing tweaking use cases to fill in all the
blanks.

6) What happens if we import extended stats with mismatching definition?
For example, what if the "new" statistics object does not have "mcv"
enabled, but the imported data do include MCV? What if the statistics do
have the same number of "dimensions" but not the same number of columns
and expressions?

The importer is currently driven by the types of stats to be expected for
that pg_attribute/pg_statistic_ext. It only looks for things that are
possible for that stat type, and any extra JSON values are ignored.

#19Corey Huinker
corey.huinker@gmail.com
In reply to: Tom Lane (#15)
Re: Statistics Import and Export

As mentioned already, we'd also need some sort of
version identifier, and we'd expect the load_statistics() functions
to be able to transform the data if the old version used a different
representation. I agree with the idea that an explicit representation
of the source table attribute's type would be wise, too.

There is a version identifier currently (its own column not embedded in the
JSON), but I discovered that I was able to put the burden on the export
queries to spackle-over the changes in the table structures over time.
Still, I knew that we'd need the version number in there eventually.

#20Corey Huinker
corey.huinker@gmail.com
In reply to: Tomas Vondra (#16)
Re: Statistics Import and Export

Yeah, this is pretty much what I meant by "functional" interface. But if
I said maybe the format implemented by the patch is maybe too close to
how we store the statistics, then this has exactly the same issue. And
it has other issues too, I think - it breaks down the stats into
multiple function calls, so ensuring the sanity/correctness of whole
sets of statistics gets much harder, I think.

Export functions was my original plan, for simplicity, maintenance, etc,
but it seemed like I'd be adding quite a few functions, so the one view
made more sense for an initial version. Also, I knew that pg_dump or some
other stats exporter would have to inline the guts of those functions into
queries for older versions, and adapting a view definition seemed more
straightforward for the reader than function definitions.

#21Tom Lane
tgl@sss.pgh.pa.us
In reply to: Corey Huinker (#20)
Re: Statistics Import and Export

Corey Huinker <corey.huinker@gmail.com> writes:

Export functions was my original plan, for simplicity, maintenance, etc,
but it seemed like I'd be adding quite a few functions, so the one view
made more sense for an initial version. Also, I knew that pg_dump or some
other stats exporter would have to inline the guts of those functions into
queries for older versions, and adapting a view definition seemed more
straightforward for the reader than function definitions.

Hmm, I'm not sure we are talking about the same thing at all.

What I am proposing is *import* functions. I didn't say anything about
how pg_dump obtains the data it prints; however, I would advocate that
we keep that part as simple as possible. You cannot expect export
functionality to know the requirements of future server versions,
so I don't think it's useful to put much intelligence there.

So I think pg_dump should produce a pretty literal representation of
what it finds in the source server's catalog, and then rely on the
import functions in the destination server to make sense of that
and do whatever slicing-n-dicing is required.

That being the case, I don't see a lot of value in a view -- especially
not given the requirement to dump from older server versions.
(Conceivably we could just say that we won't dump stats from server
versions predating the introduction of this feature, but that's hardly
a restriction that supports doing this via a view.)

regards, tom lane

#22Bruce Momjian
bruce@momjian.us
In reply to: Corey Huinker (#18)
Re: Statistics Import and Export

On Wed, Dec 27, 2023 at 09:41:31PM -0500, Corey Huinker wrote:

When I thought about the ability to dump/load statistics in the past, I
usually envisioned some sort of DDL that would do the export and import.
So for example we'd have EXPORT STATISTICS / IMPORT STATISTICS commands,
or something like that, and that'd do all the work. This would mean
stats are "first-class citizens" and it'd be fairly straightforward to
add this into pg_dump, for example. Or at least I think so ...

Alternatively we could have the usual "functional" interface, with a
functions to export/import statistics, replacing the DDL commands.

Unfortunately, none of this works for the pg_upgrade use case, because
existing cluster versions would not support this new interface, of
course. That's a significant flaw, as it'd make this useful only for
upgrades of future versions.

This was the reason I settled on the interface that I did: while we can create
whatever interface we want for importing the statistics, we would need to be
able to extract stats from databases using only the facilities available in
those same databases, and then store that in a medium that could be conveyed
across databases, either by text files or by saving them off in a side table
prior to upgrade. JSONB met the criteria.

Uh, it wouldn't be crazy to add this capability to pg_upgrade/pg_dump in
a minor version upgrade if it wasn't enabled by default, and if we were
very careful.

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

Only you can decide what is important to you.

#23Corey Huinker
corey.huinker@gmail.com
In reply to: Tom Lane (#21)
Re: Statistics Import and Export

On Wed, Dec 27, 2023 at 10:10 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:

Corey Huinker <corey.huinker@gmail.com> writes:

Export functions was my original plan, for simplicity, maintenance, etc,
but it seemed like I'd be adding quite a few functions, so the one view
made more sense for an initial version. Also, I knew that pg_dump or some
other stats exporter would have to inline the guts of those functions

into

queries for older versions, and adapting a view definition seemed more
straightforward for the reader than function definitions.

Hmm, I'm not sure we are talking about the same thing at all.

Right, I was conflating two things.

What I am proposing is *import* functions. I didn't say anything about
how pg_dump obtains the data it prints; however, I would advocate that
we keep that part as simple as possible. You cannot expect export
functionality to know the requirements of future server versions,
so I don't think it's useful to put much intelligence there.

True, but presumably you'd be using the pg_dump/pg_upgrade of that future
version to do the exporting, so the export format would always be tailored
to the importer's needs.

So I think pg_dump should produce a pretty literal representation of
what it finds in the source server's catalog, and then rely on the
import functions in the destination server to make sense of that
and do whatever slicing-n-dicing is required.

Obviously it can't be purely literal, as we have to replace the oid values
with whatever text representation we feel helps us carry forward. In
addition, we're setting the number of tuples and number of pages directly
in pg_class, and doing so non-transactionally just like ANALYZE does. We
could separate that out into its own import function, but then we're
locking every relation twice, once for the tuples/pages and once again for
the pg_statistic import.

My current line of thinking was that the stats import call, if enabled,
would immediately follow the CREATE statement of the object itself, but
that requires us to have everything we need to know for the import passed
into the import function, so we'd be needing a way to serialize _that_. If
you're thinking that we have one big bulk stats import, that might work,
but it also means that we're less tolerant of failures in the import step.

#24Bruce Momjian
bruce@momjian.us
In reply to: Corey Huinker (#23)
Re: Statistics Import and Export

On Thu, Dec 28, 2023 at 12:28:06PM -0500, Corey Huinker wrote:

What I am proposing is *import* functions.  I didn't say anything about
how pg_dump obtains the data it prints; however, I would advocate that
we keep that part as simple as possible.  You cannot expect export
functionality to know the requirements of future server versions,
so I don't think it's useful to put much intelligence there.

True, but presumably you'd be using the pg_dump/pg_upgrade of that future
version to do the exporting, so the export format would always be tailored to
the importer's needs.

I think the question is whether we will have the export functionality in
the old cluster, or if it will be queries run by pg_dump and therefore
also run by pg_upgrade calling pg_dump.

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

Only you can decide what is important to you.

#25Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Corey Huinker (#10)
Re: Statistics Import and Export

On 12/13/23 11:26, Corey Huinker wrote:

Yeah, that was the simplest output function possible, it didn't seem

worth it to implement something more advanced. pg_mcv_list_items() is
more convenient for most needs, but it's quite far from the on-disk
representation.

I was able to make it work.

That's actually a good question - how closely should the exported data
be to the on-disk format? I'd say we should keep it abstract, not tied
to the details of the on-disk format (which might easily change

between

versions).

For the most part, I chose the exported data json types and formats in a
way that was the most accommodating to cstring input functions. So,
while so many of the statistic values are obviously only ever
integers/floats, those get stored as a numeric data type which lacks
direct numeric->int/float4/float8 functions (though we could certainly
create them, and I'm not against that), casting them to text lets us
leverage pg_strtoint16, etc.

I'm a bit confused about the JSON schema used in pg_statistic_export
view, though. It simply serializes stakinds, stavalues, stanumbers

into

arrays ... which works, but why not to use the JSON nesting? I mean,
there could be a nested document for histogram, MCV, ... with just the
correct fields.

{
...
histogram : { stavalues: [...] },
mcv : { stavalues: [...], stanumbers: [...] },
...
}

That's a very good question. I went with this format because it was
fairly straightforward to code in SQL using existing JSON/JSONB
functions, and that's what we will need if we want to export statistics
on any server currently in existence. I'm certainly not locked in with
the current format, and if it can be shown how to transform the data
into a superior format, I'd happily do so.

and so on. Also, what does TRIVIAL stand for?

It's currently serving double-duty for "there are no stats in this slot"
and the situations where the stats computation could draw no conclusions
about the data.

Attached is v3 of this patch. Key features are:

* Handles regular pg_statistic stats for any relation type.
* Handles extended statistics.
* Export views pg_statistic_export and pg_statistic_ext_export to allow
inspection of existing stats and saving those values for later use.
* Import functions pg_import_rel_stats() and pg_import_ext_stats() which
take Oids as input. This is intentional to allow stats from one object
to be imported into another object.
* User scripts pg_export_stats and pg_import stats, which offer a
primitive way to serialize all the statistics of one database and import
them into another.
* Has regression test coverage for both with a variety of data types.
* Passes my own manual test of extracting all of the stats from a v15
version of the popular "dvdrental" example database, as well as some
additional extended statistics objects, and importing them into a
development database.
* Import operations never touch the heap of any relation outside of
pg_catalog. As such, this should be significantly faster than even the
most cursory analyze operation, and therefore should be useful in
upgrade situations, allowing the database to work with "good enough"
stats more quickly, while still allowing for regular autovacuum to
recalculate the stats "for real" at some later point.

The relation statistics code was adapted from similar features in
analyze.c, but is now done in a query context. As before, the
rowcount/pagecount values are updated on pg_class in a non-transactional
fashion to avoid table bloat, while the updates to pg_statistic are
pg_statistic_ext_data are done transactionally.

The existing statistics _store() functions were leveraged wherever
practical, so much so that the extended statistics import is mostly just
adapting the existing _build() functions into _import() functions which
pull their values from JSON rather than computing the statistics.

Current concerns are:

1. I had to code a special-case exception for MCELEM stats on array data
types, so that the array_in() call uses the element type rather than the
array type. I had assumed that the existing exmaine_attribute()
functions would have properly derived the typoid for that column, but it
appears to not be the case, and I'm clearly missing how the existing
code gets it right.

Hmm, after looking at this, I'm not sure it's such an ugly hack ...

The way this works for ANALYZE is that examine_attribute() eventually
calls the typanalyze function:

if (OidIsValid(stats->attrtype->typanalyze))
ok = DatumGetBool(OidFunctionCall1(stats->attrtype->typanalyze,
PointerGetDatum(stats)));

which for arrays is array_typanalyze, and this sets stats->extra_data to
ArrayAnalyzeExtraData with all the interesting info about the array
element type, and then also std_extra_data with info about the array
type itself.

stats -> extra_data -> std_extra_data

compute_array_stats then "restores" std_extra_data to compute standard
stats for the whole array, and then uses the ArrayAnalyzeExtraData to
calculate stats for the elements.

It's not exactly pretty, because there are global variables and so on.

And examine_rel_attribute() does the same thing - calls typanalyze, so
if I break after it returns, I see this for int[] column:

(gdb) p * (ArrayAnalyzeExtraData *) stat->extra_data

$1 = {type_id = 23, eq_opr = 96, coll_id = 0, typbyval = true, typlen =
4, typalign = 105 'i', cmp = 0x2e57920, hash = 0x2e57950,
std_compute_stats = 0x6681b8 <compute_scalar_stats>, std_extra_data =
0x2efe670}

I think the "problem" will be how to use this in import_stavalues(). You
can't just do this for any array type, I think. I could create an array
type (with ELEMENT=X) but with a custom analyze function, in which case
the extra_data may be something entirely different.

I suppose the correct solution would be to add an "import" function into
the pg_type catalog (next to typanalyze). Or maybe it'd be enough to set
it from the typanalyze? After all, that's what sets compute_stats.

But maybe it's enough to just do what you did - if we get an MCELEM
slot, can it ever contain anything else than array of elements of the
attribute array type? I'd bet that'd cause all sorts of issues, no?

regards

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

#26Corey Huinker
corey.huinker@gmail.com
In reply to: Tomas Vondra (#25)
Re: Statistics Import and Export

But maybe it's enough to just do what you did - if we get an MCELEM
slot, can it ever contain anything else than array of elements of the
attribute array type? I'd bet that'd cause all sorts of issues, no?

Thanks for the explanation of why it wasn't working for me. Knowing that
the case of MCELEM + is-array-type is the only case where we'd need to do
that puts me at ease.

#27Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Corey Huinker (#26)
Re: Statistics Import and Export

On 12/29/23 17:27, Corey Huinker wrote:

But maybe it's enough to just do what you did - if we get an MCELEM
slot, can it ever contain anything else than array of elements of the
attribute array type? I'd bet that'd cause all sorts of issues, no?

Thanks for the explanation of why it wasn't working for me. Knowing that
the case of MCELEM + is-array-type is the only case where we'd need to
do that puts me at ease.

But I didn't claim MCELEM is the only slot where this might be an issue.
I merely asked if a MCELEM slot can ever contain an array with element
type different from the "original" attribute.

After thinking about this a bit more, and doing a couple experiments
with a trivial custom data type, I think this is true:

1) MCELEM slots for "real" array types are OK

I don't think we allow "real" arrays created by users directly, all
arrays are created implicitly by the system. Those types always have
array_typanalyze, which guarantees MCELEM has the correct element type.

I haven't found a way to either inject my custom array type or alter the
typanalyze to some custom function. So I think this is OK.

2) I'm not sure we can extend this regular data types / other slots

For example, I think I can implement a data type with custom typanalyze
function (and custom compute_stats function) that fills slots with some
other / strange stuff. For example I might build MCV with hashes of the
original data, a CountMin sketch, or something like that.

Yes, I don't think people do that often, but as long as the type also
implements custom selectivity functions for the operators, I think this
would work.

regards

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

#28Peter Smith
smithpb2250@gmail.com
In reply to: Corey Huinker (#10)
Re: Statistics Import and Export

2024-01 Commitfest.

Hi, This patch has a CF status of "Needs Review" [1]https://commitfest.postgresql.org/46/4538/, but it seems
there were CFbot test failures last time it was run [2]https://cirrus-ci.com/github/postgresql-cfbot/postgresql/commitfest/46/4538. 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.

#29Corey Huinker
corey.huinker@gmail.com
In reply to: Peter Smith (#28)
Re: Statistics Import and Export

On Mon, Jan 22, 2024 at 1:09 AM Peter Smith <smithpb2250@gmail.com> 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.

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.

#30Corey Huinker
corey.huinker@gmail.com
In reply to: Peter Smith (#28)
3 attachment(s)
Re: Statistics Import and Export

(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 <smithpb2250@gmail.com> wrote:

Show quoted text

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.

Attachments:

v4-0001-Create-pg_import_rel_stats.patchtext/x-patch; charset=US-ASCII; name=v4-0001-Create-pg_import_rel_stats.patchDownload
From 6bdc7076d20ff4cd71b851ea34f8ffa69fe03f67 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
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

v4-0002-This-is-the-extended-statistics-equivalent-of-pg_.patchtext/x-patch; charset=US-ASCII; name=v4-0002-This-is-the-extended-statistics-equivalent-of-pg_.patchDownload
From a9de415586cb1dd73c06c173100be9aa6ea47695 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
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

v4-0003-Add-pg_export_stats-pg_import_stats.patchtext/x-patch; charset=US-ASCII; name=v4-0003-Add-pg_export_stats-pg_import_stats.patchDownload
From 101c6476df3b7e9d4f199b4c4a149e1dfeebd51c Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
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

#31Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Corey Huinker (#30)
Re: Statistics Import and Export

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?

On 2/2/24 09:37, Corey Huinker wrote:

(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.

OK

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.

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.

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.

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

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.

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

#32Corey Huinker
corey.huinker@gmail.com
In reply to: Tomas Vondra (#31)
Re: Statistics Import and Export

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).

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.

- 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?

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.

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

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.

- 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?

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

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

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.

- 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 see no need to have them be a part of the public API. Will move.

- I'm not sure we do "\set debug f" elsewhere. It took me a while to
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.

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.

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.

- What happens if we import statistics which includes data for extended
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.

0003

- 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
like that, right?

Correct, for the reason above.

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.

Makes sense. What will happen if we try to import data for extended
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.

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.

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?

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.

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.

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?

#33Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#32)
2 attachment(s)
Re: Statistics Import and Export

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.

Attachments:

v5-0001-Create-pg_import_rel_stats.patchtext/x-patch; charset=US-ASCII; name=v5-0001-Create-pg_import_rel_stats.patchDownload
From 9adcc9735069edc14af05b28088725594e912c84 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 15 Feb 2024 03:11:40 -0500
Subject: [PATCH v5 1/2] 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           |    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

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 9c120fc2b7..0e48c08566 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..0c3867f918 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -127,4 +127,6 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												int nclauses);
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
+extern Datum pg_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
 
 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..25ae54d4e8
--- /dev/null
+++ b/src/backend/statistics/statistics.c
@@ -0,0 +1,1331 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 "statistics/statistics_internal.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
+ * get_attrinfo
+ */
+typedef struct {
+	Oid		typid;
+	int32	typmod;
+	Oid		collid;
+	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++)
+	{
+		Form_pg_attribute attr = TupleDescAttr(rel->rd_att, i);
+
+		/*
+		 * If this this attribute is an expression, pop an expression off
+		 * of the list. We need to do this even if the attribute is
+		 * dropped to pop a potential expression off the list.
+		 */
+		if (has_index_exprs && (rel->rd_index->indkey.values[i] == 0))
+		{
+			Node *index_expr = NULL;
+
+			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);
+			res[i].typid = exprType(index_expr);
+			res[i].typmod = 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(attr->attcollation))
+				res[i].collid = attr->attcollation;
+			else
+				res[i].collid = exprCollation(index_expr);
+		}
+		else
+		{
+			res[i].typid = attr->atttypid;
+			res[i].typmod = attr->atttypmod;
+			res[i].collid = attr->attcollation;
+		}
+
+		if (attr->attisdropped)
+			continue;
+
+		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(res[i].typid);
+		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;
+}
+
+/*
+ * 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' : ' ')
+
+/*
+ * Find any duplicate values in a jsonb object casted via SPI SQL
+ * into a single-key table.
+ *
+ * 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.
+ *
+ * Presently the only check we make is to ensure that no duplicate oid values
+ * exist in the expanded table.
+ *
+ * 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.
+ *
+ * Presently the only check we make is to ensure that no duplicate oid values
+ * exist in the expanded table.
+ *
+ * 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.
+ *
+ * Presently the only check we make is to ensure that no duplicate oid values
+ * exist in the expanded table.
+ *
+ * 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.
+ *
+ * Presently the only check we make is to ensure that no duplicate attnum
+ * values exist in the expanded table.
+ *
+ * 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.
+ *
+ * Presently the only check we make is to ensure that no duplicate combinations
+ * of (staattnum, stainherit) exist within the expanded table.
+ *
+ * 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.
+ */
+
+static HeapTuple *
+import_pg_statistics(Relation rel, Relation sd, int server_version_num,
+					 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,
+					 bool require_match_oids, int *ntuples)
+{
+
+#define PGS_NARGS 6
+
+	Oid			argtypes[PGS_NARGS] = {
+					JSONBOID, JSONBOID, JSONBOID, JSONBOID, JSONBOID, OIDOID };
+	Datum		args[PGS_NARGS] = {
+					types_datum, collations_datum, operators_datum,
+					attributes_datum, statistics_datum,
+					ObjectIdGetDatum(RelationGetRelid(rel)) };
+	char		argnulls[PGS_NARGS] = {
+					NULLARG(types_null), NULLARG(collations_null),
+					NULLARG(operators_null), NULLARG(attributes_null),
+					NULLARG(statistics_null), NULLARG(false) };
+
+	/*
+	 * 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],
+									datums[BQ_COLLATIONS], nulls[BQ_COLLATIONS],
+									datums[BQ_OPERATORS], nulls[BQ_OPERATORS],
+									datums[BQ_ATTRIBUTES], nulls[BQ_ATTRIBUTES],
+									datums[BQ_STATISTICS], nulls[BQ_STATISTICS],
+									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 cf3de80394..2be0a30d4d 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -28732,6 +28732,71 @@ 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>
+        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

v5-0002-Create-pg_import_ext_stats.patchtext/x-patch; charset=US-ASCII; name=v5-0002-Create-pg_import_ext_stats.patchDownload
From c0567d9927055e5de6fe5bef008931e7e1de9c42 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
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().

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       | 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(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0e48c08566..701ed3a2c9 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..841c3d8f55 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,10 +34,12 @@
 #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"
 #include "statistics/statistics.h"
+#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)
 	return !heap_attisnull(htup, attnum, NULL);
 }
 
+/*
+ * Create a single StatExtEntry from a fetched heap tuple
+ */
+static StatExtEntry *
+statext_create_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 +524,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 = statext_create_entry(htup);
 		result = lappend(result, entry);
 	}
 
@@ -2636,3 +2650,839 @@ make_build_data(Relation rel, StatExtEntry *stat, int numrows, HeapTuple *rows,
 
 	return result;
 }
+
+/*
+ * 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.
+ */
+static 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;
+}
+
+
+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 = statext_create_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 2be0a30d4d..6f41c7a292 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -28787,12 +28787,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

#34Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#33)
4 attachment(s)
Re: Statistics Import and Export

On Thu, Feb 15, 2024 at 4:09 AM Corey Huinker <corey.huinker@gmail.com>
wrote:

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.

v6 posted below.

Changes:

- Additional documentation about the overall process.
- Rewording of SGML docs.
- removed a fair number of columns from the transformation queries.
- enabled require_match_oids in extended statistics, but I'm having my
doubts about the value of that.
- moved stats extraction functions to an fe_utils file stats_export.c that
will be used by both pg_export_stats and pg_dump.
- pg_export_stats now generates SQL statements rather than a tsv, and has
boolean flags to set the validate and require_match_oids parameters in the
calls to pg_import_(rel|ext)_stats.
- pg_import_stats is gone, as importing can now be done with psql.

I'm hoping to get feedback on a few areas.

1. The checks for matching oids. On the one hand, in a binary upgrade
situation, we would of course want the oid of the relation to match what
was exported, as well as all of the atttypids of the attributes to match
the type ids exported, same for collations, etc. However, the binary
upgrade is the one place where there are absolutely no middle steps that
could have altered either the stats jsons or the source tables. Given that
and that oid simply will never match in any situation other than a binary
upgrade, it may be best to discard those checks.

2. The checks for relnames matching, and typenames of attributes matching
(they are already matched by name, so the column order can change without
the import missing a beat) seem so necessary that there shouldn't be an
option to enable/disable them. But if that's true, then the initial
relation parameter becomes somewhat unnecessary, and anyone using these
functions for tuning or FDW purposes could easily transform the JSON using
SQL to put in the proper relname.

3. The data integrity validation functions may belong in a separate
function rather than being a parameter on the existing import functions.

4. Lastly, pg_dump. Each relation object and extended statistics object
will have a statistics import statement. From my limited experience with
pg_dump, it seems like we would add an additional Stmt variable (statsStmt)
to the TOC entry for each object created, and the restore process would
check the value of --with-statistics and in cases where the statistics flag
was set AND a stats import statement exists, then execute that stats
statement immediately after the creation of the object. This assumes that
there is no case where additional attributes are added to a relation after
it's initial CREATE statement. Indexes are independent relations in this
regard.

Attachments:

v6-0001-Create-pg_import_rel_stats.patchtext/x-patch; charset=US-ASCII; name=v6-0001-Create-pg_import_rel_stats.patchDownload
From 3376153585154289878fde8fbcd1cd8bddad8c52 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Tue, 20 Feb 2024 01:00:43 -0500
Subject: [PATCH v6 1/4] 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           |    2 +
 src/include/statistics/statistics_internal.h  |   28 +
 src/backend/statistics/Makefile               |    3 +-
 src/backend/statistics/meson.build            |    1 +
 src/backend/statistics/statistics.c           | 1346 +++++++++++++++++
 .../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, 2479 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

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 9c120fc2b7..0e48c08566 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..0c3867f918 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -127,4 +127,6 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												int nclauses);
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
+extern Datum pg_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
 
 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..90d117c0d6
--- /dev/null
+++ b/src/backend/statistics/statistics.c
@@ -0,0 +1,1346 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 "statistics/statistics_internal.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
+ * get_attrinfo
+ */
+typedef struct {
+	Oid		typid;
+	int32	typmod;
+	Oid		collid;
+	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++)
+	{
+		Form_pg_attribute attr = TupleDescAttr(rel->rd_att, i);
+
+		/*
+		 * If this this attribute is an expression, pop an expression off
+		 * of the list. We need to do this even if the attribute is
+		 * dropped to pop a potential expression off the list.
+		 */
+		if (has_index_exprs && (rel->rd_index->indkey.values[i] == 0))
+		{
+			Node *index_expr = NULL;
+
+			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);
+			res[i].typid = exprType(index_expr);
+			res[i].typmod = 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(attr->attcollation))
+				res[i].collid = attr->attcollation;
+			else
+				res[i].collid = exprCollation(index_expr);
+		}
+		else
+		{
+			res[i].typid = attr->atttypid;
+			res[i].typmod = attr->atttypmod;
+			res[i].collid = attr->attcollation;
+		}
+
+		if (attr->attisdropped)
+			continue;
+
+		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(res[i].typid);
+		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;
+}
+
+/*
+ * 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' : ' ')
+
+/*
+ * Find any duplicate values in a jsonb object casted via SPI SQL
+ * into a single-key table.
+ *
+ * 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.
+ *
+ * Presently the only check we make is to ensure that no duplicate oid values
+ * exist in the expanded table.
+ *
+ * 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.
+ *
+ * Presently the only check we make is to ensure that no duplicate oid values
+ * exist in the expanded table.
+ *
+ * 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.
+ *
+ * Presently the only check we make is to ensure that no duplicate oid values
+ * exist in the expanded table.
+ *
+ * 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.
+ *
+ * Presently the only check we make is to ensure that no duplicate attnum
+ * values exist in the expanded table.
+ *
+ * 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.
+ *
+ * Presently the only check we make is to ensure that no duplicate combinations
+ * of (staattnum, stainherit) exist within the expanded table.
+ *
+ * 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 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.
+ */
+
+static HeapTuple *
+import_pg_statistics(Relation rel, Relation sd, int server_version_num,
+					 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,
+					 bool require_match_oids, int *ntuples)
+{
+
+#define PGS_NARGS 6
+
+	Oid			argtypes[PGS_NARGS] = {
+					JSONBOID, JSONBOID, JSONBOID, JSONBOID, JSONBOID, OIDOID };
+	Datum		args[PGS_NARGS] = {
+					types_datum, collations_datum, operators_datum,
+					attributes_datum, statistics_datum,
+					ObjectIdGetDatum(RelationGetRelid(rel)) };
+	char		argnulls[PGS_NARGS] = {
+					NULLARG(types_null), NULLARG(collations_null),
+					NULLARG(operators_null), NULLARG(attributes_null),
+					NULLARG(statistics_null), NULLARG(false) };
+
+	/*
+	 * 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, "
+		"       pgatn.nspname AS typschema, 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.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, "
+		"       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 "
+		"JOIN pg_namespace AS pgatn ON pgatn.oid = pgat.typnamespace "
+		"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_TYPSCHEMA,
+		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_OPRNAME1,
+		PGS_EXP_OPRNAME2,
+		PGS_EXP_OPRNAME3,
+		PGS_EXP_OPRNAME4,
+		PGS_EXP_OPRNAME5,
+		PGS_STAOP1,
+		PGS_STAOP2,
+		PGS_STAOP3,
+		PGS_STAOP4,
+		PGS_STAOP5,
+		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];
+
+		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]))
+		{
+			if (require_match_oids)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("No exported attribute with name \"%s\" found.", attname)));
+			/* Do not generate a tuple */
+			continue;
+		}
+		else if (DatumGetBool(pgs_datums[PGS_STAATTNUM_MISS]))
+		{
+			if (require_match_oids)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("No exported statistic found for exported attribute \"%s\" found.",
+								attname)));
+			/* Do not generate a tuple */
+			continue;
+		}
+
+		exported_attnum = DatumGetInt16(pgs_datums[PGS_EXP_ATTNUM]);
+
+		/* Validate that the data types either match by oid or by name */
+		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)));
+		}
+		else
+		{
+			char   *local_typename = NameStr(*(DatumGetName(pgs_datums[PGS_TYPNAME])));
+			char   *local_typeschema = NameStr(*(DatumGetName(pgs_datums[PGS_TYPSCHEMA])));
+
+			char   *imported_typename;
+			char   *imported_typeschema;
+					
+
+			if (pgs_nulls[PGS_EXP_TYPNAME])
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("Attribute %d has typname %s but imported typname is NULL",
+								attnum, local_typename)));
+
+			if (pgs_nulls[PGS_EXP_TYPSCHEMA])
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("Attribute %d has typname %s but imported type schema is NULL",
+								attnum, local_typename)));
+
+			imported_typename = TextDatumGetCString(pgs_datums[PGS_EXP_TYPNAME]);
+			imported_typeschema = TextDatumGetCString(pgs_datums[PGS_EXP_TYPSCHEMA]);
+
+			if ((strcmp(local_typename, imported_typename) != 0) ||
+			    (strcmp(local_typeschema, imported_typeschema) != 0))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("Attribute %d has type %s.%s but imported type is %s.%s",
+								attnum, local_typeschema, local_typename,
+								imported_typeschema, imported_typename)));
+
+			pfree(imported_typename);
+			pfree(imported_typeschema);
+		}
+
+		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.
+			 *
+			 * A comparison of operator names isn't interesting, as they're either
+			 * < or = deterministically chosen by the stakindN.
+			 */
+			/* 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 first parameter is the oid of the target relation for the statistics.
+ * The oid must be valid.
+ *
+ * The second parameter is the JSON object that contains the statistics to be
+ * imported, as well as information about types, collations, operators, and
+ * attributes associated with those statistics.
+ *
+ * This JSON object is decoded in stages. The first stage extracts the
+ * top-level keys but does not drill down further. Among those keys will
+ * be server_version_num, and the value of key determines which queries are
+ * used to reconstruct the statistics that will be inserted into pg_statistic.
+ *
+ * The actual decoding of statistics, and the catalog data that was exported
+ * alongside those statitics, happens in import_pg_statistics().
+
+ * 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],
+									datums[BQ_COLLATIONS], nulls[BQ_COLLATIONS],
+									datums[BQ_OPERATORS], nulls[BQ_OPERATORS],
+									datums[BQ_ATTRIBUTES], nulls[BQ_ATTRIBUTES],
+									datums[BQ_STATISTICS], nulls[BQ_STATISTICS],
+									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 cf3de80394..2be0a30d4d 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -28732,6 +28732,71 @@ 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>
+        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.2

v6-0002-Create-pg_import_ext_stats.patchtext/x-patch; charset=US-ASCII; name=v6-0002-Create-pg_import_ext_stats.patchDownload
From 3b58ce044849530868a726c2ab1b0124fca3a0a0 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Tue, 20 Feb 2024 01:12:22 -0500
Subject: [PATCH v6 2/4] Create pg_import_ext_stats().

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       | 1023 +++++++++++++++--
 src/backend/statistics/mcv.c                  |  192 ++++
 src/backend/statistics/mvdistinct.c           |  160 +++
 src/backend/statistics/statistics.c           |    3 +-
 .../regress/expected/stats_export_import.out  |  269 ++++-
 src/test/regress/sql/stats_export_import.sql  |  249 +++-
 doc/src/sgml/func.sgml                        |   28 +-
 10 files changed, 2018 insertions(+), 79 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0e48c08566..701ed3a2c9 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..4a86910200 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,10 +34,12 @@
 #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"
 #include "statistics/statistics.h"
+#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)
 	return !heap_attisnull(htup, attnum, NULL);
 }
 
+/*
+ * Create a single StatExtEntry from a fetched heap tuple
+ */
+static StatExtEntry *
+statext_create_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 +524,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 = statext_create_entry(htup);
 		result = lappend(result, entry);
 	}
 
@@ -2636,3 +2650,876 @@ make_build_data(Relation rel, StatExtEntry *stat, int numrows, HeapTuple *rows,
 
 	return result;
 }
+
+/*
+ * 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.
+ */
+static 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;
+}
+
+
+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 = statext_create_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 (require_match_oids)
+	{
+		Oid imported_stxid;
+		Oid imported_relid;
+
+		if (bq_nulls[BQ_STXOID])
+			ereport(ERROR,
+					(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+					 errmsg("Expecting to import stxoid %u but stxoid is NULL",
+							stxid)));
+
+		imported_stxid = DatumGetObjectId(bq_datums[BQ_STXOID]);
+		if (stxid != imported_stxid)
+			ereport(ERROR,
+					(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+					 errmsg("Expecting to import stxoid %u but found stxoid %u",
+							stxid, imported_stxid)));
+
+		if (bq_nulls[BQ_RELOID])
+			ereport(ERROR,
+					(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+					 errmsg("Expecting to import reloid %u but reloid is NULL",
+							RelationGetRelid(rel))));
+
+		imported_relid = DatumGetObjectId(bq_datums[BQ_RELOID]);
+		if (RelationGetRelid(rel) != imported_relid)
+			ereport(ERROR,
+					(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+					 errmsg("Expecting to import reloid %u but found reloid %u",
+							RelationGetRelid(rel), imported_relid)));
+	}
+
+	/*
+	 * TODO: compare object/relation/schema names?
+	 * NameStr(RelationGetRelationName(rel))
+	 */
+
+	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/backend/statistics/statistics.c b/src/backend/statistics/statistics.c
index 90d117c0d6..d8757040e1 100644
--- a/src/backend/statistics/statistics.c
+++ b/src/backend/statistics/statistics.c
@@ -767,7 +767,6 @@ import_pg_statistics(Relation rel, Relation sd, int server_version_num,
 
 			char   *imported_typename;
 			char   *imported_typeschema;
-					
 
 			if (pgs_nulls[PGS_EXP_TYPNAME])
 				ereport(ERROR,
@@ -785,7 +784,7 @@ import_pg_statistics(Relation rel, Relation sd, int server_version_num,
 			imported_typeschema = TextDatumGetCString(pgs_datums[PGS_EXP_TYPSCHEMA]);
 
 			if ((strcmp(local_typename, imported_typename) != 0) ||
-			    (strcmp(local_typeschema, imported_typeschema) != 0))
+					(strcmp(local_typeschema, imported_typeschema) != 0))
 				ereport(ERROR,
 						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 						 errmsg("Attribute %d has type %s.%s but imported type is %s.%s",
diff --git a/src/test/regress/expected/stats_export_import.out b/src/test/regress/expected/stats_export_import.out
index 5ab51c5aa0..8e6e04eb76 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
@@ -459,7 +648,7 @@ SELECT pg_import_rel_stats(
         'stats_export_import.test'::regclass,
         :'table_stats_json'::jsonb,
         true,
-        true);
+        false);
  pg_import_rel_stats 
 ---------------------
  t
@@ -469,12 +658,25 @@ SELECT pg_import_rel_stats(
         'stats_export_import.is_odd'::regclass,
         :'index_stats_json'::jsonb,
         true,
-        true);
+        false);
  pg_import_rel_stats 
 ---------------------
  t
 (1 row)
 
+SELECT pg_import_ext_stats(
+        e.oid,
+        :'ext_stats_json'::jsonb,
+        true,
+        false)
+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..85356bfd69 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(*)
@@ -448,13 +632,22 @@ SELECT pg_import_rel_stats(
         'stats_export_import.test'::regclass,
         :'table_stats_json'::jsonb,
         true,
-        true);
+        false);
 
 SELECT pg_import_rel_stats(
         'stats_export_import.is_odd'::regclass,
         :'index_stats_json'::jsonb,
         true,
-        true);
+        false);
+
+SELECT pg_import_ext_stats(
+        e.oid,
+        :'ext_stats_json'::jsonb,
+        true,
+        false)
+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,
@@ -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 2be0a30d4d..6f41c7a292 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -28787,12 +28787,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.2

v6-0003-Add-common-functions-for-exporting-statistics.patchtext/x-patch; charset=US-ASCII; name=v6-0003-Add-common-functions-for-exporting-statistics.patchDownload
From 78c35f045bb3194bf03b8236b2ac951e4082e2a1 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 17 Feb 2024 14:20:55 -0500
Subject: [PATCH v6 3/4] Add common functions for exporting statistics.

Creates functions that aid the caller in generating the
version-appropriate function for extracting pg_statistic and
pg_statistic_ext data, and forming the SQL command to import those same
statistics.

These functions will be used by pg_export_stats and pg_dump.
---
 src/include/fe_utils/stats_export.h |  35 ++
 src/fe_utils/Makefile               |   1 +
 src/fe_utils/meson.build            |   1 +
 src/fe_utils/stats_export.c         | 880 ++++++++++++++++++++++++++++
 4 files changed, 917 insertions(+)
 create mode 100644 src/include/fe_utils/stats_export.h
 create mode 100644 src/fe_utils/stats_export.c

diff --git a/src/include/fe_utils/stats_export.h b/src/include/fe_utils/stats_export.h
new file mode 100644
index 0000000000..4f80d110cf
--- /dev/null
+++ b/src/include/fe_utils/stats_export.h
@@ -0,0 +1,35 @@
+/*-------------------------------------------------------------------------
+ *
+ * stats_export.h
+ *    Queries to export statistics from current and past versions.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1995, Regents of the University of California
+ *
+ * src/include/varatt.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef STATS_EXPORT_H
+#define STATS_EXPORT_H
+
+extern const char *stats_export_rel_query(int server_version_num);
+extern const char *stats_export_ext_query(int server_version_num);
+
+extern void stats_export_print_rel_import(FILE *outf,
+										  const char *nspname_literal,
+										  const char *relname_literal,
+										  const char *stats_json_literal,
+										  bool validate,
+										  bool require_match);
+extern void stats_export_print_ext_import(FILE *outf,
+										  const char *nspname_literal,
+										  const char *relname_literal,
+										  const char *stxname_literal,
+										  const char *ext_stats_json_literal,
+										  bool validate,
+										  bool require_match);
+
+#endif
diff --git a/src/fe_utils/Makefile b/src/fe_utils/Makefile
index 946c05258f..c734f9f6d3 100644
--- a/src/fe_utils/Makefile
+++ b/src/fe_utils/Makefile
@@ -32,6 +32,7 @@ OBJS = \
 	query_utils.o \
 	recovery_gen.o \
 	simple_list.o \
+	stats_export.o \
 	string_utils.o
 
 ifeq ($(PORTNAME), win32)
diff --git a/src/fe_utils/meson.build b/src/fe_utils/meson.build
index 14d0482a2c..fce503f641 100644
--- a/src/fe_utils/meson.build
+++ b/src/fe_utils/meson.build
@@ -12,6 +12,7 @@ fe_utils_sources = files(
   'query_utils.c',
   'recovery_gen.c',
   'simple_list.c',
+  'stats_export.c',
   'string_utils.c',
 )
 
diff --git a/src/fe_utils/stats_export.c b/src/fe_utils/stats_export.c
new file mode 100644
index 0000000000..1815793c9d
--- /dev/null
+++ b/src/fe_utils/stats_export.c
@@ -0,0 +1,880 @@
+/*-------------------------------------------------------------------------
+ *
+ * Utility functions for extracting object statistics for frontend code
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/fe_utils/stats_export.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include "fe_utils/stats_export.h"
+
+/*
+ * The following are query fragments that build one or more JSONB objects
+ * in the format expected by pg_import_rel_stats(). They cannot be used
+ * unmodified, as they are missing the SELECT clause. In addition, they are
+ * missing a WHERE clause to filter results. The expectation is that the
+ * the caller will do one of the following:
+ *   - Prepend "SELECT stats_json " and append a WHERE clause to the query
+ *     that identifies a single object.
+ *   - Similar to the previous option, but make it a correlated subquery in
+ *     a larger query.
+ *   - Prepend a SELECT list to the query and append a WHERE clause that
+ *     refines the objects selected.
+ */
+
+/*
+ * pg_statitic on versions 12+ have the same rel stats layout
+ */
+const char *stats_export_rel_query_v12 =
+	"    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_catalog.pg_type AS t "
+	"                JOIN pg_catalog.pg_namespace AS n ON n.oid = t.typnamespace "
+	"                WHERE t.oid IN ( "
+	"                    SELECT a.atttypid "
+	"                    FROM pg_catalog.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_catalog.pg_collation AS c "
+	"                JOIN pg_catalog.pg_namespace AS n ON n.oid = c.collnamespace "
+	"                WHERE c.oid IN ( "
+	"                    SELECT a.attcollation AS oid "
+	"                    FROM pg_catalog.pg_attribute AS a "
+	"                    WHERE a.attrelid = r.oid "
+	"                    AND NOT a.attisdropped "
+	"                    AND a.attnum > 0 "
+	"                    UNION "
+	"                    SELECT u.collid "
+	"                    FROM pg_catalog.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_catalog.pg_operator AS o "
+	"                JOIN pg_catalog.pg_namespace AS n ON n.oid = o.oprnamespace "
+	"                WHERE o.oid IN ( "
+	"                    SELECT u.oid "
+	"                    FROM pg_catalog.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_catalog.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_catalog.pg_statistic AS s "
+	"                WHERE s.starelid = r.oid "
+	"            ) AS sr "
+	"        ) "
+	"    ) AS stats_json "
+	"FROM pg_catalog.pg_class AS r "
+	"JOIN pg_catalog.pg_namespace AS n ON n.oid = r.relnamespace ";
+
+/*
+ * pg_statitic on versions 10-11 are missing the pg_statistic.stacollN columns
+ */
+const char *stats_export_rel_query_v10 =
+	"    jsonb_build_object( "
+	"        'types', "
+	"        ( "
+	"            SELECT array_agg(tr ORDER BY tr.oid) "
+	"            FROM ( "
+	"                SELECT "
+	"                    t.oid, "
+	"                    t.typname, "
+	"                    n.nspname "
+	"                FROM pg_catalog.pg_type AS t "
+	"                JOIN pg_catalog.pg_namespace AS n ON n.oid = t.typnamespace "
+	"                WHERE t.oid IN ( "
+	"                    SELECT a.atttypid "
+	"                    FROM pg_catalog.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_catalog.pg_collation AS c "
+	"                JOIN pg_catalog.pg_namespace AS n ON n.oid = c.collnamespace "
+	"                WHERE c.oid IN ( "
+	"                    SELECT a.attcollation AS oid "
+	"                    FROM pg_catalog.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_catalog.pg_operator AS o "
+	"                JOIN pg_catalog.pg_namespace AS n ON n.oid = o.oprnamespace "
+	"                WHERE o.oid IN ( "
+	"                    SELECT u.oid "
+	"                    FROM pg_catalog.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_catalog.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_catalog.pg_statistic AS s "
+	"                WHERE s.starelid = r.oid "
+	"            ) AS sr "
+	"        ) "
+	"    ) AS stats_json "
+	"FROM pg_catalog.pg_class AS r "
+	"JOIN pg_catalog.pg_namespace AS n ON n.oid = r.relnamespace ";
+
+/*
+ * The following are query fragments that build one or more JSONB objects
+ * in the format expected by pg_import_rel_stats(), and it is expected that
+ * they will be utilized in the same ways.
+ */
+
+const char *stats_export_ext_query_v15 =
+/* v15+ have the same format */
+	"    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_catalog.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_catalog.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_catalog.pg_type AS t "
+	"                JOIN pg_catalog.pg_namespace AS n ON n.oid = t.typnamespace "
+	"                WHERE t.oid IN ( "
+	"                    SELECT a.atttypid "
+	"                    FROM pg_catalog.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_catalog.pg_collation AS c "
+	"                JOIN pg_catalog.pg_namespace AS n ON n.oid = c.collnamespace "
+	"                WHERE c.oid IN ( "
+	"                    SELECT a.attcollation AS oid "
+	"                    FROM pg_catalog.pg_attribute AS a "
+	"                    WHERE a.attrelid = r.oid "
+	"                    AND NOT a.attisdropped "
+	"                    AND a.attnum > 0 "
+	"                    UNION "
+	"                    SELECT u.collid "
+	"                    FROM pg_catalog.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_catalog.pg_operator AS o "
+	"                JOIN pg_catalog.pg_namespace AS n ON n.oid = o.oprnamespace "
+	"                WHERE o.oid IN ( "
+	"                    SELECT u.opid "
+	"                    FROM pg_catalog.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_catalog.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_catalog.pg_class r "
+	"JOIN pg_catalog.pg_statistic_ext AS e ON e.stxrelid = r.oid "
+	"JOIN pg_catalog.pg_namespace AS en ON en.oid = e.stxnamespace "
+	"JOIN pg_catalog.pg_namespace AS n ON n.oid = r.relnamespace ";
+
+/* v14 is like v15, but lacks stxdinherit on pg_statistic_ext_data */
+const char *stats_export_ext_query_v14 =
+	"    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_catalog.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_catalog.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_catalog.pg_type as t "
+	"                join pg_catalog.pg_namespace as n on n.oid = t.typnamespace "
+	"                where t.oid in ( "
+	"                    select a.atttypid "
+	"                    from pg_catalog.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_catalog.pg_collation AS c "
+	"                JOIN pg_catalog.pg_namespace AS n ON n.oid = c.collnamespace "
+	"                WHERE c.oid IN ( "
+	"                    SELECT a.attcollation AS oid "
+	"                    FROM pg_catalog.pg_attribute AS a "
+	"                    WHERE a.attrelid = r.oid "
+	"                    AND NOT a.attisdropped "
+	"                    AND a.attnum > 0 "
+	"                    UNION "
+	"                    SELECT u.collid "
+	"                    FROM pg_catalog.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_catalog.pg_operator AS o "
+	"                JOIN pg_catalog.pg_namespace AS n ON n.oid = o.oprnamespace "
+	"                WHERE o.oid IN ( "
+	"                    SELECT u.opid "
+	"                    FROM pg_catalog.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_catalog.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_catalog.pg_class r "
+	"JOIN pg_catalog.pg_statistic_ext AS e ON e.stxrelid = r.oid "
+	"JOIN pg_catalog.pg_namespace AS en ON en.oid = e.stxnamespace "
+	"JOIN pg_catalog.pg_namespace AS n ON n.oid = r.relnamespace ";
+
+/* v12-v13 are like v14, but lack stxdexpr on pg_statistic_ext_data */
+const char *stats_export_ext_query_v12 =
+	"    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_catalog.pg_mcv_list_items(sd.stxdmcv) AS mcvl "
+	"                        WHERE sd.stxdmcv IS NOT NULL "
+	"                    ) AS stxdmcv "
+	"                FROM pg_catalog.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_catalog.pg_type AS t "
+	"                JOIN pg_catalog.pg_namespace AS n ON n.oid = t.typnamespace "
+	"                WHERE t.oid IN ( "
+	"                    SELECT a.atttypid "
+	"                    FROM pg_catalog.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_catalog.pg_collation AS c "
+	"                JOIN pg_catalog.pg_namespace AS n ON n.oid = c.collnamespace "
+	"                WHERE c.oid IN ( "
+	"                    SELECT a.attcollation AS oid "
+	"                    FROM pg_catalog.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_catalog.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_catalog.pg_class r "
+	"JOIN pg_catalog.pg_statistic_ext AS e ON e.stxrelid = r.oid "
+	"JOIN pg_catalog.pg_namespace AS en ON en.oid = e.stxnamespace "
+	"JOIN pg_catalog.pg_namespace AS n ON n.oid = r.relnamespace ";
+
+/*
+ * 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 *stats_export_ext_query_v10 =
+	"    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_catalog.pg_type AS t "
+	"                JOIN pg_catalog.pg_namespace AS n ON n.oid = t.typnamespace "
+	"                WHERE t.oid IN ( "
+	"                    SELECT a.atttypid "
+	"                    FROM pg_catalog.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_catalog.pg_collation AS c "
+	"                JOIN pg_catalog.pg_namespace AS n ON n.oid = c.collnamespace "
+	"                WHERE c.oid IN ( "
+	"                    SELECT a.attcollation AS oid "
+	"                    FROM pg_catalog.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_catalog.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_catalog.pg_class r "
+	"JOIN pg_catalog.pg_statistic_ext AS e ON e.stxrelid = r.oid "
+	"JOIN pg_catalog.pg_namespace AS en ON en.oid = e.stxnamespace "
+	"JOIN pg_catalog.pg_namespace AS n ON n.oid = r.relnamespace ";
+
+/*
+ * Return the pg_statitic export query fragment appropriate for the
+ * given server_version_num.
+ */
+const char *
+stats_export_rel_query(int server_version_num)
+{
+	if (server_version_num >= 120000)
+		return stats_export_rel_query_v12;
+	else if (server_version_num >= 100000)
+		return stats_export_rel_query_v10;
+	else
+		return NULL;
+}
+
+/*
+ * Return the pg_statitic_ext export query fragment appropriate for the
+ * given server_version_num.
+ */
+const char *
+stats_export_ext_query(int server_version_num)
+{
+	if (server_version_num >= 150000)
+		return stats_export_ext_query_v15;
+	else if (server_version_num >= 140000)
+		return stats_export_ext_query_v14;
+	else if (server_version_num >= 120000)
+		return stats_export_ext_query_v12;
+	else if (server_version_num >= 100000)
+		return stats_export_ext_query_v10;
+	else
+		return NULL;
+}
+
+/*
+ * Names cannot be null, must be regular strings
+ */
+#define is_quoted_name(s) \
+	(s != NULL) && \
+	((s[0] == '\'')) && \
+	(s[strlen(s)-1] == '\'')
+
+/*
+ * JSON strings can be NULL, and may extended format E'...' strings.
+ */
+#define is_quoted_json(s) \
+	(s == NULL) || \
+	(((s[0] == '\'') || ((s[0] == 'E' && s[1] == '\''))) && \
+	 (s[strlen(s)-1] == '\''))
+
+/*
+ * Print the SQL statement to import statistics to a given relation.
+ */
+void
+stats_export_print_rel_import(FILE *outf,
+							  const char *nspname_literal,
+							  const char *relname_literal,
+							  const char *stats_json_literal,
+							  bool validate,
+							  bool require_match)
+{
+	Assert(is_quoted_name(nspname_literal));
+	Assert(is_quoted_name(relname_literal));
+	Assert(is_quoted_json(stats_json_literal));
+
+	fprintf(outf,
+			"SELECT pg_catalog.pg_import_rel_stats(r.oid, %s::jsonb, %s, %s) "
+			"FROM pg_catalog.pg_catalog.pg_class AS r "
+			"JOIN pg_catalog.pg_namespace AS n ON n.oid = r.relnamespace "
+			"WHERE n.nspname = %s "
+			"AND r.relname = %s;\n\n",
+			(stats_json_literal != NULL) ? stats_json_literal : "NULL",
+			(validate) ? "true" : "false",
+			(require_match) ? "true" : "false",
+			nspname_literal,
+			relname_literal);
+}
+
+/*
+ * Print the SQL statement to import statistics to a given statistics object.
+ */
+void
+stats_export_print_ext_import(FILE *outf,
+							  const char *nspname_literal,
+							  const char *relname_literal,
+							  const char *stxname_literal,
+							  const char *ext_stats_json_literal,
+							  bool validate,
+							  bool require_match)
+{
+	Assert(is_quoted_name(nspname_literal));
+	Assert(is_quoted_name(relname_literal));
+	Assert(is_quoted_name(stxname_literal));
+	Assert(is_quoted_json(ext_stats_json_literal));
+
+	fprintf(outf,
+			"SELECT pg_catalog.pg_import_ext_stats(e.oid, %s::jsonb, %s, %s) "
+			"FROM pg_catalog.pg_class r "
+			"JOIN pg_catalog.pg_statistic_ext AS e ON e.stxrelid = r.oid "
+			"JOIN pg_catalog.pg_namespace AS n ON n.oid = r.relnamespace "
+			"WHERE n.nspname = %s "
+			"AND r.relname = %s "
+			"AND e.stxname = %s;\n\n",
+			(ext_stats_json_literal != NULL) ? ext_stats_json_literal : "NULL",
+			(validate) ? "true" : "false",
+			(require_match) ? "true" : "false",
+			nspname_literal,
+			relname_literal,
+			stxname_literal);
+}
-- 
2.43.2

v6-0004-Add-pg_export_stats.patchtext/x-patch; charset=US-ASCII; name=v6-0004-Add-pg_export_stats.patchDownload
From fa020383ac1811fb02997b53f0551a8d3ae26329 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Tue, 20 Feb 2024 01:14:54 -0500
Subject: [PATCH v6 4/4] Add pg_export_stats.

This is a command-line utility that connects to a database and exports
statistics from all user relations and user statistics objects, printing
SQL statements designed to re-import those statistics into a like-named
object.
---
 src/fe_utils/stats_export.c       |   2 +-
 src/bin/scripts/.gitignore        |   1 +
 src/bin/scripts/Makefile          |   3 +-
 src/bin/scripts/pg_export_stats.c | 267 ++++++++++++++++++++++++++++++
 4 files changed, 271 insertions(+), 2 deletions(-)
 create mode 100644 src/bin/scripts/pg_export_stats.c

diff --git a/src/fe_utils/stats_export.c b/src/fe_utils/stats_export.c
index 1815793c9d..09b4eb240d 100644
--- a/src/fe_utils/stats_export.c
+++ b/src/fe_utils/stats_export.c
@@ -835,7 +835,7 @@ stats_export_print_rel_import(FILE *outf,
 
 	fprintf(outf,
 			"SELECT pg_catalog.pg_import_rel_stats(r.oid, %s::jsonb, %s, %s) "
-			"FROM pg_catalog.pg_catalog.pg_class AS r "
+			"FROM pg_catalog.pg_class AS r "
 			"JOIN pg_catalog.pg_namespace AS n ON n.oid = r.relnamespace "
 			"WHERE n.nspname = %s "
 			"AND r.relname = %s;\n\n",
diff --git a/src/bin/scripts/.gitignore b/src/bin/scripts/.gitignore
index 0f23fe0004..76704a3ad4 100644
--- a/src/bin/scripts/.gitignore
+++ b/src/bin/scripts/.gitignore
@@ -6,5 +6,6 @@
 /reindexdb
 /vacuumdb
 /pg_isready
+/pg_export_stats
 
 /tmp_check/
diff --git a/src/bin/scripts/Makefile b/src/bin/scripts/Makefile
index 9633c99136..dd54faaf2a 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
 
 override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS)
 LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils $(libpq_pgport)
@@ -41,6 +41,7 @@ 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)
 
 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..7fce1ef515
--- /dev/null
+++ b/src/bin/scripts/pg_export_stats.c
@@ -0,0 +1,267 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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/stats_export.h"
+#include "fe_utils/string_utils.h"
+
+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'},
+		{"echo", no_argument, NULL, 'e'},
+		{"dbname", required_argument, NULL, 'd'},
+		{"validate", no_argument, NULL, 'v'},
+		{"require-match", no_argument, NULL, 'm'},
+		{NULL, 0, NULL, 0}
+	};
+
+	const char *progname;
+	int			optindex;
+	int			c;
+
+	bool		validate = false;
+	bool		require_match = false;
+	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;
+
+	PGresult   *result;
+
+	ExecStatusType result_status;
+
+	pg_logging_init(argv[0]);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pgscripts"));
+
+	handle_help_version_opts(argc, argv, "pg_export_stats", help);
+
+	while ((c = getopt_long(argc, argv, "d:eh:mp:U:vwW", 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 'm':
+				require_match = true;
+				break;
+			case 'p':
+				port = pg_strdup(optarg);
+				break;
+			case 'U':
+				username = pg_strdup(optarg);
+				break;
+			case 'v':
+				validate = true;
+				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);
+
+	/*
+	 * Query catalog for relations, export statistics from each of them,
+	 * and generate a SQL statement to re-import those statistics.
+	 */
+	appendPQExpBufferStr(&sql,
+		"SELECT quote_literal(b.nspname), quote_literal(b.relname), "
+		"       quote_literal(b.stats_json) "
+		"FROM ( SELECT n.nspname, r.relname, ");
+
+	appendPQExpBufferStr(&sql, stats_export_rel_query(server_version_num));
+
+	appendPQExpBufferStr(&sql,
+		"WHERE r.relkind IN ('r', 'm', 'f', 'p', 'i') "
+		"AND r.relpersistence = 'p' "
+		"AND n.nspname NOT IN ('pg_catalog', 'pg_toast', 'information_schema') "
+		"ORDER BY n.nspname, r.relname"
+		") AS b");
+
+	if (echo)
+		printf("%s\n", sql.data);
+
+	result = PQexec(conn, sql.data);
+	result_status = PQresultStatus(result);
+
+	if (result_status != PGRES_TUPLES_OK)
+		pg_fatal("malformed catalog query: %s", PQerrorMessage(conn));
+	else
+	{
+		int nrows = PQntuples(result);
+		int i;
+
+		for (i = 0; i < nrows; i++)
+			stats_export_print_rel_import(stdout,
+										  PQgetvalue(result, i, 0),
+										  PQgetvalue(result, i, 1),
+										  PQgetvalue(result, i, 2),
+										  validate,
+										  require_match);
+	}
+
+	PQclear(result);
+	resetPQExpBuffer(&sql);
+
+	/*
+	 * Query catalog for statistics objects, export statistics from each of
+	 * them, and generate a SQL statement to re-import those statistics.
+	 */
+	appendPQExpBufferStr(&sql,
+		"SELECT quote_literal(b.nspname), quote_literal(b.relname), "
+		"       quote_literal(b.stxname), quote_literal(b.ext_stats_json) "
+		"FROM ( SELECT n.nspname, r.relname, e.stxname, ");
+
+	appendPQExpBufferStr(&sql, stats_export_ext_query(server_version_num));
+
+	appendPQExpBufferStr(&sql,
+		"WHERE r.relkind IN ('r', 'm', 'f', 'p', 'i') "
+		"AND r.relpersistence = 'p' "
+		"AND n.nspname NOT IN ('pg_catalog', 'pg_toast', 'information_schema') "
+		"ORDER BY n.nspname, r.relname, e.stxname "
+		") AS b ");
+
+	if (echo)
+		printf("%s\n", sql.data);
+
+	result = PQexec(conn, sql.data);
+	result_status = PQresultStatus(result);
+
+	if (result_status != PGRES_TUPLES_OK)
+		pg_fatal("malformed catalog query: %s", PQerrorMessage(conn));
+	else
+	{
+		int nrows = PQntuples(result);
+		int i;
+
+		for (i = 0; i < nrows; i++)
+			stats_export_print_ext_import(stdout,
+										  PQgetvalue(result, i, 0),
+										  PQgetvalue(result, i, 1),
+										  PQgetvalue(result, i, 2),
+										  PQgetvalue(result, i, 3),
+										  validate,
+										  require_match);
+	}
+
+	PQclear(result);
+
+	PQfinish(conn);
+	termPQExpBuffer(&sql);
+	exit(0);
+}
+
+
+static void
+help(const char *progname)
+{
+	printf(_("%s export statistics for all objects in a database.\n\n"), progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]... [DBNAME]\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_("  -d, --dbname=DBNAME       database to export\n"));
+	printf(_("  -e, --echo                show the commands being sent to the server\n"));
+	printf(_("  -V, --version             output version information, then exit\n"));
+	printf(_("  -?, --help                show this help, then exit\n"));
+	printf(_("  -v, --validate            Import calls generated should set the validate flag\n"));
+	printf(_("  -m, --require-match       Import calls generated should set the require_match flag\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(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
-- 
2.43.2

#35Stephen Frost
sfrost@snowman.net
In reply to: Corey Huinker (#34)
Re: Statistics Import and Export

Greetings,

* Corey Huinker (corey.huinker@gmail.com) wrote:

On Thu, Feb 15, 2024 at 4:09 AM Corey Huinker <corey.huinker@gmail.com>
wrote:

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.

v6 posted below.

Changes:

- Additional documentation about the overall process.
- Rewording of SGML docs.
- removed a fair number of columns from the transformation queries.
- enabled require_match_oids in extended statistics, but I'm having my
doubts about the value of that.
- moved stats extraction functions to an fe_utils file stats_export.c that
will be used by both pg_export_stats and pg_dump.
- pg_export_stats now generates SQL statements rather than a tsv, and has
boolean flags to set the validate and require_match_oids parameters in the
calls to pg_import_(rel|ext)_stats.
- pg_import_stats is gone, as importing can now be done with psql.

Having looked through this thread and discussed a bit with Corey
off-line, the approach that Tom laid out up-thread seems like it would
make the most sense overall- that is, eliminate the JSON bits and the
SPI and instead export the stats data by running queries from the new
version of pg_dump/server (in the FDW case) against the old server
with the intelligence of how to transform the data into the format
needed for the current pg_dump/server to accept, through function calls
where the function calls generally map up to the rows/information being
updated- a call to update the information in pg_class for each relation
and then a call for each attribute to update the information in
pg_statistic.

Part of this process would include mapping from OIDs/attrnum's to names
on the source side and then from those names to the appropriate
OIDs/attrnum's on the destination side.

As this code would be used by both pg_dump and the postgres_fdw, it
seems logical that it would go into the common library. Further, it
would make sense to have this code be able to handle multiple major
versions for the foreign side, such as how postgres_fdw and pg_dump
already do.

In terms of working to ensure that newer versions support loading from
older dumps (that is, that v18 would be able to load a dump file created
by a v17 pg_dump against a v17 server in the face of changes having been
made to the statistics system in v18), we could have the functions take
a version parameter (to handle cases where the data structure is the
same but the contents have to be handled differently), use overloaded
functions, or have version-specific names for the functions. I'm also
generally supportive of the idea that we, perhaps initially, only
support dumping/loading stats with pg_dump when in binary-upgrade mode,
which removes our need to be concerned with this (perhaps that would be
a good v1 of this feature?) as the version of pg_dump needs to match
that of pg_upgrade and the destination server for various other reasons.
Including a switch to exclude stats on restore might also be an
acceptable answer, or even simply excluding them by default when going
between major versions except in binary-upgrade mode.

Along those same lines when it comes to a 'v1', I'd say that we may wish
to consider excluding extended statistics, which I am fairly confident
Corey's heard a number of times previously already but thought I would
add my own support for that. To the extent that we do want to make
extended stats work down the road, we should probably have some
pre-patches to flush out the missing _in/_recv functions for those types
which don't have them today- and that would include modifying the _out
of those types to use names instead of OIDs/attrnums. In thinking about
this, I was reviewing specifically pg_dependencies. To the extent that
there are people who depend on the current output, I would think that
they'd actually appreciate this change.

I don't generally feel like we need to be checking that the OIDs between
the old server and the new server match- I appreciate that that should
be the case in a binary-upgrade situation but it still feels unnecessary
and complicated and clutters up the output and the function calls.

Overall, I definitely think this is a good project to work on as it's an
often, rightfully, complained about issue when it comes to pg_upgrade
and the amount of downtime required for it before the upgraded system
can be reasonably used again.

Thanks,

Stephen

#36Corey Huinker
corey.huinker@gmail.com
In reply to: Stephen Frost (#35)
Re: Statistics Import and Export

Having looked through this thread and discussed a bit with Corey
off-line, the approach that Tom laid out up-thread seems like it would
make the most sense overall- that is, eliminate the JSON bits and the
SPI and instead export the stats data by running queries from the new
version of pg_dump/server (in the FDW case) against the old server
with the intelligence of how to transform the data into the format
needed for the current pg_dump/server to accept, through function calls
where the function calls generally map up to the rows/information being
updated- a call to update the information in pg_class for each relation
and then a call for each attribute to update the information in
pg_statistic.

Thanks for the excellent summary of our conversation, though I do add that
we discussed a problem with per-attribute functions: each function would be
acquiring locks on both the relation (so it doesn't go away) and
pg_statistic, and that lock thrashing would add up. Whether that overhead
is judged significant or not is up for discussion. If it is significant, it
makes sense to package up all the attributes into one call, passing in an
array of some new pg_statistic-esque special type....the very issue that
sent me down the JSON path.

I certainly see the flexibility in having a per-attribute functions, but am
concerned about non-binary-upgrade situations where the attnums won't line
up, and if we're passing them by name then the function has dig around
looking for the right matching attnum, and that's overhead too. In the
whole-table approach, we just iterate over the attributes that exist, and
find the matching parameter row.

#37Stephen Frost
sfrost@snowman.net
In reply to: Corey Huinker (#36)
Re: Statistics Import and Export

Greetings,

On Thu, Feb 29, 2024 at 17:48 Corey Huinker <corey.huinker@gmail.com> wrote:

Having looked through this thread and discussed a bit with Corey

off-line, the approach that Tom laid out up-thread seems like it would
make the most sense overall- that is, eliminate the JSON bits and the
SPI and instead export the stats data by running queries from the new
version of pg_dump/server (in the FDW case) against the old server
with the intelligence of how to transform the data into the format
needed for the current pg_dump/server to accept, through function calls
where the function calls generally map up to the rows/information being
updated- a call to update the information in pg_class for each relation
and then a call for each attribute to update the information in
pg_statistic.

Thanks for the excellent summary of our conversation, though I do add that
we discussed a problem with per-attribute functions: each function would be
acquiring locks on both the relation (so it doesn't go away) and
pg_statistic, and that lock thrashing would add up. Whether that overhead
is judged significant or not is up for discussion. If it is significant, it
makes sense to package up all the attributes into one call, passing in an
array of some new pg_statistic-esque special type....the very issue that
sent me down the JSON path.

I certainly see the flexibility in having a per-attribute functions, but
am concerned about non-binary-upgrade situations where the attnums won't
line up, and if we're passing them by name then the function has dig around
looking for the right matching attnum, and that's overhead too. In the
whole-table approach, we just iterate over the attributes that exist, and
find the matching parameter row.

That’s certainly a fair point and my initial reaction (which could
certainly be wrong) is that it’s unlikely to be an issue- but also, if you
feel you could make it work with an array and passing all the attribute
info in with one call, which I suspect would be possible but just a bit
more complex to build, then sure, go for it. If it ends up being overly
unwieldy then perhaps the per-attribute call would be better and we could
perhaps acquire the lock before the function calls..? Doing a check to see
if we have already locked it would be cheaper than trying to acquire a new
lock, I’m fairly sure.

Also per our prior discussion- this makes sense to include in post-data
section, imv, and also because then we have the indexes we may wish to load
stats for, but further that also means it’ll be in the paralleliziable part
of the process, making me a bit less concerned overall about the individual
timing.

Thanks!

Stephen

Show quoted text
#38Corey Huinker
corey.huinker@gmail.com
In reply to: Stephen Frost (#37)
Re: Statistics Import and Export

That’s certainly a fair point and my initial reaction (which could
certainly be wrong) is that it’s unlikely to be an issue- but also, if you
feel you could make it work with an array and passing all the attribute
info in with one call, which I suspect would be possible but just a bit
more complex to build, then sure, go for it. If it ends up being overly
unwieldy then perhaps the per-attribute call would be better and we could
perhaps acquire the lock before the function calls..? Doing a check to see
if we have already locked it would be cheaper than trying to acquire a new
lock, I’m fairly sure.

Well the do_analyze() code was already ok with acquiring the lock once for
non-inherited stats and again for inherited stats, so the locks were
already not the end of the world. However, that's at most a 2x of the
locking required, and this would natts * x, quite a bit more. Having the
procedures check for a pre-existing lock seems like a good compromise.

Also per our prior discussion- this makes sense to include in post-data
section, imv, and also because then we have the indexes we may wish to load
stats for, but further that also means it’ll be in the paralleliziable part
of the process, making me a bit less concerned overall about the individual
timing.

The ability to parallelize is pretty persuasive. But is that per-statement
parallelization or do we get transaction blocks? i.e. if we ended up
importing stats like this:

BEGIN;
LOCK TABLE schema.relation IN SHARE UPDATE EXCLUSIVE MODE;
LOCK TABLE pg_catalog.pg_statistic IN ROW UPDATE EXCLUSIVE MODE;
SELECT pg_import_rel_stats('schema.relation', ntuples, npages);
SELECT pg_import_pg_statistic('schema.relation', 'id', ...);
SELECT pg_import_pg_statistic('schema.relation', 'name', ...);
SELECT pg_import_pg_statistic('schema.relation', 'description', ...);
...
COMMIT;

#39Nathan Bossart
nathandbossart@gmail.com
In reply to: Corey Huinker (#38)
Re: Statistics Import and Export

On Thu, Feb 29, 2024 at 10:55:20PM -0500, Corey Huinker wrote:

That’s certainly a fair point and my initial reaction (which could
certainly be wrong) is that it’s unlikely to be an issue- but also, if you
feel you could make it work with an array and passing all the attribute
info in with one call, which I suspect would be possible but just a bit
more complex to build, then sure, go for it. If it ends up being overly
unwieldy then perhaps the per-attribute call would be better and we could
perhaps acquire the lock before the function calls..? Doing a check to see
if we have already locked it would be cheaper than trying to acquire a new
lock, I’m fairly sure.

Well the do_analyze() code was already ok with acquiring the lock once for
non-inherited stats and again for inherited stats, so the locks were
already not the end of the world. However, that's at most a 2x of the
locking required, and this would natts * x, quite a bit more. Having the
procedures check for a pre-existing lock seems like a good compromise.

I think this is a reasonable starting point. If the benchmarks show that
the locking is a problem, we can reevaluate, but otherwise IMHO we should
try to keep it as simple/flexible as possible.

--
Nathan Bossart
Amazon Web Services: https://aws.amazon.com

#40Stephen Frost
sfrost@snowman.net
In reply to: Nathan Bossart (#39)
Re: Statistics Import and Export

Greetings,

On Fri, Mar 1, 2024 at 12:14 Nathan Bossart <nathandbossart@gmail.com>
wrote:

On Thu, Feb 29, 2024 at 10:55:20PM -0500, Corey Huinker wrote:

That’s certainly a fair point and my initial reaction (which could
certainly be wrong) is that it’s unlikely to be an issue- but also, if

you

feel you could make it work with an array and passing all the attribute
info in with one call, which I suspect would be possible but just a bit
more complex to build, then sure, go for it. If it ends up being overly
unwieldy then perhaps the per-attribute call would be better and we

could

perhaps acquire the lock before the function calls..? Doing a check to

see

if we have already locked it would be cheaper than trying to acquire a

new

lock, I’m fairly sure.

Well the do_analyze() code was already ok with acquiring the lock once

for

non-inherited stats and again for inherited stats, so the locks were
already not the end of the world. However, that's at most a 2x of the
locking required, and this would natts * x, quite a bit more. Having the
procedures check for a pre-existing lock seems like a good compromise.

I think this is a reasonable starting point. If the benchmarks show that
the locking is a problem, we can reevaluate, but otherwise IMHO we should
try to keep it as simple/flexible as possible.

Yeah, this was my general feeling as well. If it does become an issue, it
certainly seems like we would have ways to improve it in the future. Even
with this locking it is surely going to be better than having to re-analyze
the entire database which is where we are at now.

Thanks,

Stephen

Show quoted text
#41Bertrand Drouvot
bertranddrouvot.pg@gmail.com
In reply to: Corey Huinker (#34)
Re: Statistics Import and Export

Hi,

On Tue, Feb 20, 2024 at 02:24:52AM -0500, Corey Huinker wrote:

On Thu, Feb 15, 2024 at 4:09 AM Corey Huinker <corey.huinker@gmail.com>
wrote:

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.

v6 posted below.

Thanks!

I had in mind to look at it but it looks like a rebase is needed.

Regards,

--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com

#42Matthias van de Meent
boekewurm+postgres@gmail.com
In reply to: Corey Huinker (#38)
Re: Statistics Import and Export

On Fri, 1 Mar 2024, 04:55 Corey Huinker, <corey.huinker@gmail.com> wrote:

Also per our prior discussion- this makes sense to include in post-data section, imv, and also because then we have the indexes we may wish to load stats for, but further that also means it’ll be in the paralleliziable part of the process, making me a bit less concerned overall about the individual timing.

The ability to parallelize is pretty persuasive. But is that per-statement parallelization or do we get transaction blocks? i.e. if we ended up importing stats like this:

BEGIN;
LOCK TABLE schema.relation IN SHARE UPDATE EXCLUSIVE MODE;
LOCK TABLE pg_catalog.pg_statistic IN ROW UPDATE EXCLUSIVE MODE;
SELECT pg_import_rel_stats('schema.relation', ntuples, npages);
SELECT pg_import_pg_statistic('schema.relation', 'id', ...);
SELECT pg_import_pg_statistic('schema.relation', 'name', ...);

How well would this simplify to the following:

SELECT pg_import_statistic('schema.relation', attname, ...)
FROM (VALUES ('id', ...), ...) AS relation_stats (attname, ...);

Or even just one VALUES for the whole statistics loading?

I suspect the main issue with combining this into one statement
(transaction) is that failure to load one column's statistics implies
you'll have to redo all the other statistics (or fail to load the
statistics at all), which may be problematic at the scale of thousands
of relations with tens of columns each.

Kind regards,

Matthias van de Meent
Neon (https://neon.tech)

#43Stephen Frost
sfrost@snowman.net
In reply to: Matthias van de Meent (#42)
Re: Statistics Import and Export

Greetings,

On Wed, Mar 6, 2024 at 11:07 Matthias van de Meent <
boekewurm+postgres@gmail.com> wrote:

On Fri, 1 Mar 2024, 04:55 Corey Huinker, <corey.huinker@gmail.com> wrote:

Also per our prior discussion- this makes sense to include in post-data

section, imv, and also because then we have the indexes we may wish to load
stats for, but further that also means it’ll be in the paralleliziable part
of the process, making me a bit less concerned overall about the individual
timing.

The ability to parallelize is pretty persuasive. But is that

per-statement parallelization or do we get transaction blocks? i.e. if we
ended up importing stats like this:

BEGIN;
LOCK TABLE schema.relation IN SHARE UPDATE EXCLUSIVE MODE;
LOCK TABLE pg_catalog.pg_statistic IN ROW UPDATE EXCLUSIVE MODE;
SELECT pg_import_rel_stats('schema.relation', ntuples, npages);
SELECT pg_import_pg_statistic('schema.relation', 'id', ...);
SELECT pg_import_pg_statistic('schema.relation', 'name', ...);

How well would this simplify to the following:

SELECT pg_import_statistic('schema.relation', attname, ...)
FROM (VALUES ('id', ...), ...) AS relation_stats (attname, ...);

Using a VALUES construct for this does seem like it might make it cleaner,
so +1 for investigating that idea.

Or even just one VALUES for the whole statistics loading?

I don’t think we’d want to go beyond one relation at a time as then it can
be parallelized, we won’t be trying to lock a whole bunch of objects at
once, and any failures would only impact that one relation’s stats load.

I suspect the main issue with combining this into one statement

(transaction) is that failure to load one column's statistics implies
you'll have to redo all the other statistics (or fail to load the
statistics at all), which may be problematic at the scale of thousands
of relations with tens of columns each.

I’m pretty skeptical that “stats fail to load and lead to a failed
transaction” is a likely scenario that we have to spend a lot of effort
on. I’m pretty bullish on the idea that this simply won’t happen except in
very exceptional cases under a pg_upgrade (where the pg_dump that’s used
must match the target server version) and where it happens under a pg_dump
it’ll be because it’s an older pg_dump’s output and the answer will likely
need to be “you’re using a pg_dump file generated using an older version of
pg_dump and need to exclude stats entirely from the load and instead run
analyze on the data after loading it.”

What are the cases where we would be seeing stats reloads failing where it
would make sense to re-try on a subset of columns, or just generally, if we
know that the pg_dump version matches the target server version?

Thanks!

Stephen

Show quoted text
#44Matthias van de Meent
boekewurm+postgres@gmail.com
In reply to: Stephen Frost (#43)
Re: Statistics Import and Export

On Wed, 6 Mar 2024 at 11:33, Stephen Frost <sfrost@snowman.net> wrote:

On Wed, Mar 6, 2024 at 11:07 Matthias van de Meent <boekewurm+postgres@gmail.com> wrote:

Or even just one VALUES for the whole statistics loading?

I don’t think we’d want to go beyond one relation at a time as then it can be parallelized, we won’t be trying to lock a whole bunch of objects at once, and any failures would only impact that one relation’s stats load.

That also makes sense.

I suspect the main issue with combining this into one statement
(transaction) is that failure to load one column's statistics implies
you'll have to redo all the other statistics (or fail to load the
statistics at all), which may be problematic at the scale of thousands
of relations with tens of columns each.

I’m pretty skeptical that “stats fail to load and lead to a failed transaction” is a likely scenario that we have to spend a lot of effort on.

Agreed on the "don't have to spend a lot of time on it", but I'm not
so sure on the "unlikely" part while the autovacuum deamon is
involved, specifically for non-upgrade pg_restore. I imagine (haven't
checked) that autoanalyze is disabled during pg_upgrade, but
pg_restore doesn't do that, while it would have to be able to restore
statistics of a table if it is included in the dump (and the version
matches).

What are the cases where we would be seeing stats reloads failing where it would make sense to re-try on a subset of columns, or just generally, if we know that the pg_dump version matches the target server version?

Last time I checked, pg_restore's default is to load data on a
row-by-row basis without --single-transaction or --exit-on-error. Of
course, pg_upgrade uses it's own set of flags, but if a user is
restoring stats with pg_restore, I suspect they'd rather have some
column's stats loaded than no stats at all; so I would assume this
requires one separate pg_import_pg_statistic()-transaction for every
column.

Kind regards,

Matthias van de Meent
Neon (https://neon.tech)

#45Stephen Frost
sfrost@snowman.net
In reply to: Matthias van de Meent (#44)
Re: Statistics Import and Export

Greetings,

* Matthias van de Meent (boekewurm+postgres@gmail.com) wrote:

On Wed, 6 Mar 2024 at 11:33, Stephen Frost <sfrost@snowman.net> wrote:

On Wed, Mar 6, 2024 at 11:07 Matthias van de Meent <boekewurm+postgres@gmail.com> wrote:

Or even just one VALUES for the whole statistics loading?

I don’t think we’d want to go beyond one relation at a time as then it can be parallelized, we won’t be trying to lock a whole bunch of objects at once, and any failures would only impact that one relation’s stats load.

That also makes sense.

Great, thanks.

I suspect the main issue with combining this into one statement
(transaction) is that failure to load one column's statistics implies
you'll have to redo all the other statistics (or fail to load the
statistics at all), which may be problematic at the scale of thousands
of relations with tens of columns each.

I’m pretty skeptical that “stats fail to load and lead to a failed transaction” is a likely scenario that we have to spend a lot of effort on.

Agreed on the "don't have to spend a lot of time on it", but I'm not
so sure on the "unlikely" part while the autovacuum deamon is
involved, specifically for non-upgrade pg_restore. I imagine (haven't
checked) that autoanalyze is disabled during pg_upgrade, but
pg_restore doesn't do that, while it would have to be able to restore
statistics of a table if it is included in the dump (and the version
matches).

Even if autovacuum was running and it kicked off an auto-analyze of the
relation at just the time that we were trying to load the stats, there
would be appropriate locking happening to keep them from causing an
outright ERROR and transaction failure, or if not, that's a lack of
locking and should be fixed. With the per-attribute-function-call
approach, that could lead to a situation where some stats are from the
auto-analyze and some are from the stats being loaded but I'm not sure
if that's a big concern or not.

For users of this, I would think we'd generally encourage them to
disable autovacuum on the tables they're loading as otherwise they'll
end up with the stats going back to whatever an auto-analyze ends up
finding. That may be fine in some cases, but not in others.

A couple questions to think about though: Should pg_dump explicitly ask
autovacuum to ignore these tables while we're loading them?
Should these functions only perform a load when there aren't any
existing stats? Should the latter be an argument to the functions to
allow the caller to decide?

What are the cases where we would be seeing stats reloads failing where it would make sense to re-try on a subset of columns, or just generally, if we know that the pg_dump version matches the target server version?

Last time I checked, pg_restore's default is to load data on a
row-by-row basis without --single-transaction or --exit-on-error. Of
course, pg_upgrade uses it's own set of flags, but if a user is
restoring stats with pg_restore, I suspect they'd rather have some
column's stats loaded than no stats at all; so I would assume this
requires one separate pg_import_pg_statistic()-transaction for every
column.

Having some discussion around that would be useful. Is it better to
have a situation where there are stats for some columns but no stats for
other columns? There would be a good chance that this would lead to a
set of queries that were properly planned out and a set which end up
with unexpected and likely poor query plans due to lack of stats.
Arguably that's better overall, but either way an ANALYZE needs to be
done to address the lack of stats for those columns and then that
ANALYZE is going to blow away whatever stats got loaded previously
anyway and all we did with a partial stats load was maybe have a subset
of queries have better plans in the interim, after having expended the
cost to try and individually load the stats and dealing with the case of
some of them succeeding and some failing.

Overall, I'd suggest we wait to see what Corey comes up with in terms of
doing the stats load for all attributes in a single function call,
perhaps using the VALUES construct as you suggested up-thread, and then
we can contemplate if that's clean enough to work or if it's so grotty
that the better plan would be to do per-attribute function calls. If it
ends up being the latter, then we can revisit this discussion and try to
answer some of the questions raised above.

Thanks!

Stephen

#46Corey Huinker
corey.huinker@gmail.com
In reply to: Matthias van de Meent (#42)
Re: Statistics Import and Export

BEGIN;
LOCK TABLE schema.relation IN SHARE UPDATE EXCLUSIVE MODE;
LOCK TABLE pg_catalog.pg_statistic IN ROW UPDATE EXCLUSIVE MODE;
SELECT pg_import_rel_stats('schema.relation', ntuples, npages);
SELECT pg_import_pg_statistic('schema.relation', 'id', ...);
SELECT pg_import_pg_statistic('schema.relation', 'name', ...);

How well would this simplify to the following:

SELECT pg_import_statistic('schema.relation', attname, ...)
FROM (VALUES ('id', ...), ...) AS relation_stats (attname, ...);

Or even just one VALUES for the whole statistics loading?

I'm sorry, I don't quite understand what you're suggesting here. I'm about
to post the new functions, so perhaps you can rephrase this in the context
of those functions.

I suspect the main issue with combining this into one statement
(transaction) is that failure to load one column's statistics implies
you'll have to redo all the other statistics (or fail to load the
statistics at all), which may be problematic at the scale of thousands
of relations with tens of columns each.

Yes, that is is a concern, and I can see value to having it both ways (one
failure fails the whole table's worth of set_something() functions, but I
can also see emitting a warning instead of error and returning false. I'm
eager to get feedback on which the community would prefer, or perhaps even
make it a parameter.

#47Corey Huinker
corey.huinker@gmail.com
In reply to: Stephen Frost (#45)
1 attachment(s)
Re: Statistics Import and Export

Having some discussion around that would be useful. Is it better to
have a situation where there are stats for some columns but no stats for
other columns? There would be a good chance that this would lead to a
set of queries that were properly planned out and a set which end up
with unexpected and likely poor query plans due to lack of stats.
Arguably that's better overall, but either way an ANALYZE needs to be
done to address the lack of stats for those columns and then that
ANALYZE is going to blow away whatever stats got loaded previously
anyway and all we did with a partial stats load was maybe have a subset
of queries have better plans in the interim, after having expended the
cost to try and individually load the stats and dealing with the case of
some of them succeeding and some failing.

It is my (incomplete and entirely second-hand) understanding is that
pg_upgrade doesn't STOP autovacuum, but sets a delay to a very long value
and then resets it on completion, presumably because analyzing a table
before its data is loaded and indexes are created would just be a waste of
time.

Overall, I'd suggest we wait to see what Corey comes up with in terms of
doing the stats load for all attributes in a single function call,
perhaps using the VALUES construct as you suggested up-thread, and then
we can contemplate if that's clean enough to work or if it's so grotty
that the better plan would be to do per-attribute function calls. If it
ends up being the latter, then we can revisit this discussion and try to
answer some of the questions raised above.

In the patch below, I ended up doing per-attribute function calls, mostly
because it allowed me to avoid creating a custom data type for the portable
version of pg_statistic. This comes at the cost of a very high number of
parameters, but that's the breaks.

I am a bit concerned about the number of locks on pg_statistic and the
relation itself, doing CatalogOpenIndexes/CatalogCloseIndexes once per
attribute rather than once per relation. But I also see that this will
mostly get used at a time when no other traffic is on the machine, and
whatever it costs, it's still faster than the smallest table sample (insert
joke about "don't have to be faster than the bear" here).

This raises questions about whether a failure in one attribute update
statement should cause the others in that relation to roll back or not, and
I can see situations where both would be desirable.

I'm putting this out there ahead of the pg_dump / fe_utils work, mostly
because what I do there heavily depends on how this is received.

Also, I'm still seeking confirmation that I can create a pg_dump TOC entry
with a chain of commands (e.g. BEGIN; ... COMMIT; ) or if I have to fan
them out into multiple entries.

Anyway, here's v7. Eagerly awaiting feedback.

Attachments:

v7-0001-Create-pg_set_relation_stats-pg_set_attribute_sta.patchtext/x-patch; charset=US-ASCII; name=v7-0001-Create-pg_set_relation_stats-pg_set_attribute_sta.patchDownload
From 9bc7200b8a67efefe20453e0c48aed8a0a5f62f8 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 7 Mar 2024 22:18:48 -0500
Subject: [PATCH v7] Create pg_set_relation_stats, pg_set_attribute_stats.

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

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

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

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

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

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 291ed876fc..d12b6e3ca3 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8818,7 +8818,6 @@
 { oid => '3813', descr => 'generate XML text node',
   proname => 'xmltext', proisstrict => 't', prorettype => 'xml',
   proargtypes => 'text', prosrc => 'xmltext' },
-
 { oid => '2923', descr => 'map table contents to XML',
   proname => 'table_to_xml', procost => '100', provolatile => 's',
   proparallel => 'r', prorettype => 'xml',
@@ -12163,8 +12162,24 @@
 
 # GiST stratnum implementations
 { oid => '8047', descr => 'GiST support',
-  proname => 'gist_stratnum_identity', prorettype => 'int2',
+  proname => 'gist_stratnum_identity',prorettype => 'int2',
   proargtypes => 'int2',
   prosrc => 'gist_stratnum_identity' },
 
+# Statistics Import
+{ oid => '8048',
+  descr => 'set statistics on relation',
+  proname => 'pg_set_relation_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'oid float4 int4',
+  proargnames => '{relation,reltuples,relpages}',
+  prosrc => 'pg_set_relation_stats' },
+
+{ oid => '8049',
+  descr => 'set statistics on attribute',
+  proname => 'pg_set_attribute_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'oid name bool float4 int4 float4 int2 int2 int2 int2 int2 _float4 _float4 _float4 _float4 _float4 text text text text text',
+  proargnames => '{relation,attname,stainherit,stanullfrac,stawidth,stadistinct,stakind1,stakind2,stakind3,stakind4,stakind5,stanumbers1,stanumbers2,stanumbers3,stanumbers4,stanumbers5,stavalues1,stavalues2,stavalues3,stavalues4,stavalues5}',
+  prosrc => 'pg_set_attribute_stats' },
 ]
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 7f2bf18716..73d3b541dd 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -127,4 +127,7 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												int nclauses);
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
+extern Datum pg_set_relation_stats(PG_FUNCTION_ARGS);
+extern Datum pg_set_attribute_stats(PG_FUNCTION_ARGS);
+
 #endif							/* STATISTICS_H */
diff --git a/src/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..999aebdfa9
--- /dev/null
+++ b/src/backend/statistics/statistics.c
@@ -0,0 +1,360 @@
+/*------------------------------------------------------------------------- * * 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 "fmgr.h"
+#include "nodes/nodeFuncs.h"
+#include "parser/parse_oper.h"
+#include "statistics/statistics.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/syscache.h"
+
+/*
+ * Set statistics for a given pg_class entry.
+ *
+ * pg_set_relation_stats(relation Oid, reltuples double, relpages int)
+ *
+ * This does an in-place (i.e. non-transactional) update of pg_class, just as
+ * is done in ANALYZE.
+ *
+ */
+Datum
+pg_set_relation_stats(PG_FUNCTION_ARGS)
+{
+	const char *param_names[] = {
+		"relation",
+		"reltuples",
+		"relpages",
+	};
+
+	Oid				relid;
+	Relation		rel;
+	HeapTuple		ctup;
+	Form_pg_class	pgcform;
+
+	for (int i = 0; i <= 2; i++)
+		if (PG_ARGISNULL(i))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be NULL", param_names[i])));
+
+	relid = PG_GETARG_OID(0);
+
+	/*
+	 * Open the relation, getting ShareUpdateExclusiveLock to ensure that no
+	 * other stat-setting operation can run on it concurrently.
+	 */
+	rel = table_open(RelationRelationId, ShareUpdateExclusiveLock);
+
+	ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(ctup))
+		elog(ERROR, "pg_class entry for relid %u vanished during statistics import",
+			 relid);
+
+	pgcform = (Form_pg_class) GETSTRUCT(ctup);
+	pgcform->reltuples = PG_GETARG_FLOAT4(1);
+	pgcform->relpages = PG_GETARG_INT32(2);
+
+	heap_inplace_update(rel, ctup);
+
+	table_close(rel, ShareUpdateExclusiveLock);
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * Import statistics for a given relation attribute
+ *
+ * pg_set_attribute_stats(relation Oid, attname name, stainherit bool,
+ *                        stanullfrac float4, stawidth int, stadistinct float4,
+ *                        stakind1 int2, stakind2 int2, stakind3 int3,
+ *                        stakind4 int2, stakind5 int2, stanumbers1 float4[],
+ *                        stanumbers2 float4[], stanumbers3 float4[],
+ *                        stanumbers4 float4[], stanumbers5 float4[],
+ *                        stanumbers1 float4[], stanumbers2 float4[],
+ *                        stanumbers3 float4[], stanumbers4 float4[],
+ *                        stanumbers5 float4[], stavalues1 text,
+ *                        stavalues2 text, stavalues3 text,
+ *                        stavalues4 text, stavalues5 text);
+ *
+ *
+ */
+Datum
+pg_set_attribute_stats(PG_FUNCTION_ARGS)
+{
+	enum {
+		P_RELATION = 0,
+		P_ATTNAME,
+		P_STAINHERIT,
+		P_STANULLFRAC,
+		P_STAWIDTH,
+		P_STADISTINCT,
+		P_STAKIND1,
+		P_STAKIND2,
+		P_STAKIND3,
+		P_STAKIND4,
+		P_STAKIND5,
+		P_STANUMBERS1,
+		P_STANUMBERS2,
+		P_STANUMBERS3,
+		P_STANUMBERS4,
+		P_STANUMBERS5,
+		P_STAVALUES1,
+		P_STAVALUES2,
+		P_STAVALUES3,
+		P_STAVALUES4,
+		P_STAVALUES5,
+		P_NUM_PARAMS
+	};
+
+	/* names of columns that cannot be null */
+	const char *required_param_names[] = {
+		"relation",
+		"attname",
+		"stainherit",
+		"stanullfrac",
+		"stawidth",
+		"stadistinct",
+		"stakind1",
+		"stakind2",
+		"stakind3",
+		"stakind4",
+		"stakind5",
+	};
+
+	Oid			relid;
+	Name		attname;
+	Relation	rel;
+	HeapTuple	tuple;
+
+	Oid		typid;
+	int32	typmod;
+	Oid		typcoll;
+	Oid 	eqopr;
+	Oid 	ltopr;
+	Oid		basetypid;
+	Oid 	baseeqopr;
+	Oid 	baseltopr;
+
+	float4	stanullfrac;
+	int		stawidth;
+
+	Datum	values[Natts_pg_statistic] = { 0 };
+	bool	nulls[Natts_pg_statistic] = { false };
+
+	Relation			sd;
+	HeapTuple			oldtup;
+	CatalogIndexState	indstate;
+	HeapTuple			stup;
+	Form_pg_attribute	attr;
+
+	for (int i = P_RELATION; i <= P_STAKIND5; i++)
+		if (PG_ARGISNULL(i))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be NULL", required_param_names[i])));
+
+	relid = PG_GETARG_OID(P_RELATION);
+	attname = PG_GETARG_NAME(P_ATTNAME);
+	rel = relation_open(relid, ShareUpdateExclusiveLock);
+	tuple = SearchSysCache2(ATTNAME,
+							ObjectIdGetDatum(relid),
+							NameGetDatum(attname));
+
+	if (!HeapTupleIsValid(tuple))
+	{
+		relation_close(rel, NoLock);
+		PG_RETURN_BOOL(false);
+	}
+
+	attr = (Form_pg_attribute) GETSTRUCT(tuple);
+	if (attr->attisdropped)
+	{
+		ReleaseSysCache(tuple);
+		relation_close(rel, NoLock);
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * If this relation is an index and that index has expressions in
+	 * it, and the attnum specified is known to be an expression, then
+	 * we must walk the list attributes up to the specified attnum
+	 * to get the right expression.
+	 */
+	if ((rel->rd_rel->relkind == RELKIND_INDEX
+				|| (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+			&& (rel->rd_indexprs != NIL)
+			&& (rel->rd_index->indkey.values[attr->attnum-1] == 0))
+	{
+		ListCell   *indexpr_item = list_head(rel->rd_indexprs);
+		Node	   *expr;
+
+		for (int i = 0; i < attr->attnum - 1; i++)
+			if (rel->rd_index->indkey.values[i] == 0)
+				indexpr_item = lnext(rel->rd_indexprs, indexpr_item);
+
+		if (indexpr_item == NULL)	/* shouldn't happen */
+			elog(ERROR, "too few entries in indexprs list");
+
+		expr = (Node *) lfirst(indexpr_item);
+
+		typid = exprType(expr);
+		typmod = exprTypmod(expr);
+
+		/*
+		 * If a collation has been specified for the index column, use that in
+		 * preference to anything else; but if not, fall back to whatever we
+		 * can get from the expression.
+		 */
+		if (OidIsValid(attr->attcollation))
+			typcoll = attr->attcollation;
+		else
+			typcoll = exprCollation(expr);
+	}
+	else
+	{
+		typid = attr->atttypid;
+		typmod = attr->atttypmod;
+		typcoll = attr->attcollation;
+	}
+
+	get_sort_group_operators(typid, false, false, false,
+							 &ltopr, &eqopr, NULL, NULL);
+
+	basetypid = get_base_element_type(typid);
+
+	if (basetypid == InvalidOid)
+	{
+		/* type is its own base type */
+		basetypid = typid;
+		baseltopr = ltopr;
+		baseeqopr = eqopr;
+	}
+	else
+		get_sort_group_operators(basetypid, false, false, false,
+								 &baseltopr, &baseeqopr, NULL, NULL);
+
+	stanullfrac = PG_GETARG_FLOAT4(P_STANULLFRAC);
+	if ((stanullfrac < 0.0) || (stanullfrac > 1.0))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("stanullfrac %f is out of range 0.0 to 1.0", stanullfrac)));
+
+	stawidth = PG_GETARG_INT32(P_STAWIDTH);
+	if (stawidth < 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("stawidth %d must be >= 0", stawidth)));
+
+	values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(relid);
+	values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(attr->attnum);
+	values[Anum_pg_statistic_stainherit - 1] = PG_GETARG_DATUM(P_STAINHERIT);
+	values[Anum_pg_statistic_stanullfrac - 1] = PG_GETARG_DATUM(P_STANULLFRAC);
+	values[Anum_pg_statistic_stawidth - 1] = PG_GETARG_DATUM(P_STAWIDTH);
+	values[Anum_pg_statistic_stadistinct - 1] = PG_GETARG_DATUM(P_STADISTINCT);
+
+	/* The remaining fields are all parallel arrays, so we iterate over them */
+	for (int k = 0; k < 5; k++)
+	{
+		int16	kind = PG_GETARG_INT16(P_STAKIND1 + k);
+		Oid		opoid;
+		Oid		colloid;
+
+		switch(kind) {
+			case 0:
+				opoid = InvalidOid;
+				colloid = InvalidOid;
+				break;
+			case STATISTIC_KIND_MCV:
+				opoid = eqopr;
+				colloid = typcoll;
+				break;
+			case STATISTIC_KIND_HISTOGRAM:
+			case STATISTIC_KIND_CORRELATION:
+			case STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM:
+			case STATISTIC_KIND_BOUNDS_HISTOGRAM:
+				opoid = ltopr;
+				colloid = typcoll;
+				break;
+			case STATISTIC_KIND_MCELEM:
+			case STATISTIC_KIND_DECHIST:
+				opoid = baseeqopr;
+				colloid = typcoll;
+				break;
+			default:
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("stakind%d = %d is out of the range 0 to %d", k + 1,
+								kind, STATISTIC_KIND_BOUNDS_HISTOGRAM)));
+		}
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] = PG_GETARG_DATUM(P_STAKIND1 + k);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(opoid);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(colloid);
+
+		if (PG_ARGISNULL(P_STANUMBERS1 + k))
+			nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		else
+			values[Anum_pg_statistic_stanumbers1 - 1 + k] = PG_GETARG_DATUM(P_STANUMBERS1 + k);
+
+		if (PG_ARGISNULL(P_STAVALUES1 + k))
+			nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+		else
+		{
+			char   *s = TextDatumGetCString(PG_GETARG_DATUM(P_STAVALUES1 + k));
+			FmgrInfo finfo;
+
+			fmgr_info(F_ARRAY_IN, &finfo);
+
+			values[Anum_pg_statistic_stavalues1 - 1 + k] =
+				FunctionCall3(&finfo, CStringGetDatum(s), ObjectIdGetDatum(basetypid),
+							  Int32GetDatum(typmod));
+
+			pfree(s);
+		}
+	}
+
+	sd = table_open(StatisticRelationId, RowExclusiveLock);
+
+	/* Is there already a pg_statistic tuple for this attribute? */
+	oldtup = SearchSysCache3(STATRELATTINH,
+							 ObjectIdGetDatum(relid),
+							 Int16GetDatum(attr->attnum),
+							 PG_GETARG_DATUM(P_STAINHERIT));
+
+	indstate = CatalogOpenIndexes(sd);
+
+	if (HeapTupleIsValid(oldtup))
+	{
+		/* Yes, replace it */
+		bool replaces[Natts_pg_statistic] = { true };
+
+		stup = heap_modify_tuple(oldtup, RelationGetDescr(sd),
+								 values, nulls, replaces);
+		ReleaseSysCache(oldtup);
+		CatalogTupleUpdateWithInfo(sd, &stup->t_self, stup, indstate);
+	}
+	else
+	{
+		/* No, insert new tuple */
+		stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
+		CatalogTupleInsertWithInfo(sd, stup, indstate);
+	}
+
+	heap_freetuple(stup);
+	CatalogCloseIndexes(indstate);
+	table_close(sd, RowExclusiveLock);
+	relation_close(rel, NoLock);
+	ReleaseSysCache(tuple);
+	PG_RETURN_BOOL(true);
+}
diff --git a/src/test/regress/expected/stats_export_import.out b/src/test/regress/expected/stats_export_import.out
new file mode 100644
index 0000000000..50ce86693a
--- /dev/null
+++ b/src/test/regress/expected/stats_export_import.out
@@ -0,0 +1,211 @@
+CREATE SCHEMA stats_export_import;
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b float,
+    c text,
+    d date,
+    e jsonb);
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    tags text[]
+);
+SELECT reltuples, relpages FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ reltuples | relpages 
+-----------+----------
+        -1 |        0
+(1 row)
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass, 3.6::float4, 15000);
+ pg_set_relation_stats 
+-----------------------
+ t
+(1 row)
+
+SELECT reltuples, relpages FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ reltuples | relpages 
+-----------+----------
+       3.6 |    15000
+(1 row)
+
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type, array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, NULL;
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+   relname    | num_stats 
+--------------+-----------
+ is_odd       |         1
+ is_odd_clone |         1
+ test         |         4
+ test_clone   |         4
+(4 rows)
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 1d8a414eea..0c89ffc02d 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -103,7 +103,7 @@ test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combo
 # ----------
 # Another group of parallel tests (JSON related)
 # ----------
-test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson
+test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson stats_export_import
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/sql/stats_export_import.sql b/src/test/regress/sql/stats_export_import.sql
new file mode 100644
index 0000000000..75c9d130b1
--- /dev/null
+++ b/src/test/regress/sql/stats_export_import.sql
@@ -0,0 +1,198 @@
+CREATE SCHEMA stats_export_import;
+
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b float,
+    c text,
+    d date,
+    e jsonb);
+
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    tags text[]
+);
+
+SELECT reltuples, relpages FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass, 3.6::float4, 15000);
+
+SELECT reltuples, relpages FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type, array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, NULL;
+
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+
+-- copy stats from test to test_clone and is_odd to is_odd_clone
+SELECT
+    format('SELECT pg_catalog.pg_set_attribute_stats( '
+           || 'relation => %L::regclass, attname => %L::name, '
+           || 'stainherit => %L::boolean, stanullfrac => %s::real, '
+           || 'stawidth => %s::integer, stadistinct => %s::real, '
+           || 'stakind1 => %s::smallint, stakind2 => %s::smallint, '
+           || 'stakind3 => %s::smallint, stakind4 => %s::smallint, '
+           || 'stakind5 => %s::smallint, '
+           || 'stanumbers1 => %L::real[], stanumbers2 => %L::real[], '
+           || 'stanumbers3 => %L::real[], stanumbers4 => %L::real[], '
+           || 'stanumbers5 => %L::real[], '
+           || 'stavalues1 => %L::text, stavalues2 => %L::text, '
+           || 'stavalues3 => %L::text, stavalues4 => %L::text, '
+           || 'stavalues5 => %L::text)',
+        'stats_export_import.' || c.relname || '_clone', a.attname,
+        s.stainherit, s.stanullfrac,
+        s.stawidth, s.stadistinct,
+        s.stakind1, s.stakind2, s.stakind3,
+        s.stakind4, s.stakind5,
+        s.stanumbers1, s.stanumbers2,
+        s.stanumbers3, s.stanumbers4,
+        s.stanumbers5,
+        s.stavalues1::text, s.stavalues2::text, s.stavalues3::text,
+        s.stavalues4::text, s.stavalues5::text)
+FROM pg_class AS c
+JOIN pg_attribute a ON a.attrelid = c.oid
+JOIN pg_statistic s ON s.starelid = a.attrelid AND s.staattnum = a.attnum
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'is_odd')
+\gexec
+
+-- restore ECHO to original value
+\set ECHO :orig_ECHO
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 0bb7aeb40e..3e88aec27f 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -28739,6 +28739,95 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
     in identifying the specific disk files associated with database objects.
    </para>
 
+   <table id="functions-admin-statsimport">
+    <title>Database Object Statistics Import Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_relation_stats</primary>
+        </indexterm>
+        <function>pg_set_relation_stats</function> ( <parameter>relation</parameter> <type>regclass</type>, <parameter>reltuples</parameter> <type>float4</type>, <parameter>relpages</parameter> <type>integer</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Updates the <structname>pg_class</structname> row for the specified
+        <parameter>relation</parameter>, setting the values for the columns
+        <structfield>reltuples</structfield> and <structfield>relpages</structfield>.
+        To avoid table bloat in <structname>pg_class</structname>, this change
+        is made with an in-place update, and therefore cannot be rolled back through
+        normal transaction processing.
+       </para>
+       <para>
+        The purpose of this function is to apply statistics values in an
+        upgrade situation that are "good enough" for system operation until
+        they are replaced by the next auto-analyze. This function is used by
+        <command>pg_upgrade</command> and <command>pg_restore</command> to
+        convey the statistics from the old system version into the new one.
+       </para>
+       </entry>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_attribute_stats</primary>
+        </indexterm>
+        <function>pg_set_attribute_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>attname</parameter> <type>name</type>,
+         <parameter>stainherit</parameter> <type>boolean</type>,
+         <parameter>stanullfrac</parameter> <type>real</type>,
+         <parameter>stawidth</parameter> <type>integer</type>,
+         <parameter>stadistinct</parameter> <type>real</type>,
+         <parameter>stakind1</parameter> <type>smallint</type>,
+         <parameter>stakind2</parameter> <type>smallint</type>,
+         <parameter>stakind3</parameter> <type>smallint</type>,
+         <parameter>stakind4</parameter> <type>smallint</type>,
+         <parameter>stakind5</parameter> <type>smallint</type>,
+         <parameter>stanumbers1</parameter> <type>real[]</type>,
+         <parameter>stanumbers2</parameter> <type>real[]</type>,
+         <parameter>stanumbers3</parameter> <type>real[]</type>,
+         <parameter>stanumbers4</parameter> <type>real[]</type>,
+         <parameter>stanumbers5</parameter> <type>real[]</type>,
+         <parameter>stavalues1</parameter> <type>text</type>,
+         <parameter>stavalues2</parameter> <type>text</type>,
+         <parameter>stavalues3</parameter> <type>text</type>,
+         <parameter>stavalues4</parameter> <type>text</type>,
+         <parameter>stavalues5</parameter> <type>text</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Replaces the <structname>pg_statistic</structname> row for the
+        <structname>pg_attribute</structname> row specified by
+        <parameter>relation</parameter> and <parameter>attname</parameter>.
+        The values for fields <structfield>staopN</structfield> and
+        <structfield>stacollN</structfield> are derived from the
+        <structname>pg_attribute</structname> row and the corresponding
+        <parameter>stakindN</parameter> value.
+       </para>
+       <para>
+        The purpose of this function is to apply statistics values in an
+        upgrade situation that are "good enough" for system operation until
+        they are replaced by the next auto-analyze. This function is used by
+        <command>pg_upgrade</command> and <command>pg_restore</command> to
+        convey the statistics from the old system version into the new one.
+       </para>
+       </entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
    <table id="functions-admin-dblocation">
     <title>Database Object Location Functions</title>
     <tgroup cols="1">
-- 
2.44.0

#48Stephen Frost
sfrost@snowman.net
In reply to: Corey Huinker (#47)
Re: Statistics Import and Export

Greetings,

* Corey Huinker (corey.huinker@gmail.com) wrote:

Having some discussion around that would be useful. Is it better to
have a situation where there are stats for some columns but no stats for
other columns? There would be a good chance that this would lead to a
set of queries that were properly planned out and a set which end up
with unexpected and likely poor query plans due to lack of stats.
Arguably that's better overall, but either way an ANALYZE needs to be
done to address the lack of stats for those columns and then that
ANALYZE is going to blow away whatever stats got loaded previously
anyway and all we did with a partial stats load was maybe have a subset
of queries have better plans in the interim, after having expended the
cost to try and individually load the stats and dealing with the case of
some of them succeeding and some failing.

It is my (incomplete and entirely second-hand) understanding is that
pg_upgrade doesn't STOP autovacuum, but sets a delay to a very long value
and then resets it on completion, presumably because analyzing a table
before its data is loaded and indexes are created would just be a waste of
time.

No, pg_upgrade starts the postmaster with -b (undocumented
binary-upgrade mode) which prevents autovacuum (and logical replication
workers) from starting, so we don't need to worry about autovacuum
coming in and causing issues during binary upgrade. That doesn't
entirely eliminate the concerns around pg_dump vs. autovacuum because we
may restore a dump into a non-binary-upgrade-in-progress system though,
of course.

Overall, I'd suggest we wait to see what Corey comes up with in terms of
doing the stats load for all attributes in a single function call,
perhaps using the VALUES construct as you suggested up-thread, and then
we can contemplate if that's clean enough to work or if it's so grotty
that the better plan would be to do per-attribute function calls. If it
ends up being the latter, then we can revisit this discussion and try to
answer some of the questions raised above.

In the patch below, I ended up doing per-attribute function calls, mostly
because it allowed me to avoid creating a custom data type for the portable
version of pg_statistic. This comes at the cost of a very high number of
parameters, but that's the breaks.

Perhaps it's just me ... but it doesn't seem like it's all that many
parameters.

I am a bit concerned about the number of locks on pg_statistic and the
relation itself, doing CatalogOpenIndexes/CatalogCloseIndexes once per
attribute rather than once per relation. But I also see that this will
mostly get used at a time when no other traffic is on the machine, and
whatever it costs, it's still faster than the smallest table sample (insert
joke about "don't have to be faster than the bear" here).

I continue to not be too concerned about this until and unless it's
actually shown to be an issue. Keeping things simple and
straight-forward has its own value.

This raises questions about whether a failure in one attribute update
statement should cause the others in that relation to roll back or not, and
I can see situations where both would be desirable.

I'm putting this out there ahead of the pg_dump / fe_utils work, mostly
because what I do there heavily depends on how this is received.

Also, I'm still seeking confirmation that I can create a pg_dump TOC entry
with a chain of commands (e.g. BEGIN; ... COMMIT; ) or if I have to fan
them out into multiple entries.

If we do go with this approach, we'd certainly want to make sure to do
it in a manner which would allow pg_restore's single-transaction mode to
still work, which definitely complicates this some.

Given that and the other general feeling that the locking won't be a big
issue, I'd suggest the simple approach on the pg_dump side of just
dumping the stats out without the BEGIN/COMMIT.

Anyway, here's v7. Eagerly awaiting feedback.

Subject: [PATCH v7] Create pg_set_relation_stats, pg_set_attribute_stats.

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 291ed876fc..d12b6e3ca3 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8818,7 +8818,6 @@
{ oid => '3813', descr => 'generate XML text node',
proname => 'xmltext', proisstrict => 't', prorettype => 'xml',
proargtypes => 'text', prosrc => 'xmltext' },
-
{ oid => '2923', descr => 'map table contents to XML',
proname => 'table_to_xml', procost => '100', provolatile => 's',
proparallel => 'r', prorettype => 'xml',
@@ -12163,8 +12162,24 @@
# GiST stratnum implementations
{ oid => '8047', descr => 'GiST support',
-  proname => 'gist_stratnum_identity', prorettype => 'int2',
+  proname => 'gist_stratnum_identity',prorettype => 'int2',
proargtypes => 'int2',
prosrc => 'gist_stratnum_identity' },

Random whitespace hunks shouldn't be included

diff --git a/src/backend/statistics/statistics.c b/src/backend/statistics/statistics.c
new file mode 100644
index 0000000000..999aebdfa9
--- /dev/null
+++ b/src/backend/statistics/statistics.c
@@ -0,0 +1,360 @@
+/*------------------------------------------------------------------------- * * statistics.c *
+ * IDENTIFICATION
+ *       src/backend/statistics/statistics.c
+ *
+ *-------------------------------------------------------------------------
+ */

Top-of-file comment should be cleaned up.

+/*
+ * Set statistics for a given pg_class entry.
+ *
+ * pg_set_relation_stats(relation Oid, reltuples double, relpages int)
+ *
+ * This does an in-place (i.e. non-transactional) update of pg_class, just as
+ * is done in ANALYZE.
+ *
+ */
+Datum
+pg_set_relation_stats(PG_FUNCTION_ARGS)
+{
+	const char *param_names[] = {
+		"relation",
+		"reltuples",
+		"relpages",
+	};
+
+	Oid				relid;
+	Relation		rel;
+	HeapTuple		ctup;
+	Form_pg_class	pgcform;
+
+	for (int i = 0; i <= 2; i++)
+		if (PG_ARGISNULL(i))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be NULL", param_names[i])));

Why not just mark this function as strict..? Or perhaps we should allow
NULLs to be passed in and just not update the current value in that
case? Also, in some cases we allow the function to be called with a
NULL but then make it a no-op rather than throwing an ERROR (eg, if the
OID ends up being NULL). Not sure if that makes sense here or not
offhand but figured I'd mention it as something to consider.

+	pgcform = (Form_pg_class) GETSTRUCT(ctup);
+	pgcform->reltuples = PG_GETARG_FLOAT4(1);
+	pgcform->relpages = PG_GETARG_INT32(2);

Shouldn't we include relallvisible?

Also, perhaps we should use the approach that we have in ANALYZE, and
only actually do something if the values are different rather than just
always doing an update.

+/*
+ * Import statistics for a given relation attribute
+ *
+ * pg_set_attribute_stats(relation Oid, attname name, stainherit bool,
+ *                        stanullfrac float4, stawidth int, stadistinct float4,
+ *                        stakind1 int2, stakind2 int2, stakind3 int3,
+ *                        stakind4 int2, stakind5 int2, stanumbers1 float4[],
+ *                        stanumbers2 float4[], stanumbers3 float4[],
+ *                        stanumbers4 float4[], stanumbers5 float4[],
+ *                        stanumbers1 float4[], stanumbers2 float4[],
+ *                        stanumbers3 float4[], stanumbers4 float4[],
+ *                        stanumbers5 float4[], stavalues1 text,
+ *                        stavalues2 text, stavalues3 text,
+ *                        stavalues4 text, stavalues5 text);
+ *
+ *
+ */

Don't know that it makes sense to just repeat the function declaration
inside a comment like this- it'll just end up out of date.

+Datum
+pg_set_attribute_stats(PG_FUNCTION_ARGS)
+	/* names of columns that cannot be null */
+	const char *required_param_names[] = {
+		"relation",
+		"attname",
+		"stainherit",
+		"stanullfrac",
+		"stawidth",
+		"stadistinct",
+		"stakind1",
+		"stakind2",
+		"stakind3",
+		"stakind4",
+		"stakind5",
+	};

Same comment here as above wrt NULL being passed in.

+ for (int k = 0; k < 5; k++)

Shouldn't we use STATISTIC_NUM_SLOTS here?

Thanks!

Stephen

#49Corey Huinker
corey.huinker@gmail.com
In reply to: Stephen Frost (#48)
Re: Statistics Import and Export

Perhaps it's just me ... but it doesn't seem like it's all that many

parameters.

It's more than I can memorize, so that feels like a lot to me. Clearly it's
not insurmountable.

I am a bit concerned about the number of locks on pg_statistic and the
relation itself, doing CatalogOpenIndexes/CatalogCloseIndexes once per
attribute rather than once per relation. But I also see that this will
mostly get used at a time when no other traffic is on the machine, and
whatever it costs, it's still faster than the smallest table sample

(insert

joke about "don't have to be faster than the bear" here).

I continue to not be too concerned about this until and unless it's
actually shown to be an issue. Keeping things simple and
straight-forward has its own value.

Ok, I'm sold on that plan.

+/*
+ * Set statistics for a given pg_class entry.
+ *
+ * pg_set_relation_stats(relation Oid, reltuples double, relpages int)
+ *
+ * This does an in-place (i.e. non-transactional) update of pg_class,

just as

+ * is done in ANALYZE.
+ *
+ */
+Datum
+pg_set_relation_stats(PG_FUNCTION_ARGS)
+{
+     const char *param_names[] = {
+             "relation",
+             "reltuples",
+             "relpages",
+     };
+
+     Oid                             relid;
+     Relation                rel;
+     HeapTuple               ctup;
+     Form_pg_class   pgcform;
+
+     for (int i = 0; i <= 2; i++)
+             if (PG_ARGISNULL(i))
+                     ereport(ERROR,
+

(errcode(ERRCODE_INVALID_PARAMETER_VALUE),

+ errmsg("%s cannot be NULL",

param_names[i])));

Why not just mark this function as strict..? Or perhaps we should allow
NULLs to be passed in and just not update the current value in that
case?

Strict could definitely apply here, and I'm inclined to make it so.

Also, in some cases we allow the function to be called with a
NULL but then make it a no-op rather than throwing an ERROR (eg, if the
OID ends up being NULL).

Thoughts on it emitting a WARN or NOTICE before returning false?

Not sure if that makes sense here or not
offhand but figured I'd mention it as something to consider.

+     pgcform = (Form_pg_class) GETSTRUCT(ctup);
+     pgcform->reltuples = PG_GETARG_FLOAT4(1);
+     pgcform->relpages = PG_GETARG_INT32(2);

Shouldn't we include relallvisible?

Yes. No idea why I didn't have that in there from the start.

Also, perhaps we should use the approach that we have in ANALYZE, and
only actually do something if the values are different rather than just
always doing an update.

That was how it worked back in v1, more for the possibility that there was
no matching JSON to set values.

Looking again at analyze.c (currently lines 1751-1780), we just check if
there is a row in the way, and if so we replace it. I don't see where we
compare existing values to new values.

+/*
+ * Import statistics for a given relation attribute
+ *
+ * pg_set_attribute_stats(relation Oid, attname name, stainherit bool,
+ *                        stanullfrac float4, stawidth int, stadistinct

float4,

+ *                        stakind1 int2, stakind2 int2, stakind3 int3,
+ *                        stakind4 int2, stakind5 int2, stanumbers1

float4[],

+ *                        stanumbers2 float4[], stanumbers3 float4[],
+ *                        stanumbers4 float4[], stanumbers5 float4[],
+ *                        stanumbers1 float4[], stanumbers2 float4[],
+ *                        stanumbers3 float4[], stanumbers4 float4[],
+ *                        stanumbers5 float4[], stavalues1 text,
+ *                        stavalues2 text, stavalues3 text,
+ *                        stavalues4 text, stavalues5 text);
+ *
+ *
+ */

Don't know that it makes sense to just repeat the function declaration
inside a comment like this- it'll just end up out of date.

Historical artifact - previous versions had a long explanation of the JSON
format.

+Datum
+pg_set_attribute_stats(PG_FUNCTION_ARGS)
+     /* names of columns that cannot be null */
+     const char *required_param_names[] = {
+             "relation",
+             "attname",
+             "stainherit",
+             "stanullfrac",
+             "stawidth",
+             "stadistinct",
+             "stakind1",
+             "stakind2",
+             "stakind3",
+             "stakind4",
+             "stakind5",
+     };

Same comment here as above wrt NULL being passed in.

In this case, the last 10 params (stanumbersN and stavaluesN) can be null,
and are NULL more often than not.

+ for (int k = 0; k < 5; k++)

Shouldn't we use STATISTIC_NUM_SLOTS here?

Yes, I had in the past. Not sure why I didn't again.

#50Bharath Rupireddy
bharath.rupireddyforpostgres@gmail.com
In reply to: Corey Huinker (#47)
Re: Statistics Import and Export

On Fri, Mar 8, 2024 at 12:06 PM Corey Huinker <corey.huinker@gmail.com> wrote:

Anyway, here's v7. Eagerly awaiting feedback.

Thanks for working on this. It looks useful to have the ability to
restore the stats after upgrade and restore. But, the exported stats
are valid only until the next ANALYZE is run on the table. IIUC,
postgres collects stats during VACUUM, autovacuum and ANALYZE, right?
Perhaps there are other ways to collect stats. I'm thinking what
problems does the user face if they are just asked to run ANALYZE on
the tables (I'm assuming ANALYZE doesn't block concurrent access to
the tables) instead of automatically exporting stats.

Here are some comments on the v7 patch. I've not dived into the whole
thread, so some comments may be of type repeated or need
clarification. Please bear with me.

1. The following two are unnecessary changes in pg_proc.dat, please remove them.

--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8818,7 +8818,6 @@
 { oid => '3813', descr => 'generate XML text node',
   proname => 'xmltext', proisstrict => 't', prorettype => 'xml',
   proargtypes => 'text', prosrc => 'xmltext' },
-
 { oid => '2923', descr => 'map table contents to XML',
   proname => 'table_to_xml', procost => '100', provolatile => 's',
   proparallel => 'r', prorettype => 'xml',
@@ -12163,8 +12162,24 @@
 # GiST stratnum implementations
 { oid => '8047', descr => 'GiST support',
-  proname => 'gist_stratnum_identity', prorettype => 'int2',
+  proname => 'gist_stratnum_identity',prorettype => 'int2',
   proargtypes => 'int2',
   prosrc => 'gist_stratnum_identity' },
2.
+        they are replaced by the next auto-analyze. This function is used by
+        <command>pg_upgrade</command> and <command>pg_restore</command> to
+        convey the statistics from the old system version into the new one.
+       </para>

Is there any demonstration of pg_set_relation_stats and
pg_set_attribute_stats being used either in pg_upgrade or in
pg_restore? Perhaps, having them as 0002, 0003 and so on patches might
show real need for functions like this. It also clarifies how these
functions pull stats from tables on the old cluster to the tables on
the new cluster.

3. pg_set_relation_stats and pg_set_attribute_stats seem to be writing
to pg_class and might affect the plans as stats can get tampered. Can
we REVOKE the execute permissions from the public out of the box in
src/backend/catalog/system_functions.sql? This way one can decide who
to give permissions to.

4.
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
3.6::float4, 15000);
+ pg_set_relation_stats
+-----------------------
+ t
+(1 row)
+
+SELECT reltuples, relpages FROM pg_class WHERE oid =
'stats_export_import.test'::regclass;
+ reltuples | relpages
+-----------+----------
+       3.6 |    15000

Isn't this test case showing a misuse of these functions? Table
actually has no rows, but we are lying to the postgres optimizer on
stats. I think altering stats of a table mustn't be that easy for the
end user. As mentioned in comment #3, permissions need to be
tightened. In addition, we can also mark the functions pg_upgrade only
with CHECK_IS_BINARY_UPGRADE, but that might not work for pg_restore
(or I don't know if we have a way to know within the server that the
server is running for pg_restore).

5. In continuation to the comment #2, is pg_dump supposed to generate
pg_set_relation_stats and pg_set_attribute_stats statements for each
table? When pg_dump does that , pg_restore can automatically load the
stats.

6.
+/*-------------------------------------------------------------------------
* * statistics.c *
+ * IDENTIFICATION
+ *       src/backend/statistics/statistics.c
+ *
+ *-------------------------------------------------------------------------

A description of what the new file statistics.c does is missing.

7. pgindent isn't happy with new file statistics.c, please check.

8.
+/*
+ * Import statistics for a given relation attribute
+ *
+ * pg_set_attribute_stats(relation Oid, attname name, stainherit bool,
+ *                        stanullfrac float4, stawidth int, stadistinct float4,

Having function definition in the function comment isn't necessary -
it's hard to keep it consistent with pg_proc.dat in future. If
required, one can either look at pg_proc.dat or docs.

9. Isn't it good to add a test case where the plan of a query on table
after exporting the stats would remain same as that of the original
table from which the stats are exported? IMO, this is a more realistic
than just comparing pg_statistic of the tables because this is what an
end-user wants eventually.

--
Bharath Rupireddy
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com

#51Corey Huinker
corey.huinker@gmail.com
In reply to: Bharath Rupireddy (#50)
Re: Statistics Import and Export

On Sun, Mar 10, 2024 at 11:57 AM Bharath Rupireddy <
bharath.rupireddyforpostgres@gmail.com> wrote:

On Fri, Mar 8, 2024 at 12:06 PM Corey Huinker <corey.huinker@gmail.com>
wrote:

Anyway, here's v7. Eagerly awaiting feedback.

Thanks for working on this. It looks useful to have the ability to
restore the stats after upgrade and restore. But, the exported stats
are valid only until the next ANALYZE is run on the table. IIUC,
postgres collects stats during VACUUM, autovacuum and ANALYZE, right?
Perhaps there are other ways to collect stats. I'm thinking what
problems does the user face if they are just asked to run ANALYZE on
the tables (I'm assuming ANALYZE doesn't block concurrent access to
the tables) instead of automatically exporting stats.

Correct. These are just as temporary as any other analyze of the table.
Another analyze will happen later, probably through autovacuum and wipe out
these values. This is designed to QUICKLY get stats into a table to enable
the database to be operational sooner. This is especially important after
an upgrade/restore, when all stats were wiped out. Other uses could be
adapting this for use the postgres_fdw so that we don't have to do table
sampling on the remote table, and of course statistics injection to test
the query planner.

2.
+        they are replaced by the next auto-analyze. This function is used
by
+        <command>pg_upgrade</command> and <command>pg_restore</command> to
+        convey the statistics from the old system version into the new
one.
+       </para>

Is there any demonstration of pg_set_relation_stats and
pg_set_attribute_stats being used either in pg_upgrade or in
pg_restore? Perhaps, having them as 0002, 0003 and so on patches might
show real need for functions like this. It also clarifies how these
functions pull stats from tables on the old cluster to the tables on
the new cluster.

That code was adapted from do_analyze(), and yes, there is a patch for
pg_dump, but as I noted earlier it is on hold pending feedback.

3. pg_set_relation_stats and pg_set_attribute_stats seem to be writing
to pg_class and might affect the plans as stats can get tampered. Can
we REVOKE the execute permissions from the public out of the box in
src/backend/catalog/system_functions.sql? This way one can decide who
to give permissions to.

You'd have to be the table owner to alter the stats. I can envision these
functions getting a special role, but they could also be fine as
superuser-only.

4.
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
3.6::float4, 15000);
+ pg_set_relation_stats
+-----------------------
+ t
+(1 row)
+
+SELECT reltuples, relpages FROM pg_class WHERE oid =
'stats_export_import.test'::regclass;
+ reltuples | relpages
+-----------+----------
+       3.6 |    15000

Isn't this test case showing a misuse of these functions? Table
actually has no rows, but we are lying to the postgres optimizer on
stats.

Consider this case. You want to know at what point the query planner will
start using a given index. You can generate dummy data for a thousand, a
million, a billion rows, and wait for that to complete, or you can just
tell the table "I say you have a billion rows, twenty million pages, etc"
and see when it changes.

But again, in most cases, you're setting the values to the same values the
table had on the old database just before the restore/upgrade.

I think altering stats of a table mustn't be that easy for the
end user.

Only easy for the end users that happen to be the table owner or a
superuser.

As mentioned in comment #3, permissions need to be
tightened. In addition, we can also mark the functions pg_upgrade only
with CHECK_IS_BINARY_UPGRADE, but that might not work for pg_restore
(or I don't know if we have a way to know within the server that the
server is running for pg_restore).

I think they will have usage both in postgres_fdw and for tuning.

5. In continuation to the comment #2, is pg_dump supposed to generate
pg_set_relation_stats and pg_set_attribute_stats statements for each
table? When pg_dump does that , pg_restore can automatically load the
stats.

Current plan is to have one TOC entry in the post-data section with a
dependency on the table/index/matview. That let's us leverage existing
filters. The TOC entry will have a series of statements in it, one
pg_set_relation_stats() and one pg_set_attribute_stats() per attribute.

9. Isn't it good to add a test case where the plan of a query on table
after exporting the stats would remain same as that of the original
table from which the stats are exported? IMO, this is a more realistic
than just comparing pg_statistic of the tables because this is what an
end-user wants eventually.

I'm sure we can add something like that, but query plan formats change a
lot and are greatly dependent on database configuration, so maintaining
such a test would be a lot of work.

#52Bertrand Drouvot
bertranddrouvot.pg@gmail.com
In reply to: Corey Huinker (#47)
Re: Statistics Import and Export

Hi,

On Fri, Mar 08, 2024 at 01:35:40AM -0500, Corey Huinker wrote:

Anyway, here's v7. Eagerly awaiting feedback.

Thanks!

A few random comments:

1 ===

+        The purpose of this function is to apply statistics values in an
+        upgrade situation that are "good enough" for system operation until

Worth to add a few words about "influencing" the planner use case?

2 ===

+#include "catalog/pg_type.h"
+#include "fmgr.h"

Are those 2 needed?

3 ===

+       if (!HeapTupleIsValid(ctup))
+               elog(ERROR, "pg_class entry for relid %u vanished during statistics import",

s/during statistics import/when setting statistics/?

4 ===

+Datum
+pg_set_relation_stats(PG_FUNCTION_ARGS)
+{
.
.
+       table_close(rel, ShareUpdateExclusiveLock);
+
+       PG_RETURN_BOOL(true);

Why returning a bool? (I mean we'd throw an error or return true).

5 ===

+ */
+Datum
+pg_set_attribute_stats(PG_FUNCTION_ARGS)
+{

This function is not that simple, worth to explain its logic in a comment above?

6 ===

+       if (!HeapTupleIsValid(tuple))
+       {
+               relation_close(rel, NoLock);
+               PG_RETURN_BOOL(false);
+       }
+
+       attr = (Form_pg_attribute) GETSTRUCT(tuple);
+       if (attr->attisdropped)
+       {
+               ReleaseSysCache(tuple);
+               relation_close(rel, NoLock);
+               PG_RETURN_BOOL(false);
+       }

Why is it returning "false" and not throwing an error? (if ok, then I think
we can get rid of returning a bool).

7 ===

+        * If this relation is an index and that index has expressions in
+        * it, and the attnum specified

s/is an index and that index has/is an index that has/?

Regards,

--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com

#53Stephen Frost
sfrost@snowman.net
In reply to: Corey Huinker (#49)
Re: Statistics Import and Export

Greetings,

* Corey Huinker (corey.huinker@gmail.com) wrote:

+/*
+ * Set statistics for a given pg_class entry.
+ *
+ * pg_set_relation_stats(relation Oid, reltuples double, relpages int)
+ *
+ * This does an in-place (i.e. non-transactional) update of pg_class,

just as

+ * is done in ANALYZE.
+ *
+ */
+Datum
+pg_set_relation_stats(PG_FUNCTION_ARGS)
+{
+     const char *param_names[] = {
+             "relation",
+             "reltuples",
+             "relpages",
+     };
+
+     Oid                             relid;
+     Relation                rel;
+     HeapTuple               ctup;
+     Form_pg_class   pgcform;
+
+     for (int i = 0; i <= 2; i++)
+             if (PG_ARGISNULL(i))
+                     ereport(ERROR,
+

(errcode(ERRCODE_INVALID_PARAMETER_VALUE),

+ errmsg("%s cannot be NULL",

param_names[i])));

Why not just mark this function as strict..? Or perhaps we should allow
NULLs to be passed in and just not update the current value in that
case?

Strict could definitely apply here, and I'm inclined to make it so.

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

Also, in some cases we allow the function to be called with a
NULL but then make it a no-op rather than throwing an ERROR (eg, if the
OID ends up being NULL).

Thoughts on it emitting a WARN or NOTICE before returning false?

Eh, I don't think so?

Where this is coming from is that we can often end up with functions
like these being called inside of larger queries, and having them spit
out WARN or NOTICE will just make them noisy.

That leads to my general feeling of just returning NULL if called with a
NULL OID, as we would get with setting the function strict.

Not sure if that makes sense here or not
offhand but figured I'd mention it as something to consider.

+     pgcform = (Form_pg_class) GETSTRUCT(ctup);
+     pgcform->reltuples = PG_GETARG_FLOAT4(1);
+     pgcform->relpages = PG_GETARG_INT32(2);

Shouldn't we include relallvisible?

Yes. No idea why I didn't have that in there from the start.

Ok.

Also, perhaps we should use the approach that we have in ANALYZE, and
only actually do something if the values are different rather than just
always doing an update.

That was how it worked back in v1, more for the possibility that there was
no matching JSON to set values.

Looking again at analyze.c (currently lines 1751-1780), we just check if
there is a row in the way, and if so we replace it. I don't see where we
compare existing values to new values.

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

vacuum.c:1531
/* If anything changed, write out the tuple. */
if (dirty)
heap_inplace_update(rd, ctup);

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

+Datum
+pg_set_attribute_stats(PG_FUNCTION_ARGS)
+     /* names of columns that cannot be null */
+     const char *required_param_names[] = {
+             "relation",
+             "attname",
+             "stainherit",
+             "stanullfrac",
+             "stawidth",
+             "stadistinct",
+             "stakind1",
+             "stakind2",
+             "stakind3",
+             "stakind4",
+             "stakind5",
+     };

Same comment here as above wrt NULL being passed in.

In this case, the last 10 params (stanumbersN and stavaluesN) can be null,
and are NULL more often than not.

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

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

+ for (int k = 0; k < 5; k++)

Shouldn't we use STATISTIC_NUM_SLOTS here?

Yes, I had in the past. Not sure why I didn't again.

No worries.

Thanks!

Stephen

#54Corey Huinker
corey.huinker@gmail.com
In reply to: Stephen Frost (#53)
1 attachment(s)
Re: Statistics Import and Export

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

v8 has it as strict.

Also, in some cases we allow the function to be called with a
NULL but then make it a no-op rather than throwing an ERROR (eg, if the
OID ends up being NULL).

Thoughts on it emitting a WARN or NOTICE before returning false?

Eh, I don't think so?

Where this is coming from is that we can often end up with functions
like these being called inside of larger queries, and having them spit
out WARN or NOTICE will just make them noisy.

That leads to my general feeling of just returning NULL if called with a
NULL OID, as we would get with setting the function strict.

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

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

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

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

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

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

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

(What I'm trying to drive at here is a consistent interface for these

functions, but one which does a no-op instead of returning an ERROR on
values being passed in which aren't allowable; it can be quite
frustrating trying to get a query to work where one of the functions
decides to return ERROR instead of just ignoring things passed in which
aren't valid.)

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

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

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

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

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

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

Or one compound command

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

The second one has the feature that if any one attribute fails, then the
whole update fails, except, of course, for the in-place update of pg_class.
This avoids having an explicit transaction block, but we could get that
back by having restore wrap the list of commands in a transaction block
(and adding the explicit lock commands) when it is safe to do so.

Attachments:

v8-0001-Create-pg_set_relation_stats-pg_set_attribute_sta.patchtext/x-patch; charset=US-ASCII; name=v8-0001-Create-pg_set_relation_stats-pg_set_attribute_sta.patchDownload
From bdfde573f4f79770439a1455c1cb337701eb20dc Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Mon, 11 Mar 2024 14:18:39 -0400
Subject: [PATCH v8] Create pg_set_relation_stats, pg_set_attribute_stats.

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

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

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

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

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

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

#55Stephen Frost
sfrost@snowman.net
In reply to: Corey Huinker (#54)
Re: Statistics Import and Export

Greetings,

* Corey Huinker (corey.huinker@gmail.com) wrote:

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

v8 has it as strict.

Ok.

Also, in some cases we allow the function to be called with a
NULL but then make it a no-op rather than throwing an ERROR (eg, if the
OID ends up being NULL).

Thoughts on it emitting a WARN or NOTICE before returning false?

Eh, I don't think so?

Where this is coming from is that we can often end up with functions
like these being called inside of larger queries, and having them spit
out WARN or NOTICE will just make them noisy.

That leads to my general feeling of just returning NULL if called with a
NULL OID, as we would get with setting the function strict.

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

I can see it both ways and don't feel super strongly about it ... I just
know that I've had some cases where we returned an ERROR or otherwise
were a bit noisy on NULL values getting passed into a function and it
was much more on the annoying side than on the helpful side; to the
point where we've gone back and pulled out ereport(ERROR) calls from
functions before because they were causing issues in otherwise pretty
reasonable queries (consider things like functions getting pushed down
to below WHERE clauses and such...).

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

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

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

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

Sure. Not a huge deal either way, was just pointing out the difference.
I do think it'd be good to match what ANALYZE does here, so checking if
the values in pg_class are different and only updating if they are,
while keeping the code for pg_statistic where it'll just always update.

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

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

That'd be my suggestion, at least, but as I mention above, it's not a
position I hold very strongly.

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

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

Don't know that I really consider that to be the same kind of thing when
it comes to talking about the interface as the other aspects we're
discussing ...

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

That would happen before we even get to being called and there's not
much to do about it anyway.

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

Seems reasonable to me.

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

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

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

I'm guessing the above was intended to be SELECT ..; SELECT ..;

Or one compound command

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

The second one has the feature that if any one attribute fails, then the
whole update fails, except, of course, for the in-place update of pg_class.
This avoids having an explicit transaction block, but we could get that
back by having restore wrap the list of commands in a transaction block
(and adding the explicit lock commands) when it is safe to do so.

Hm, I like this approach as it should essentially give us the
transaction block we had been talking about wanting but without needing
to explicitly do a begin/commit, which would add in some annoying
complications. This would hopefully also reduce the locking concern
mentioned previously, since we'd get the lock needed in the first
function call and then the others would be able to just see that we've
already got the lock pretty quickly.

Subject: [PATCH v8] Create pg_set_relation_stats, pg_set_attribute_stats.

[...]

+Datum
+pg_set_relation_stats(PG_FUNCTION_ARGS)

[...]

+	ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(ctup))
+		elog(ERROR, "pg_class entry for relid %u vanished during statistics import",
+			 relid);

Maybe drop the 'during statistics import' part of this message? Also
wonder if maybe we should make it a regular ereport() instead, since it
might be possible for a user to end up seeing this?

+	pgcform = (Form_pg_class) GETSTRUCT(ctup);
+
+	reltuples = PG_GETARG_FLOAT4(P_RELTUPLES);
+	relpages = PG_GETARG_INT32(P_RELPAGES);
+	relallvisible = PG_GETARG_INT32(P_RELALLVISIBLE);
+
+	/* Do not update pg_class unless there is no meaningful change */

This comment doesn't seem quite right. Maybe it would be better if it
was in the positive, eg: Only update pg_class if there is a meaningful
change.

Rest of it looks pretty good to me, at least.

Thanks!

Stephen

#56Corey Huinker
corey.huinker@gmail.com
In reply to: Stephen Frost (#55)
Re: Statistics Import and Export

In which case we're failing nearly silently, yes, there is a null

returned,

but we have no idea why there is a null returned. If I were using this
function manually I'd want to know what I did wrong, what parameter I
skipped, etc.

I can see it both ways and don't feel super strongly about it ... I just
know that I've had some cases where we returned an ERROR or otherwise
were a bit noisy on NULL values getting passed into a function and it
was much more on the annoying side than on the helpful side; to the
point where we've gone back and pulled out ereport(ERROR) calls from
functions before because they were causing issues in otherwise pretty
reasonable queries (consider things like functions getting pushed down
to below WHERE clauses and such...).

I don't have strong feelings either. I think we should get more input on
this. Regardless, it's easy to change...for now.

Sure. Not a huge deal either way, was just pointing out the difference.
I do think it'd be good to match what ANALYZE does here, so checking if
the values in pg_class are different and only updating if they are,
while keeping the code for pg_statistic where it'll just always update.

I agree that mirroring ANALYZE wherever possible is the ideal.

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

Don't know that I really consider that to be the same kind of thing when
it comes to talking about the interface as the other aspects we're
discussing ...

Fair.

One persistent problem is that there is no _safe equivalent to ARRAY_IN,

so

that can always fail on us, though it should only do so if the string
passed in wasn't a valid array input format, or the values in the array
can't coerce to the attribute's basetype.

That would happen before we even get to being called and there's not
much to do about it anyway.

Not sure I follow you here. the ARRAY_IN function calls happen once for
every non-null stavaluesN parameter, and it's done inside the function
because the result type could be the base type for a domain/array type, or
could be the type itself. I suppose we could move that determination to the
caller, but then we'd need to call get_base_element_type() inside a client,
and that seems wrong if it's even possible.

I should also point out that we've lost the ability to check if the

export

values were of a type, and if the destination column is also of that

type.

That's a non-issue in binary upgrades, but of course if a field changed
from integers to text the histograms would now be highly misleading.
Thoughts on adding a typname parameter that the function uses as a cheap
validity check?

Seems reasonable to me.

I'd like to hear what Tomas thinks about this, as he was the initial
advocate for it.

As for pg_dump, I'm currently leading toward the TOC entry having either

a

series of commands:

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

I'm guessing the above was intended to be SELECT ..; SELECT ..;

Yes.

Or one compound command

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

The second one has the feature that if any one attribute fails, then the
whole update fails, except, of course, for the in-place update of

pg_class.

This avoids having an explicit transaction block, but we could get that
back by having restore wrap the list of commands in a transaction block
(and adding the explicit lock commands) when it is safe to do so.

Hm, I like this approach as it should essentially give us the
transaction block we had been talking about wanting but without needing
to explicitly do a begin/commit, which would add in some annoying
complications. This would hopefully also reduce the locking concern
mentioned previously, since we'd get the lock needed in the first
function call and then the others would be able to just see that we've
already got the lock pretty quickly.

True, we'd get the lock needed in the first function call, but wouldn't we
also release that very lock before the subsequent call? Obviously we'd be
shrinking the window in which another process could get in line and take a
superior lock, and the universe of other processes that would even want a
lock that blocks us is nil in the case of an upgrade, identical to existing
behavior in the case of an FDW ANALYZE, and perfectly fine in the case of
someone tinkering with stats.

Subject: [PATCH v8] Create pg_set_relation_stats, pg_set_attribute_stats.

[...]

+Datum
+pg_set_relation_stats(PG_FUNCTION_ARGS)

[...]

+     ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+     if (!HeapTupleIsValid(ctup))
+             elog(ERROR, "pg_class entry for relid %u vanished during

statistics import",

+ relid);

Maybe drop the 'during statistics import' part of this message? Also
wonder if maybe we should make it a regular ereport() instead, since it
might be possible for a user to end up seeing this?

Agreed and agreed. It was copypasta from ANALYZE.

This comment doesn't seem quite right. Maybe it would be better if it
was in the positive, eg: Only update pg_class if there is a meaningful
change.

+1

#57Stephen Frost
sfrost@snowman.net
In reply to: Corey Huinker (#56)
Re: Statistics Import and Export

Greetings,

* Corey Huinker (corey.huinker@gmail.com) wrote:

One persistent problem is that there is no _safe equivalent to ARRAY_IN,

so

that can always fail on us, though it should only do so if the string
passed in wasn't a valid array input format, or the values in the array
can't coerce to the attribute's basetype.

That would happen before we even get to being called and there's not
much to do about it anyway.

Not sure I follow you here. the ARRAY_IN function calls happen once for
every non-null stavaluesN parameter, and it's done inside the function
because the result type could be the base type for a domain/array type, or
could be the type itself. I suppose we could move that determination to the
caller, but then we'd need to call get_base_element_type() inside a client,
and that seems wrong if it's even possible.

Ah, yeah, ok, I see what you're saying here and sure, there's a risk
those might ERROR too, but that's outright invalid data then as opposed
to a NULL getting passed in.

Or one compound command

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

The second one has the feature that if any one attribute fails, then the
whole update fails, except, of course, for the in-place update of

pg_class.

This avoids having an explicit transaction block, but we could get that
back by having restore wrap the list of commands in a transaction block
(and adding the explicit lock commands) when it is safe to do so.

Hm, I like this approach as it should essentially give us the
transaction block we had been talking about wanting but without needing
to explicitly do a begin/commit, which would add in some annoying
complications. This would hopefully also reduce the locking concern
mentioned previously, since we'd get the lock needed in the first
function call and then the others would be able to just see that we've
already got the lock pretty quickly.

True, we'd get the lock needed in the first function call, but wouldn't we
also release that very lock before the subsequent call? Obviously we'd be
shrinking the window in which another process could get in line and take a
superior lock, and the universe of other processes that would even want a
lock that blocks us is nil in the case of an upgrade, identical to existing
behavior in the case of an FDW ANALYZE, and perfectly fine in the case of
someone tinkering with stats.

No, we should be keeping the lock until the end of the transaction
(which in this case would be just the one statement, but it would be the
whole statement and all of the calls in it). See analyze.c:268 or
so, where we call relation_close(onerel, NoLock); meaning we're closing
the relation but we're *not* releasing the lock on it- it'll get
released at the end of the transaction.

Thanks!

Stephen

#58Corey Huinker
corey.huinker@gmail.com
In reply to: Stephen Frost (#57)
Re: Statistics Import and Export

No, we should be keeping the lock until the end of the transaction
(which in this case would be just the one statement, but it would be the
whole statement and all of the calls in it). See analyze.c:268 or
so, where we call relation_close(onerel, NoLock); meaning we're closing
the relation but we're *not* releasing the lock on it- it'll get
released at the end of the transaction.

If that's the case, then changing the two table_close() statements to
NoLock should resolve any remaining concern.

#59Stephen Frost
sfrost@snowman.net
In reply to: Corey Huinker (#58)
Re: Statistics Import and Export

Greetings,

* Corey Huinker (corey.huinker@gmail.com) wrote:

No, we should be keeping the lock until the end of the transaction
(which in this case would be just the one statement, but it would be the
whole statement and all of the calls in it). See analyze.c:268 or
so, where we call relation_close(onerel, NoLock); meaning we're closing
the relation but we're *not* releasing the lock on it- it'll get
released at the end of the transaction.

If that's the case, then changing the two table_close() statements to
NoLock should resolve any remaining concern.

Note that there's two different things we're talking about here- the
lock on the relation that we're analyzing and then the lock on the
pg_statistic (or pg_class) catalog itself. Currently, at least, it
looks like in the three places in the backend that we open
StatisticRelationId, we release the lock when we close it rather than
waiting for transaction end. I'd be inclined to keep it that way in
these functions also. I doubt that one lock will end up causing much in
the way of issues to acquire/release it multiple times and it would keep
the code consistent with the way ANALYZE works.

If it can be shown to be an issue then we could certainly revisit this.

Thanks,

Stephen

#60Corey Huinker
corey.huinker@gmail.com
In reply to: Stephen Frost (#59)
Re: Statistics Import and Export

Note that there's two different things we're talking about here- the
lock on the relation that we're analyzing and then the lock on the
pg_statistic (or pg_class) catalog itself. Currently, at least, it
looks like in the three places in the backend that we open
StatisticRelationId, we release the lock when we close it rather than
waiting for transaction end. I'd be inclined to keep it that way in
these functions also. I doubt that one lock will end up causing much in
the way of issues to acquire/release it multiple times and it would keep
the code consistent with the way ANALYZE works.

ANALYZE takes out one lock StatisticRelationId per relation, not per
attribute like we do now. If we didn't release the lock after every
attribute, and we only called the function outside of a larger transaction
(as we plan to do with pg_restore) then that is the closest we're going to
get to being consistent with ANALYZE.

#61Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#60)
2 attachment(s)
Re: Statistics Import and Export

ANALYZE takes out one lock StatisticRelationId per relation, not per
attribute like we do now. If we didn't release the lock after every
attribute, and we only called the function outside of a larger transaction
(as we plan to do with pg_restore) then that is the closest we're going to
get to being consistent with ANALYZE.

v9 attached. This adds pg_dump support. It works in tests against existing
databases such as dvdrental, though I was surprised at how few indexes have
attribute stats there.

Statistics are preserved by default, but this can be disabled with the
option --no-statistics. This follows the prevailing option pattern in
pg_dump, etc.

There are currently several failing TAP tests around
pg_dump/pg_restore/pg_upgrade. I'm looking at those, but in the mean
time I'm seeking feedback on the progress so far.

Attachments:

v9-0001-Create-pg_set_relation_stats-pg_set_attribute_sta.patchtext/x-patch; charset=US-ASCII; name=v9-0001-Create-pg_set_relation_stats-pg_set_attribute_sta.patchDownload
From bf291e323fc01215264d41b75d579c11bd22d2ec Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Mon, 11 Mar 2024 14:18:39 -0400
Subject: [PATCH v9 1/2] Create pg_set_relation_stats, pg_set_attribute_stats.

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

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

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

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

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

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

v9-0002-Enable-dumping-of-table-index-stats-in-pg_dump.patchtext/x-patch; charset=US-ASCII; name=v9-0002-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From 7ff026f3bb1a7ffb663a7221a1a9a69df33a3e7a Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 14 Mar 2024 04:42:41 -0400
Subject: [PATCH v9 2/2] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will also generate a statement
that calls all of the pg_set_relation_stats() and
pg_set_attribute_stats() calls necessary to restore the statistics of
the current system onto the destination system.

As is the pattern with pg_dump options, this can be disabled with
--no-statistics.
---
 src/include/fe_utils/stats_export.h  |  38 +++++
 src/fe_utils/Makefile                |   1 +
 src/fe_utils/meson.build             |   1 +
 src/fe_utils/stats_export.c          | 239 +++++++++++++++++++++++++++
 src/bin/pg_dump/pg_backup.h          |   2 +
 src/bin/pg_dump/pg_backup_archiver.c |   5 +
 src/bin/pg_dump/pg_dump.c            |  60 +++++++
 src/bin/pg_dump/pg_dump.h            |   1 +
 src/bin/pg_dump/pg_dumpall.c         |   5 +
 src/bin/pg_dump/pg_restore.c         |   3 +
 10 files changed, 355 insertions(+)
 create mode 100644 src/include/fe_utils/stats_export.h
 create mode 100644 src/fe_utils/stats_export.c

diff --git a/src/include/fe_utils/stats_export.h b/src/include/fe_utils/stats_export.h
new file mode 100644
index 0000000000..eb2141d639
--- /dev/null
+++ b/src/include/fe_utils/stats_export.h
@@ -0,0 +1,38 @@
+/*-------------------------------------------------------------------------
+ *
+ * stats_export.h
+ *    Queries to export statistics from current and past versions.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1995, Regents of the University of California
+ *
+ * src/include/varatt.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef STATS_EXPORT_H
+#define STATS_EXPORT_H
+
+#include "postgres_fe.h"
+#include "libpq-fe.h"
+
+/*
+ * The minimum supported version number. No attempt is made to get statistics
+ * import to work on versions older than this. This version was initially chosen
+ * because that was the minimum version supported by pg_dump at the time.
+ */
+#define MIN_SERVER_NUM 90200
+
+extern bool exportStatsSupported(int server_version_num);
+extern bool exportExtStatsSupported(int server_version_num);
+
+extern const char *exportClassStatsSQL(int server_verson_num);
+extern const char *exportAttributeStatsSQL(int server_verson_num);
+
+extern char *exportRelationStatsSQL(int server_version_num);
+extern char *exportRelationStatsStmt(PGconn *conn, const char *srcrel,
+									 const char *destrel);
+
+#endif
diff --git a/src/fe_utils/Makefile b/src/fe_utils/Makefile
index 946c05258f..c734f9f6d3 100644
--- a/src/fe_utils/Makefile
+++ b/src/fe_utils/Makefile
@@ -32,6 +32,7 @@ OBJS = \
 	query_utils.o \
 	recovery_gen.o \
 	simple_list.o \
+	stats_export.o \
 	string_utils.o
 
 ifeq ($(PORTNAME), win32)
diff --git a/src/fe_utils/meson.build b/src/fe_utils/meson.build
index 14d0482a2c..fce503f641 100644
--- a/src/fe_utils/meson.build
+++ b/src/fe_utils/meson.build
@@ -12,6 +12,7 @@ fe_utils_sources = files(
   'query_utils.c',
   'recovery_gen.c',
   'simple_list.c',
+  'stats_export.c',
   'string_utils.c',
 )
 
diff --git a/src/fe_utils/stats_export.c b/src/fe_utils/stats_export.c
new file mode 100644
index 0000000000..cd2b38172c
--- /dev/null
+++ b/src/fe_utils/stats_export.c
@@ -0,0 +1,239 @@
+/*-------------------------------------------------------------------------
+ *
+ * Utility functions for extracting object statistics for frontend code
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/fe_utils/stats_export.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include "fe_utils/stats_export.h"
+/*
+#include "libpq/libpq-fs.h"
+*/
+#include "fe_utils/string_utils.h"
+
+/*
+ * No-frills catalog queries that are named according to the statistics they
+ * fetch (relation, attribute, extended) and the earliest server version for
+ * which they work. These are presented so that if other use cases arise they
+ * can share the same base queries but utilize them in their own way.
+ *
+ * The queries themselves do not filter results, so it is up to the caller
+ * to append a WHERE clause filtering either on either c.oid or a combination
+ * of c.relname and n.nspname.
+ */
+
+const char *export_class_stats_query_v9_2 =
+	"SELECT c.oid, n.nspname, c.relname, c.reltuples, c.relpages, c.relallvisible "
+	"FROM pg_class AS c "
+	"JOIN pg_namespace AS n ON n.oid = c.relnamespace ";
+
+const char *export_attribute_stats_query_v9_2 =
+	"SELECT c.oid, n.nspname, c.relname, a.attnum, a.attname, s.stainherit, "
+	"s.stanullfrac, s.stawidth, s.stadistinct, s.stakind1, s.stakind2, "
+	"s.stakind3, s.stakind4, s.stakind5, s.stanumbers1, s.stanumbers2, "
+	"s.stanumbers3, s.stanumbers4, s.stanumbers5, "
+	"s.stavalues1::text AS stavalues1, s.stavalues2::text AS stavalues2, "
+	"s.stavalues3::text AS stavalues3, s.stavalues4::text AS stavalues4, "
+	"s.stavalues5::text AS stavalues5 "
+	"FROM pg_class AS c "
+	"JOIN pg_namespace AS n ON n.oid = c.relnamespace "
+	"JOIN pg_attribute AS a ON a.attrelid = c.oid AND not a.attisdropped "
+	"JOIN pg_statistic s ON s.starelid = a.attrelid AND s.staattnum = a.attnum ";
+
+/*
+ * Returns true if the server version number supports exporting regular
+ * (e.g. pg_statistic) statistics.
+ */
+bool
+exportStatsSupported(int server_version_num)
+{
+	return (server_version_num >= MIN_SERVER_NUM);
+}
+
+/*
+ * Returns true if the server version number supports exporting extended
+ * (e.g. pg_statistic_ext, pg_statitic_ext_data) statistics.
+ *
+ * Currently, none do.
+ */
+bool
+exportExtStatsSupported(int server_version_num)
+{
+	return false;
+}
+
+/*
+ * Return the query appropriate for extracting relation statistics for the
+ * given server version, if one exists.
+ */
+const char *
+exportClassStatsSQL(int server_version_num)
+{
+	if (server_version_num >= MIN_SERVER_NUM)
+		return export_class_stats_query_v9_2;
+	return NULL;
+}
+
+/*
+ * Return the query appropriate for extracting attribute statistics for the
+ * given server version, if one exists.
+ */
+const char *
+exportAttributeStatsSQL(int server_version_num)
+{
+	if (server_version_num >= MIN_SERVER_NUM)
+		return export_attribute_stats_query_v9_2;
+	return NULL;
+}
+
+/*
+ * Generate a SQL statement that will itself generate a SQL statement to
+ * import all regular stats from a given relation into another relation.
+ *
+ * The query generated takes two parameters.
+ *
+ * $1 is of type Oid, and represents the oid of the source relation.
+ *
+ * $2 is is a cstring, and represents the qualified name of the destination
+ * relation. If NULL, then the qualified name of the source relation will
+ * be used. In either case, the value is casted via ::regclass.
+ *
+ * The function will return NULL for invalid server version numbers.
+ * Otherwise,
+ *
+ * This function needs to work on databases back to 9.2.
+ * The format() function was introduced in 9.1.
+ * The string_agg() aggregate was introduced in 9.0.
+ *
+ */
+char *exportRelationStatsSQL(int server_version_num)
+{
+	const char *relsql = exportClassStatsSQL(server_version_num);
+	const char *attrsql = exportAttributeStatsSQL(server_version_num);
+	const char *filter = "WHERE c.oid = $1::regclass";
+	char	   *s;
+	PQExpBuffer sql;
+
+	if ((relsql == NULL) || (attrsql == NULL))
+		return NULL;
+
+	/*
+	 * Set up the initial CTEs each with the same oid filter
+	 */
+	sql = createPQExpBuffer();
+	appendPQExpBuffer(sql,
+					  "WITH r AS (%s %s), a AS (%s %s), ",
+					  relsql, filter, attrsql, filter);
+
+	/*
+	 * Generate the pg_set_relation_stats function call for the relation
+	 * and one pg_set_attribute_stats function call for each attribute with
+	 * a pg_statistic entry. Give each row an order value such that the
+	 * set relation stats call will be first, followed by the set attribute
+	 * stats calls in attnum order (even though the attributes are identified
+	 * by attname).
+	 *
+	 * Then aggregate the function calls into a single SELECT statement that
+	 * puts the calls in the order described above.
+	 */
+	appendPQExpBufferStr(sql,
+		"s(ord,sql) AS ( "
+			"SELECT 0, format('pg_catalog.pg_set_relation_stats("
+			"%L::regclass, %s::float4, %s::integer, %s::integer)', "
+			"coalesce($2, format('%I.%I', r.nspname, r.relname)), "
+			"r.reltuples, r.relpages, r.relallvisible) "
+			"FROM r "
+			"UNION ALL "
+			"SELECT a.attnum, format('pg_catalog.pg_set_attribute_stats( "
+			"relation => %L::regclass, attname => %L::name, "
+			"stainherit => %L::boolean, stanullfrac => %s::real, "
+			"stawidth => %s::integer, stadistinct => %s::real, "
+			"stakind1 => %s::smallint, stakind2 => %s::smallint, "
+			"stakind3 => %s::smallint, stakind4 => %s::smallint, "
+			"stakind5 => %s::smallint, "
+			"stanumbers1 => %L::real[], stanumbers2 => %L::real[], "
+			"stanumbers3 => %L::real[], stanumbers4 => %L::real[], "
+			"stanumbers5 => %L::real[], "
+			"stavalues1 => %L::text, stavalues2 => %L::text, "
+			"stavalues3 => %L::text, stavalues4 => %L::text, "
+			"stavalues5 => %L::text)', "
+			"coalesce($2, format('%I.%I', a.nspname, a.relname)), "
+			"a.attname, a.stainherit, a.stanullfrac, a.stawidth, "
+			"a.stadistinct, a.stakind1, a.stakind2, a.stakind3, a.stakind4, "
+			"a.stakind5, a.stanumbers1, a.stanumbers2, a.stanumbers3, "
+			"a.stanumbers4, a.stanumbers5, a.stavalues1, a.stavalues2, "
+			"a.stavalues3, a.stavalues4, a.stavalues5) "
+			"FROM a "
+		") "
+		"SELECT 'SELECT ' || string_agg(s.sql, ', ' ORDER BY s.ord) "
+		"FROM s ");
+
+	s = strdup(sql->data);
+	destroyPQExpBuffer(sql);
+	return s;
+}
+
+/*
+ * Return the SQL command that will import all relation stats and attribute
+ * stats for a given relation.
+ *
+ * The return string is malloc()ed and must be freed by the caller.
+ *
+ * If the database is a version before 9.2, the function will return NULL
+ * with no corresponding error message. This can be verified beforehand via
+ * exportStatisticsSupported().
+ *
+ * Any other errors will result in the function returning NULL and the error
+ * message can be found via PQerrorMessage(conn).
+ *
+ */
+char *
+exportRelationStatsStmt(PGconn *conn, const char *srcrel, const char *destrel)
+{
+	const char *stmtname = "relstats";
+	const char *values[] = { srcrel, destrel };
+
+	static bool prepared = false;
+
+	PGresult   *res;
+	char	   *out = NULL;
+
+	/*
+	 * Most use-cases for this function involve invoking this function once for
+	 * each of a large set of relations. As such, using a prepapred statement 
+	 * make sense.
+	 */
+	if (!prepared)
+	{
+		char *sql = exportRelationStatsSQL(PQserverVersion(conn));
+
+		if (sql == NULL)
+			return NULL;
+
+		res = PQprepare(conn, stmtname, sql, 2, NULL);
+		if (res == NULL || PQresultStatus(res) != PGRES_COMMAND_OK)
+		{
+			PQclear(res);
+			return NULL;
+		}
+
+		free(sql);
+		prepared = true;
+	}
+
+	res = PQexecPrepared(conn, stmtname, 2, values, NULL, NULL, 0);
+
+	/* Result set must be 1x1 */
+	if ((PQresultStatus(res) == PGRES_TUPLES_OK) && (PQntuples(res) == 1))
+		out = strdup(PQgetvalue(res, 0, 0));
+
+	PQclear(res);
+	return out;
+}
diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 9ef2f2017e..1db5cf52eb 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -112,6 +112,7 @@ typedef struct _restoreOptions
 	int			no_publications;	/* Skip publication entries */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
@@ -179,6 +180,7 @@ typedef struct _dumpOptions
 	int			no_security_labels;
 	int			no_publications;
 	int			no_subscriptions;
+	int			no_statistics;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index d97ebaff5b..d5f61399d9 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2833,6 +2833,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's a stats dump, maybe ignore it */
+	if (ropt->no_statistics && strcmp(te->desc, "STATISTICS") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2862,6 +2866,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 171e591696..5a23034c7a 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -59,6 +59,7 @@
 #include "compress_io.h"
 #include "dumputils.h"
 #include "fe_utils/option_utils.h"
+#include "fe_utils/stats_export.h"
 #include "fe_utils/string_utils.h"
 #include "filter.h"
 #include "getopt_long.h"
@@ -425,6 +426,7 @@ main(int argc, char **argv)
 		{"no-comments", no_argument, &dopt.no_comments, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -1130,6 +1132,7 @@ help(const char *progname)
 	printf(_("  --no-comments                do not dump comments\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -6981,6 +6984,7 @@ getTables(Archive *fout, int *numTables)
 
 		/* Tables have data */
 		tblinfo[i].dobj.components |= DUMP_COMPONENT_DATA;
+		tblinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
 
 		/* Mark whether table has an ACL */
 		if (!PQgetisnull(res, i, i_relacl))
@@ -7478,6 +7482,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -10224,6 +10229,53 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const DumpableObject *dobj, char *reltypename)
+{
+	PGconn     *conn;
+	const char *qualname;
+	char	   *stmt;
+	PQExpBuffer query;
+	PQExpBuffer tag;
+
+	/* do nothing, if --no-statistics is supplied */
+	if (fout->dopt->no_statistics)
+		return;
+
+	conn = GetConnection(fout);
+
+	qualname = fmtQualifiedId(dobj->namespace->dobj.name,
+							  dobj->name);
+
+	stmt = exportRelationStatsStmt(conn, qualname, NULL);
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", reltypename,
+					  fmtId(dobj->name));
+
+	query = createPQExpBuffer();
+	appendPQExpBufferStr(query, stmt);
+	free(stmt);
+	appendPQExpBufferStr(query, ";\n");
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATS IMPORT",
+							  .section = SECTION_POST_DATA,
+							  .createStmt = query->data,
+							  .deps = &(dobj->dumpId),
+							  .nDeps = 1));
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -16657,6 +16709,10 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 	if (tbinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
 		dumpTableSecLabel(fout, tbinfo, reltypename);
 
+	/* Statistics are dependent on the definition, not the data */
+	if (tbinfo->dobj.dump & DUMP_COMPONENT_STATISTICS)
+		dumpRelationStats(fout, &tbinfo->dobj, reltypename);
+
 	/* Dump comments on inlined table constraints */
 	for (j = 0; j < tbinfo->ncheck; j++)
 	{
@@ -16979,6 +17035,10 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 					is_constraint ? indxinfo->indexconstraint :
 					indxinfo->dobj.dumpId);
 
+	/* Dump Index Stats */
+	if (indxinfo->dobj.dump & DUMP_COMPONENT_STATISTICS)
+		dumpRelationStats(fout, &indxinfo->dobj, "INDEX");
+
 	destroyPQExpBuffer(q);
 	destroyPQExpBuffer(delq);
 	free(qindxname);
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 9bc93520b4..d6a071ec28 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -101,6 +101,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 491311fe79..37b6ba8a49 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -105,6 +105,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -174,6 +175,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -453,6 +455,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -668,6 +672,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index c3beacdec1..2d326dec72 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -75,6 +75,7 @@ main(int argc, char **argv)
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
 	static int	no_subscriptions = 0;
+	static int  no_statistics = 0;
 	static int	strict_names = 0;
 
 	struct option cmdopts[] = {
@@ -126,6 +127,7 @@ main(int argc, char **argv)
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
+		{"no-statistics", no_argument, &no_statistics, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -358,6 +360,7 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
-- 
2.44.0

#62Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#61)
Re: Statistics Import and Export

On Fri, 2024-03-15 at 03:55 -0400, Corey Huinker wrote:

Statistics are preserved by default, but this can be disabled with
the option --no-statistics. This follows the prevailing option
pattern in pg_dump, etc.

I'm not sure if saving statistics should be the default in 17. I'm
inclined to make it opt-in.

There are currently several failing TAP tests around
pg_dump/pg_restore/pg_upgrade.

It is a permissions problem. When user running pg_dump is not the
superuser, they don't have permission to access pg_statistic. That
causes an error in exportRelationStatsStmt(), which returns NULL, and
then the caller segfaults.

I'm looking at those, but in the mean time I'm seeking feedback on
the progress so far.

Still looking, but one quick comment is that the third argument of
dumpRelationStats() should be const, which eliminates a warning.

Regards,
Jeff Davis

#63Jeff Davis
pgsql@j-davis.com
In reply to: Jeff Davis (#62)
Re: Statistics Import and Export

On Fri, 2024-03-15 at 15:30 -0700, Jeff Davis wrote:

Still looking, but one quick comment is that the third argument of
dumpRelationStats() should be const, which eliminates a warning.

A few other comments:

* pg_set_relation_stats() needs to do an ACL check so you can't set the
stats on someone else's table. I suggest honoring the new MAINTAIN
privilege as well.

* If possible, reading from pg_stats (instead of pg_statistic) would be
ideal because pg_stats already does the right checks at read time, so a
non-superuser can export stats, too.

* If reading from pg_stats, should you change the signature of
pg_set_relation_stats() to have argument names matching the columns of
pg_stats (e.g. most_common_vals instead of stakind/stavalues)?

In other words, make this a slightly higher level: conceptually
exporting/importing pg_stats rather than pg_statistic. This may also
make the SQL export queries simpler.

Also, I'm wondering about error handling. Is some kind of error thrown
by pg_set_relation_stats() going to abort an entire restore? That might
be easy to prevent with pg_restore, because it can just omit the stats,
but harder if it's in a SQL file.

Regards,
Jeff Davis

#64Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#63)
2 attachment(s)
Re: Statistics Import and Export

* pg_set_relation_stats() needs to do an ACL check so you can't set the
stats on someone else's table. I suggest honoring the new MAINTAIN
privilege as well.

Added.

* If possible, reading from pg_stats (instead of pg_statistic) would be
ideal because pg_stats already does the right checks at read time, so a
non-superuser can export stats, too.

Done. That was sorta how it was originally, so returning to that wasn't too
hard.

* If reading from pg_stats, should you change the signature of
pg_set_relation_stats() to have argument names matching the columns of
pg_stats (e.g. most_common_vals instead of stakind/stavalues)?

Done.

In other words, make this a slightly higher level: conceptually
exporting/importing pg_stats rather than pg_statistic. This may also
make the SQL export queries simpler.

Eh, about the same.

Also, I'm wondering about error handling. Is some kind of error thrown
by pg_set_relation_stats() going to abort an entire restore? That might
be easy to prevent with pg_restore, because it can just omit the stats,
but harder if it's in a SQL file.

Aside from the oid being invalid, there's not a whole lot that can go wrong
in set_relation_stats(). The error checking I did closely mirrors that in
analyze.c.

Aside from the changes you suggested, as well as the error reporting change
you suggested for pg_dump, I also filtered out attempts to dump stats on
views.

A few TAP tests are still failing and I haven't been able to diagnose why,
though the failures in parallel dump seem to be that it tries to import
stats on indexes that haven't been created yet, which is odd because I sent
the dependency.

All those changes are available in the patches attached.

Attachments:

v10-0001-Create-pg_set_relation_stats-pg_set_attribute_st.patchtext/x-patch; charset=US-ASCII; name=v10-0001-Create-pg_set_relation_stats-pg_set_attribute_st.patchDownload
From ba411ce31c25193cf05bc4206dcb8f2bf32af0c7 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Mon, 11 Mar 2024 14:18:39 -0400
Subject: [PATCH v10 1/2] Create pg_set_relation_stats, pg_set_attribute_stats.

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

Both functions take an oid to identify the target relation that will
receive the statistics. There is nothing requiring that relation to be
the same one as the one exported, though the statistics would have to
make sense in the context of the new relation.

The parameters of pg_set_attribute_stats intentionaly mirror the columns
in the view pg_stats, with the ANYARRAY types casted to TEXT. Those
values will be cast to arrays of the basetype of the attribute, and that
operation may fail if the attribute type has changed.

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

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

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 700f7daf7b..3070d72d7a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12170,4 +12170,19 @@
   proargtypes => 'int2',
   prosrc => 'gist_stratnum_identity' },
 
+# Statistics Import
+{ oid => '8048',
+  descr => 'set statistics on relation',
+  proname => 'pg_set_relation_stats', provolatile => 'v', proisstrict => 't',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'oid float4 int4 int4',
+  proargnames => '{relation,reltuples,relpages,relallvisible}',
+  prosrc => 'pg_set_relation_stats' },
+{ oid => '8049',
+  descr => 'set statistics on attribute',
+  proname => 'pg_set_attribute_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'oid name bool float4 int4 float4 text _float4 text float4 text _float4 _float4',
+  proargnames => '{relation,attname,inherited,null_frac,avg_width,n_distinct,most_common_vals,most_common_freqs,histogram_bounds,correlation,most_common_elems,most_common_elem_freqs,elem_count_histogram}',
+  prosrc => 'pg_set_attribute_stats' },
 ]
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 7f2bf18716..1dddf96576 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -127,4 +127,6 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												int nclauses);
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
+extern Datum pg_set_relation_stats(PG_FUNCTION_ARGS);
+extern Datum pg_set_attribute_stats(PG_FUNCTION_ARGS);
 #endif							/* STATISTICS_H */
diff --git a/src/backend/statistics/Makefile b/src/backend/statistics/Makefile
index 89cf8c2797..e4f8ab7c4f 100644
--- a/src/backend/statistics/Makefile
+++ b/src/backend/statistics/Makefile
@@ -16,6 +16,7 @@ OBJS = \
 	dependencies.o \
 	extended_stats.o \
 	mcv.o \
-	mvdistinct.o
+	mvdistinct.o \
+	statistics.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/statistics/meson.build b/src/backend/statistics/meson.build
index 73b29a3d50..331e82c776 100644
--- a/src/backend/statistics/meson.build
+++ b/src/backend/statistics/meson.build
@@ -5,4 +5,5 @@ backend_sources += files(
   'extended_stats.c',
   'mcv.c',
   'mvdistinct.c',
+  'statistics.c',
 )
diff --git a/src/backend/statistics/statistics.c b/src/backend/statistics/statistics.c
new file mode 100644
index 0000000000..19a4a740df
--- /dev/null
+++ b/src/backend/statistics/statistics.c
@@ -0,0 +1,521 @@
+/*-------------------------------------------------------------------------
+ * statistics.c
+ *
+ *	  POSTGRES statistics import
+ *
+ * Code supporting the direct importation of relation statistics, similar to
+ * what is done by the ANALYZE command.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *       src/backend/statistics/statistics.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "catalog/pg_database.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_type.h"
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "nodes/nodeFuncs.h"
+#include "parser/parse_oper.h"
+#include "statistics/statistics.h"
+#include "utils/acl.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/syscache.h"
+
+/*
+ * A role has privileges to vacuum or analyze the relation if any of the
+ * following are true:
+ *   - the role owns the current database and the relation is not shared
+ *   - the role has the MAINTAIN privilege on the relation
+ *
+ */
+static bool
+canModifyRelation(Oid relid, Form_pg_class reltuple)
+{
+	return ((object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId())
+			 && !reltuple->relisshared) ||
+			 pg_class_aclcheck(relid, GetUserId(), ACL_MAINTAIN) == ACLCHECK_OK);
+}
+
+/*
+ * Set statistics for a given pg_class entry.
+ *
+ * This does an in-place (i.e. non-transactional) update of pg_class, just as
+ * is done in ANALYZE.
+ *
+ */
+Datum
+pg_set_relation_stats(PG_FUNCTION_ARGS)
+{
+	enum
+	{
+		P_RELATION = 0,			/* oid */
+		P_RELTUPLES,			/* float4 */
+		P_RELPAGES,				/* int */
+		P_RELALLVISIBLE,		/* int */
+		P_NUM_PARAMS
+	};
+
+	Oid			relid;
+	Relation	rel;
+	HeapTuple	ctup;
+	Form_pg_class pgcform;
+	float4		reltuples;
+	int			relpages;
+	int			relallvisible;
+
+	/* The function is strict, but just to be safe */
+	Assert(!PG_ARGISNULL(P_RELATION) && !PG_ARGISNULL(P_RELTUPLES) &&
+		   !PG_ARGISNULL(P_RELPAGES) && !PG_ARGISNULL(P_RELALLVISIBLE));
+
+	relid = PG_GETARG_OID(P_RELATION);
+
+	/*
+	 * Open the relation, getting ShareUpdateExclusiveLock to ensure that no
+	 * other stat-setting operation can run on it concurrently.
+	 */
+	rel = table_open(RelationRelationId, ShareUpdateExclusiveLock);
+
+	/*
+	 * TODO: choose an error code appropriate for this situation.
+	 * Canidates are:
+	 * ERRCODE_INVALID_CATALOG_NAME
+	 * ERRCODE_UNDEFINED_TABLE
+	 * ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE
+	 * ERRCODE_OBJECT_IN_USE
+	 * ERRCODE_SYSTEM_ERROR
+	 * ERRCODE_INTERNAL_ERROR
+	 */
+	ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(ctup))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_IN_USE),
+				 errmsg("pg_class entry for relid %u not found", relid)));
+
+	pgcform = (Form_pg_class) GETSTRUCT(ctup);
+
+	if (!canModifyRelation(relid, pgcform))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						 RelationGetRelationName(rel))));
+
+
+	reltuples = PG_GETARG_FLOAT4(P_RELTUPLES);
+	relpages = PG_GETARG_INT32(P_RELPAGES);
+	relallvisible = PG_GETARG_INT32(P_RELALLVISIBLE);
+
+	/* Only update pg_class if there is a meaningful change */
+	if ((pgcform->reltuples != reltuples)
+		|| (pgcform->relpages != relpages)
+		|| (pgcform->relallvisible != relallvisible))
+	{
+		pgcform->reltuples = PG_GETARG_FLOAT4(P_RELTUPLES);
+		pgcform->relpages = PG_GETARG_INT32(P_RELPAGES);
+		pgcform->relallvisible = PG_GETARG_INT32(P_RELALLVISIBLE);
+
+		heap_inplace_update(rel, ctup);
+	}
+
+	table_close(rel, NoLock);
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * Convenience wrapper to confirm that the user can modify the relation.
+ * Use this when there is no need to modify the pg_class entry itself.
+ */
+static void
+checkCanModifyRelation(Relation rel)
+{
+	Oid				relid;
+	HeapTuple		tup;
+	Form_pg_class	pgcform;
+
+	relid = RelationGetRelid(rel);
+	tup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for relation %u", relid);
+
+	pgcform = (Form_pg_class) GETSTRUCT(tup);
+
+	if (!canModifyRelation(relid, pgcform))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						 RelationGetRelationName(rel))));
+
+	ReleaseSysCache(tup);
+}
+
+
+/*
+ * Perform the cast of text to some array type
+ */
+static Datum
+cast_stavalue(FmgrInfo *finfo, Datum d, Oid typid, int32 typmod)
+{
+	char   *s = TextDatumGetCString(d);
+	Datum out = FunctionCall3(finfo, CStringGetDatum(s),
+							  ObjectIdGetDatum(typid),
+							  Int32GetDatum(typmod));
+
+	pfree(s);
+
+	return out;
+}
+
+
+/*
+ * Import statistics for a given relation attribute.
+ *
+ * This will insert/replace a row in pg_statistic for the given relation and
+ * attribute name.
+ *
+ * This function will return NULL if either the relation or the attname are
+ * NULL.
+ *
+ * If there is no attribute with a matching attname in the relation, the
+ * function will return false. If there is a matching attribute but the
+ * attribute is dropped, then function will return false.
+ *
+ * The function does not specify values for staopN or stacollN parameters
+ * because those values are determined by the corresponding stakindN value and
+ * the attribute's underlying datatype.
+ *
+ * The stavaluesN parameters are text values, and must be a valid input string
+ * for an array of the basetype of the attribute. Any error generated by the
+ * array_in() function will in turn fail the function.
+ */
+Datum
+pg_set_attribute_stats(PG_FUNCTION_ARGS)
+{
+	enum
+	{
+		P_RELATION = 0,		/* oid */
+		P_ATTNAME,			/* name */
+		P_INHERITED,		/* bool */
+		P_NULL_FRAC,		/* float4 */
+		P_AVG_WIDTH,			/* int32 */
+		P_N_DISTINCT,		/* float4 */
+		P_MC_VALS,			/* text, null */
+		P_MC_FREQS,			/* float4[], null */
+		P_HIST_BOUNDS,		/* text, null */
+		P_CORRELATION,		/* float4, null */
+		P_MC_ELEMS,			/* text, null */
+		P_MC_ELEM_FREQS,	/* float4[], null */
+		P_ELEM_COUNT_HIST,	/* float[], null */
+		P_NUM_PARAMS
+	};
+
+	/* names of columns that cannot be null */
+	const char *required_param_names[] = {
+		"relation",
+		"attname",
+		"stainherit",
+		"stanullfrac",
+		"stawidth",
+		"stadistinct"
+	};
+
+	Oid			relid;
+	Name		attname;
+	Relation	rel;
+	HeapTuple	atup;
+
+	Oid			typid;
+	int32		typmod;
+	Oid			typcoll;
+	Oid			eqopr;
+	Oid			ltopr;
+	Oid			basetypid;
+	Oid			baseeqopr;
+	Oid			baseltopr;
+
+	float4		stanullfrac;
+	int			stawidth;
+
+	Datum		values[Natts_pg_statistic] = {0};
+	bool		nulls[Natts_pg_statistic] = {false};
+
+	Relation	sd;
+	HeapTuple	oldtup;
+	CatalogIndexState indstate;
+	HeapTuple	stup;
+	Form_pg_attribute attr;
+
+	FmgrInfo	finfo;
+
+	int k = 0;
+
+	/*
+	 * This function is pseudo-strct in that a NULL for the relation oid or
+	 * the attribute name results is a NULL return value.
+	 */
+	if (PG_ARGISNULL(P_RELATION) || PG_ARGISNULL(P_ATTNAME))
+		PG_RETURN_NULL();
+
+	for (int i = P_INHERITED; i <= P_N_DISTINCT; i++)
+		if (PG_ARGISNULL(i))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be NULL", required_param_names[i])));
+
+	relid = PG_GETARG_OID(P_RELATION);
+
+	attname = PG_GETARG_NAME(P_ATTNAME);
+
+	rel = relation_open(relid, ShareUpdateExclusiveLock);
+	checkCanModifyRelation(rel);
+
+	sd = table_open(StatisticRelationId, RowExclusiveLock);
+	checkCanModifyRelation(sd);
+
+	atup = SearchSysCache2(ATTNAME,
+						   ObjectIdGetDatum(relid),
+						   NameGetDatum(attname));
+
+	if (!HeapTupleIsValid(atup))
+	{
+		/* Attribute not found nowhere to import the stats to */
+		relation_close(rel, NoLock);
+		PG_RETURN_BOOL(false);
+	}
+
+	attr = (Form_pg_attribute) GETSTRUCT(atup);
+	if (attr->attisdropped)
+	{
+		ReleaseSysCache(atup);
+		relation_close(rel, NoLock);
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * If this relation is an index and that index has expressions in it, and
+	 * the attnum specified is known to be an expression, then we must walk
+	 * the list attributes up to the specified attnum to get the right
+	 * expression.
+	 */
+	if ((rel->rd_rel->relkind == RELKIND_INDEX
+		 || (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+		&& (rel->rd_indexprs != NIL)
+		&& (rel->rd_index->indkey.values[attr->attnum - 1] == 0))
+	{
+		ListCell   *indexpr_item = list_head(rel->rd_indexprs);
+		Node	   *expr;
+
+		for (int i = 0; i < attr->attnum - 1; i++)
+			if (rel->rd_index->indkey.values[i] == 0)
+				indexpr_item = lnext(rel->rd_indexprs, indexpr_item);
+
+		if (indexpr_item == NULL)	/* shouldn't happen */
+			elog(ERROR, "too few entries in indexprs list");
+
+		expr = (Node *) lfirst(indexpr_item);
+
+		typid = exprType(expr);
+		typmod = exprTypmod(expr);
+
+		/*
+		 * If a collation has been specified for the index column, use that in
+		 * preference to anything else; but if not, fall back to whatever we
+		 * can get from the expression.
+		 */
+		if (OidIsValid(attr->attcollation))
+			typcoll = attr->attcollation;
+		else
+			typcoll = exprCollation(expr);
+	}
+	else
+	{
+		typid = attr->atttypid;
+		typmod = attr->atttypmod;
+		typcoll = attr->attcollation;
+	}
+
+	get_sort_group_operators(typid, false, false, false,
+							 &ltopr, &eqopr, NULL, NULL);
+
+	basetypid = get_base_element_type(typid);
+
+	if (basetypid == InvalidOid)
+	{
+		/* type is its own base type */
+		basetypid = typid;
+		baseltopr = ltopr;
+		baseeqopr = eqopr;
+	}
+	else
+		get_sort_group_operators(basetypid, false, false, false,
+								 &baseltopr, &baseeqopr, NULL, NULL);
+
+	stanullfrac = PG_GETARG_FLOAT4(P_NULL_FRAC);
+	if ((stanullfrac < 0.0) || (stanullfrac > 1.0))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("stanullfrac %f is out of range 0.0 to 1.0", stanullfrac)));
+
+	stawidth = PG_GETARG_INT32(P_AVG_WIDTH);
+	if (stawidth < 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("stawidth %d must be >= 0", stawidth)));
+
+	values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(relid);
+	values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(attr->attnum);
+	values[Anum_pg_statistic_stainherit - 1] = PG_GETARG_DATUM(P_INHERITED);
+	values[Anum_pg_statistic_stanullfrac - 1] = PG_GETARG_DATUM(P_NULL_FRAC);
+	values[Anum_pg_statistic_stawidth - 1] = PG_GETARG_DATUM(P_AVG_WIDTH);
+	values[Anum_pg_statistic_stadistinct - 1] = PG_GETARG_DATUM(P_N_DISTINCT);
+
+	fmgr_info(F_ARRAY_IN, &finfo);
+
+	/* MC_VALS || MC_FREQS => STATISTIC_KIND_MCV */
+	if ((!PG_ARGISNULL(P_MC_VALS)) || (!PG_ARGISNULL(P_MC_FREQS)))
+	{
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_MCV);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(eqopr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+
+		if (PG_ARGISNULL(P_MC_FREQS))
+			nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		else
+			values[Anum_pg_statistic_stanumbers1 - 1 + k] =
+				PG_GETARG_DATUM(P_MC_FREQS);
+
+		if (PG_ARGISNULL(P_MC_VALS))
+			nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+		else
+			values[Anum_pg_statistic_stavalues1 - 1 + k] =
+				cast_stavalue(&finfo, PG_GETARG_DATUM(P_MC_VALS), basetypid, typmod);
+
+		k++;
+	}
+
+	/* HIST_BOUNDS => STATISTIC_KIND_HISTOGRAM */
+	if (!PG_ARGISNULL(P_HIST_BOUNDS))
+	{
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_HISTOGRAM);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(ltopr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		values[Anum_pg_statistic_stavalues1 - 1 + k] =
+			cast_stavalue(&finfo, PG_GETARG_DATUM(P_HIST_BOUNDS), basetypid, typmod);
+
+		k++;
+	}
+
+	/* CORRELATION => STATISTIC_KIND_CORRELATION */
+	if (!PG_ARGISNULL(P_CORRELATION))
+	{
+		Datum		elems[] = { PG_GETARG_DATUM(P_CORRELATION) };
+        ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_CORRELATION);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(ltopr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] = PointerGetDatum(arry);
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+		k++;
+	}
+
+	/* MC_ELEMS || MC_ELEM_FREQS => STATISTIC_KIND_MCELEM */
+	if ((!PG_ARGISNULL(P_MC_ELEMS)) || (!PG_ARGISNULL(P_MC_ELEM_FREQS)))
+	{
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_MCELEM); 
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(baseeqopr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+
+		if (PG_ARGISNULL(P_MC_ELEM_FREQS))
+			nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		else
+			values[Anum_pg_statistic_stanumbers1 - 1 + k] =
+				PG_GETARG_DATUM(P_MC_ELEM_FREQS);
+
+		if (PG_ARGISNULL(P_MC_ELEMS))
+			nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+		else
+			values[Anum_pg_statistic_stavalues1 - 1 + k] =
+				cast_stavalue(&finfo, PG_GETARG_DATUM(P_MC_ELEMS),
+							  basetypid, typmod);
+
+		k++;
+	}
+
+	/* ELEM_COUNT_HIST => STATISTIC_KIND_DECHIST */
+	if (!PG_ARGISNULL(P_ELEM_COUNT_HIST))
+	{
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_DECHIST); 
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(baseeqopr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] =
+			PG_GETARG_DATUM(P_ELEM_COUNT_HIST);
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+		k++;
+	}
+
+	/* fill in all remaining slots */
+	for(; k < STATISTIC_NUM_SLOTS; k++)
+	{
+		values[Anum_pg_statistic_stakind1 - 1 + k] = Int16GetDatum(0);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+	}
+
+	/* Is there already a pg_statistic tuple for this attribute? */
+	oldtup = SearchSysCache3(STATRELATTINH,
+							 ObjectIdGetDatum(relid),
+							 Int16GetDatum(attr->attnum),
+							 PG_GETARG_DATUM(P_INHERITED));
+
+	indstate = CatalogOpenIndexes(sd);
+
+	if (HeapTupleIsValid(oldtup))
+	{
+		/* Yes, replace it */
+		bool		replaces[Natts_pg_statistic] = {true};
+
+		stup = heap_modify_tuple(oldtup, RelationGetDescr(sd),
+								 values, nulls, replaces);
+		ReleaseSysCache(oldtup);
+		CatalogTupleUpdateWithInfo(sd, &stup->t_self, stup, indstate);
+	}
+	else
+	{
+		/* No, insert new tuple */
+		stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
+		CatalogTupleInsertWithInfo(sd, stup, indstate);
+	}
+
+	heap_freetuple(stup);
+	CatalogCloseIndexes(indstate);
+	table_close(sd, NoLock);
+	relation_close(rel, NoLock);
+	ReleaseSysCache(atup);
+	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..e7af39bf51
--- /dev/null
+++ b/src/test/regress/expected/stats_export_import.out
@@ -0,0 +1,211 @@
+CREATE SCHEMA stats_export_import;
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    tags text[]
+);
+SELECT reltuples, relpages, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ reltuples | relpages | relallvisible 
+-----------+----------+---------------
+        -1 |        0 |             0
+(1 row)
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass, 3.6::real, 15000, 999);
+ pg_set_relation_stats 
+-----------------------
+ t
+(1 row)
+
+SELECT reltuples, relpages, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ reltuples | relpages | relallvisible 
+-----------+----------+---------------
+       3.6 |    15000 |           999
+(1 row)
+
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type, array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, NULL;
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+   relname    | num_stats 
+--------------+-----------
+ is_odd       |         1
+ is_odd_clone |         1
+ test         |         4
+ test_clone   |         4
+(4 rows)
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 1d8a414eea..0c89ffc02d 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -103,7 +103,7 @@ test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combo
 # ----------
 # Another group of parallel tests (JSON related)
 # ----------
-test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson
+test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson stats_export_import
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/sql/stats_export_import.sql b/src/test/regress/sql/stats_export_import.sql
new file mode 100644
index 0000000000..fabfc9b666
--- /dev/null
+++ b/src/test/regress/sql/stats_export_import.sql
@@ -0,0 +1,188 @@
+CREATE SCHEMA stats_export_import;
+
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    tags text[]
+);
+
+SELECT reltuples, relpages, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass, 3.6::real, 15000, 999);
+
+SELECT reltuples, relpages, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type, array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, NULL;
+
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+
+SELECT
+    format('SELECT pg_catalog.pg_set_attribute_stats( '
+            || 'relation => %L::regclass::oid, attname => %L::name, '
+            || 'inherited => %L::boolean, null_frac => %s::real, '
+            || 'avg_width => %s::integer, n_distinct => %s::real, '
+            || 'most_common_vals => %L::text, '
+            || 'most_common_freqs => %L::real[], '
+            || 'histogram_bounds => %L::text, '
+            || 'correlation => %s::real, '
+            || 'most_common_elems => %L::text, '
+            || 'most_common_elem_freqs => %L::real[], '
+            || 'elem_count_histogram => %L::real[]) ',
+        'stats_export_import.' || s.tablename || '_clone', s.attname,
+        s.inherited, s.null_frac, s.avg_width, s.n_distinct,
+        s.most_common_vals, s.most_common_freqs, s.histogram_bounds,
+        s.correlation, s.most_common_elems, s.most_common_elem_freqs,
+        s.elem_count_histogram)
+FROM pg_catalog.pg_stats AS s
+WHERE s.schemaname = 'stats_export_import'
+AND s.tablename IN ('test', 'is_odd')
+\gexec
+
+-- restore ECHO to original value
+\set ECHO :orig_ECHO
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 3c52d90d3a..fdb05a0498 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -28835,6 +28835,89 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
     in identifying the specific disk files associated with database objects.
    </para>
 
+   <table id="functions-admin-statsimport">
+    <title>Database Object Statistics Import Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_relation_stats</primary>
+        </indexterm>
+        <function>pg_set_relation_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>reltuples</parameter> <type>real</type>,
+         <parameter>relpages</parameter> <type>integer</type> )
+         <parameter>relallvisible</parameter> <type>integer</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Updates the <structname>pg_class</structname> row for the specified
+        <parameter>relation</parameter>, setting the values for the columns
+        <structfield>reltuples</structfield>,
+        <structfield>relpages</structfield>, and
+        <structfield>relallvisible</structfield>.
+        To avoid table bloat in <structname>pg_class</structname>, this change
+        is made with an in-place update, and therefore cannot be rolled back through
+        normal transaction processing.
+       </para>
+       <para>
+        The purpose of this function is to apply statistics values in an
+        upgrade situation that are "good enough" for system operation until
+        they are replaced by the next auto-analyze. This function is used by
+        <command>pg_upgrade</command> and <command>pg_restore</command> to
+        convey the statistics from the old system version into the new one.
+       </para>
+       </entry>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_attribute_stats</primary>
+        </indexterm>
+        <function>pg_set_attribute_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>attname</parameter> <type>name</type>,
+         <parameter>inherited</parameter> <type>boolean</type>,
+         <parameter>null_frac</parameter> <type>real</type>,
+         <parameter>avg_width</parameter> <type>integer</type>,
+         <parameter>n_distinct</parameter> <type>real</type>,
+         <parameter>most_common_vals</parameter> <type>text</type>,
+         <parameter>most_common_freqs</parameter> <type>real[]</type>,
+         <parameter>histogram_bounds</parameter> <type>text</type>,
+         <parameter>correlation</parameter> <type>real</type>,
+         <parameter>most_common_elems</parameter> <type>text</type>,
+         <parameter>most_common_elem_freqs</parameter> <type>real[]</type>,
+         <parameter>elem_count_histogram</parameter> <type>real[]</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Replaces the <structname>pg_statistic</structname> row for the
+        <structname>pg_attribute</structname> row specified by
+        <parameter>relation</parameter> and <parameter>attname</parameter>.
+       </para>
+       <para>
+        The purpose of this function is to apply statistics values in an
+        upgrade situation that are "good enough" for system operation until
+        they are replaced by the next auto-analyze. This function is used by
+        <command>pg_upgrade</command> and <command>pg_restore</command> to
+        convey the statistics from the old system version into the new one.
+       </para>
+       </entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
    <table id="functions-admin-dblocation">
     <title>Database Object Location Functions</title>
     <tgroup cols="1">
-- 
2.44.0

v10-0002-Enable-dumping-of-table-index-stats-in-pg_dump.patchtext/x-patch; charset=US-ASCII; name=v10-0002-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From ab5556cb26af437ea7b3837c94988c2587db95ca Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH v10 2/2] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will also generate a statement
that calls all of the pg_set_relation_stats() and
pg_set_attribute_stats() calls necessary to restore the statistics of
the current system onto the destination system.

As is the pattern with pg_dump options, this can be disabled with
--no-statistics.
---
 src/include/fe_utils/stats_export.h  |  36 ++++++
 src/fe_utils/Makefile                |   1 +
 src/fe_utils/meson.build             |   1 +
 src/fe_utils/stats_export.c          | 178 +++++++++++++++++++++++++++
 src/bin/pg_dump/pg_backup.h          |   2 +
 src/bin/pg_dump/pg_backup_archiver.c |   5 +
 src/bin/pg_dump/pg_dump.c            |  91 ++++++++++++++
 src/bin/pg_dump/pg_dump.h            |   1 +
 src/bin/pg_dump/pg_dumpall.c         |   5 +
 src/bin/pg_dump/pg_restore.c         |   3 +
 10 files changed, 323 insertions(+)
 create mode 100644 src/include/fe_utils/stats_export.h
 create mode 100644 src/fe_utils/stats_export.c

diff --git a/src/include/fe_utils/stats_export.h b/src/include/fe_utils/stats_export.h
new file mode 100644
index 0000000000..f0dc7041f7
--- /dev/null
+++ b/src/include/fe_utils/stats_export.h
@@ -0,0 +1,36 @@
+/*-------------------------------------------------------------------------
+ *
+ * stats_export.h
+ *    Queries to export statistics from current and past versions.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1995, Regents of the University of California
+ *
+ * src/include/varatt.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef STATS_EXPORT_H
+#define STATS_EXPORT_H
+
+#include "postgres_fe.h"
+#include "libpq-fe.h"
+
+/*
+ * The minimum supported version number. No attempt is made to get statistics
+ * import to work on versions older than this. This version was initially chosen
+ * because that was the minimum version supported by pg_dump at the time.
+ */
+#define MIN_SERVER_NUM 90200
+
+extern bool exportStatsSupported(int server_version_num);
+extern bool exportExtStatsSupported(int server_version_num);
+
+extern const char *exportClassStatsSQL(int server_verson_num);
+extern const char *exportAttributeStatsSQL(int server_verson_num);
+
+extern char *exportRelationStatsSQL(int server_version_num);
+
+#endif
diff --git a/src/fe_utils/Makefile b/src/fe_utils/Makefile
index 946c05258f..c734f9f6d3 100644
--- a/src/fe_utils/Makefile
+++ b/src/fe_utils/Makefile
@@ -32,6 +32,7 @@ OBJS = \
 	query_utils.o \
 	recovery_gen.o \
 	simple_list.o \
+	stats_export.o \
 	string_utils.o
 
 ifeq ($(PORTNAME), win32)
diff --git a/src/fe_utils/meson.build b/src/fe_utils/meson.build
index 14d0482a2c..fce503f641 100644
--- a/src/fe_utils/meson.build
+++ b/src/fe_utils/meson.build
@@ -12,6 +12,7 @@ fe_utils_sources = files(
   'query_utils.c',
   'recovery_gen.c',
   'simple_list.c',
+  'stats_export.c',
   'string_utils.c',
 )
 
diff --git a/src/fe_utils/stats_export.c b/src/fe_utils/stats_export.c
new file mode 100644
index 0000000000..106907df09
--- /dev/null
+++ b/src/fe_utils/stats_export.c
@@ -0,0 +1,178 @@
+/*-------------------------------------------------------------------------
+ *
+ * Utility functions for extracting object statistics for frontend code
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/fe_utils/stats_export.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include "fe_utils/stats_export.h"
+/*
+#include "libpq/libpq-fs.h"
+*/
+#include "fe_utils/string_utils.h"
+
+/*
+ * No-frills catalog queries that are named according to the statistics they
+ * fetch (relation, attribute, extended) and the earliest server version for
+ * which they work. These are presented so that if other use cases arise they
+ * can share the same base queries but utilize them in their own way.
+ *
+ * The queries themselves do not filter results, so it is up to the caller
+ * to append a WHERE clause filtering either on either c.oid or a combination
+ * of c.relname and n.nspname.
+ */
+
+const char *export_class_stats_query_v9_2 =
+	"SELECT c.oid, n.nspname, c.relname, c.reltuples, c.relpages, c.relallvisible "
+	"FROM pg_class AS c "
+	"JOIN pg_namespace AS n ON n.oid = c.relnamespace";
+
+const char *export_attribute_stats_query_v9_2 =
+	"SELECT c.oid, n.nspname, c.relname, a.attnum, a.attname, s.inherited, "
+	"s.null_frac, s.avg_width, s.n_distinct, "
+	"s.most_common_vals::text AS most_common_vals, s.most_common_freqs, "
+	"s.histogram_bounds::text AS histogram_bounds, s.correlation, "
+	"s.most_common_elems::text AS most_common_elems, "
+	"s.most_common_elems_freq, s.elem_count_histogram "
+	"FROM pg_class AS c "
+	"JOIN pg_namespace AS n ON n.oid = c.relnamespace "
+	"JOIN pg_attribute AS a ON a.attrelid = c.oid AND not a.attisdropped "
+	"JOIN pg_stats AS s ON s.schemaname = n.nspname AND s.tablename = c.relname";
+
+/*
+ * Returns true if the server version number supports exporting regular
+ * (e.g. pg_statistic) statistics.
+ */
+bool
+exportStatsSupported(int server_version_num)
+{
+	return (server_version_num >= MIN_SERVER_NUM);
+}
+
+/*
+ * Returns true if the server version number supports exporting extended
+ * (e.g. pg_statistic_ext, pg_statitic_ext_data) statistics.
+ *
+ * Currently, none do.
+ */
+bool
+exportExtStatsSupported(int server_version_num)
+{
+	return false;
+}
+
+/*
+ * Return the query appropriate for extracting relation statistics for the
+ * given server version, if one exists.
+ */
+const char *
+exportClassStatsSQL(int server_version_num)
+{
+	if (server_version_num >= MIN_SERVER_NUM)
+		return export_class_stats_query_v9_2;
+	return NULL;
+}
+
+/*
+ * Return the query appropriate for extracting attribute statistics for the
+ * given server version, if one exists.
+ */
+const char *
+exportAttributeStatsSQL(int server_version_num)
+{
+	if (server_version_num >= MIN_SERVER_NUM)
+		return export_attribute_stats_query_v9_2;
+	return NULL;
+}
+
+/*
+ * Generate a SQL statement that will itself generate a SQL statement to
+ * import all regular stats from a given relation into another relation.
+ *
+ * The query generated takes two parameters.
+ *
+ * $1 is of type Oid, and represents the oid of the source relation.
+ *
+ * $2 is is a cstring, and represents the qualified name of the destination
+ * relation. If NULL, then the qualified name of the source relation will
+ * be used. In either case, the value is casted via ::regclass.
+ *
+ * The function will return NULL for invalid server version numbers.
+ * Otherwise,
+ *
+ * This function needs to work on databases back to 9.2.
+ * The format() function was introduced in 9.1.
+ * The string_agg() aggregate was introduced in 9.0.
+ *
+ */
+char *exportRelationStatsSQL(int server_version_num)
+{
+	const char *relsql = exportClassStatsSQL(server_version_num);
+	const char *attrsql = exportAttributeStatsSQL(server_version_num);
+	const char *filter = "WHERE c.oid = $1::regclass";
+	char	   *s;
+	PQExpBuffer sql;
+
+	if ((relsql == NULL) || (attrsql == NULL))
+		return NULL;
+
+	/*
+	 * Set up the initial CTEs each with the same oid filter
+	 */
+	sql = createPQExpBuffer();
+	appendPQExpBuffer(sql,
+					  "WITH r AS (%s %s), a AS (%s %s), ",
+					  relsql, filter, attrsql, filter);
+
+	/*
+	 * Generate the pg_set_relation_stats function call for the relation
+	 * and one pg_set_attribute_stats function call for each attribute with
+	 * a pg_statistic entry. Give each row an order value such that the
+	 * set relation stats call will be first, followed by the set attribute
+	 * stats calls in attnum order (even though the attributes are identified
+	 * by attname).
+	 *
+	 * Then aggregate the function calls into a single SELECT statement that
+	 * puts the calls in the order described above.
+	 */
+	appendPQExpBufferStr(sql,
+		"s(ord,sql) AS ( "
+			"SELECT 0, format('pg_catalog.pg_set_relation_stats("
+			"%L::regclass, %s::real, %s::integer, %s::integer)', "
+			"coalesce($2, format('%I.%I', r.nspname, r.relname)), "
+			"r.reltuples, r.relpages, r.relallvisible) "
+			"FROM r "
+			"UNION ALL "
+			"SELECT 1, format('pg_catalog.pg_set_attribute_stats( "
+			"relation => %L::regclass, attname => %L::name, "
+			"inherited => %L::boolean, null_frac => %s::real, "
+			"avg_width => %s::integer, n_distinct => %s::real, "
+			"most_common_vals => %L::text, "
+			"most_common_freqs => %L::real[], "
+			"most_common_bounds => %L::text, "
+			"correlation => %s::real, "
+			"most_common_elems => %L::text, "
+			"most_common_elems_freq => %L::real[], "
+			"elem_count_histogram => %L::real[]) "
+			"coalesce($2, format('%I.%I', a.nspname, a.relname)), "
+			"a.attname, a.inherited, a.null_frac, a.avg_width, "
+			"a.n_distinct, a.most_common_vals, a.most_common_freqs, "
+			"a.most_common_bounds, a.correlation, "
+			"a.most_common_elems, a.most_common_elems_freq, "
+			"a.elem_count_histogram) "
+			"FROM a "
+		") "
+		"SELECT 'SELECT ' || string_agg(s.sql, ', ' ORDER BY s.ord) "
+		"FROM s ");
+
+	s = strdup(sql->data);
+	destroyPQExpBuffer(sql);
+	return s;
+}
diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 9ef2f2017e..1db5cf52eb 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -112,6 +112,7 @@ typedef struct _restoreOptions
 	int			no_publications;	/* Skip publication entries */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
@@ -179,6 +180,7 @@ typedef struct _dumpOptions
 	int			no_security_labels;
 	int			no_publications;
 	int			no_subscriptions;
+	int			no_statistics;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index d97ebaff5b..d5f61399d9 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2833,6 +2833,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's a stats dump, maybe ignore it */
+	if (ropt->no_statistics && strcmp(te->desc, "STATISTICS") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2862,6 +2866,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a5149ca823..8c29e2ddf6 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -59,6 +59,7 @@
 #include "compress_io.h"
 #include "dumputils.h"
 #include "fe_utils/option_utils.h"
+#include "fe_utils/stats_export.h"
 #include "fe_utils/string_utils.h"
 #include "filter.h"
 #include "getopt_long.h"
@@ -425,6 +426,7 @@ main(int argc, char **argv)
 		{"no-comments", no_argument, &dopt.no_comments, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -1130,6 +1132,7 @@ help(const char *progname)
 	printf(_("  --no-comments                do not dump comments\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -6981,6 +6984,7 @@ getTables(Archive *fout, int *numTables)
 
 		/* Tables have data */
 		tblinfo[i].dobj.components |= DUMP_COMPONENT_DATA;
+		tblinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
 
 		/* Mark whether table has an ACL */
 		if (!PQgetisnull(res, i, i_relacl))
@@ -7478,6 +7482,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -10227,6 +10232,82 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const DumpableObject *dobj,
+				  const char *reltypename)
+{
+	const char *stmtname = "relstats";
+	static bool prepared = false;
+	const char *values[2];
+	PGconn     *conn;
+	PGresult   *res;
+	PQExpBuffer query;
+	PQExpBuffer tag;
+
+	/* do nothing, if --no-statistics is supplied */
+	if (fout->dopt->no_statistics)
+		return;
+
+	conn = GetConnection(fout);
+
+	if (!prepared)
+	{
+		int		ver = PQserverVersion(conn);
+		char   *sql = exportRelationStatsSQL(ver);
+
+		if (sql == NULL)
+			pg_fatal("could not prepare stats export query for server version %d",
+					 ver);
+
+		res = PQprepare(conn, stmtname, sql, 2, NULL);
+		if (res == NULL || PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("prepared statement failed: %s",
+					 PQerrorMessage(conn));
+
+		free(sql);
+		prepared = true;
+	}
+
+	values[0] = fmtQualifiedId(dobj->namespace->dobj.name, dobj->name);
+	values[1] = NULL;
+	res = PQexecPrepared(conn, stmtname, 2, values, NULL, NULL, 0);
+
+
+	/* Result set must be 1x1 */
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		pg_fatal("error in statistics extraction: %s", PQerrorMessage(conn));
+
+	if (PQntuples(res) != 1)
+		pg_fatal("statistics extraction expected one row, but got %d rows", 
+				 PQntuples(res));
+
+	query = createPQExpBuffer();
+	appendPQExpBufferStr(query, strdup(PQgetvalue(res, 0, 0)));
+	appendPQExpBufferStr(query, ";\n");
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", reltypename,
+					  fmtId(dobj->name));
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATS IMPORT",
+							  .section = SECTION_POST_DATA,
+							  .createStmt = query->data,
+							  .deps = &(dobj->dumpId),
+							  .nDeps = 1));
+
+	PQclear(res);
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -16660,6 +16741,12 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 	if (tbinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
 		dumpTableSecLabel(fout, tbinfo, reltypename);
 
+	/* Statistics are dependent on the definition, not the data */
+	/* Views don't have stats */
+	if ((tbinfo->dobj.dump & DUMP_COMPONENT_STATISTICS) &&
+		(tbinfo->relkind == RELKIND_VIEW))
+		dumpRelationStats(fout, &tbinfo->dobj, reltypename);
+
 	/* Dump comments on inlined table constraints */
 	for (j = 0; j < tbinfo->ncheck; j++)
 	{
@@ -16982,6 +17069,10 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 					is_constraint ? indxinfo->indexconstraint :
 					indxinfo->dobj.dumpId);
 
+	/* Dump Index Stats */
+	if (indxinfo->dobj.dump & DUMP_COMPONENT_STATISTICS)
+		dumpRelationStats(fout, &indxinfo->dobj, "INDEX");
+
 	destroyPQExpBuffer(q);
 	destroyPQExpBuffer(delq);
 	free(qindxname);
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 9bc93520b4..d6a071ec28 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -101,6 +101,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 491311fe79..37b6ba8a49 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -105,6 +105,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -174,6 +175,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -453,6 +455,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -668,6 +672,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index c3beacdec1..2d326dec72 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -75,6 +75,7 @@ main(int argc, char **argv)
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
 	static int	no_subscriptions = 0;
+	static int  no_statistics = 0;
 	static int	strict_names = 0;
 
 	struct option cmdopts[] = {
@@ -126,6 +127,7 @@ main(int argc, char **argv)
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
+		{"no-statistics", no_argument, &no_statistics, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -358,6 +360,7 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
-- 
2.44.0

#65Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#64)
Re: Statistics Import and Export

On Sun, 2024-03-17 at 23:33 -0400, Corey Huinker wrote:

A few TAP tests are still failing and I haven't been able to diagnose
why, though the failures in parallel dump seem to be that it tries to
import stats on indexes that haven't been created yet, which is odd
because I sent the dependency.

From testrun/pg_dump/002_pg_dump/log/regress_log_002_pg_dump, search
for the "not ok" and then look at what it tried to do right before
that. I see:

pg_dump: error: prepared statement failed: ERROR: syntax error at or
near "%"
LINE 1: ..._histogram => %L::real[]) coalesce($2, format('%I.%I',
a.nsp...

All those changes are available in the patches attached.

How about if you provided "get" versions of the functions that return a
set of rows that match what the "set" versions expect? That would make
0001 essentially a complete feature itself.

I think it would also make the changes in pg_dump simpler, and the
tests in 0001 a lot simpler.

Regards,
Jeff Davis

#66Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#65)
Re: Statistics Import and Export

From testrun/pg_dump/002_pg_dump/log/regress_log_002_pg_dump, search
for the "not ok" and then look at what it tried to do right before
that. I see:

pg_dump: error: prepared statement failed: ERROR: syntax error at or
near "%"
LINE 1: ..._histogram => %L::real[]) coalesce($2, format('%I.%I',
a.nsp...

Thanks. Unfamiliar turf for me.

All those changes are available in the patches attached.

How about if you provided "get" versions of the functions that return a
set of rows that match what the "set" versions expect? That would make
0001 essentially a complete feature itself.

That's tricky. At the base level, those functions would just be an
encapsulation of "SELECT * FROM pg_stats WHERE schemaname = $1 AND
tablename = $2" which isn't all that much of a savings. Perhaps we can make
the documentation more explicit about the source and nature of the
parameters going into the pg_set_ functions.

Per conversation, it would be trivial to add a helper functions that
replace the parameters after the initial oid with a pg_class rowtype, and
that would dissect the values needed and call the more complex function:

pg_set_relation_stats( oid, pg_class)
pg_set_attribute_stats( oid, pg_stats)

I think it would also make the changes in pg_dump simpler, and the
tests in 0001 a lot simpler.

I agree. The tests are currently showing that a fidelity copy can be made
from one table to another, but to do so we have to conceal the actual stats
values because those are 1. not deterministic/known and 2. subject to
change from version to version.

I can add some sets to arbitrary values like was done for
pg_set_relation_stats().

#67Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#66)
2 attachment(s)
Re: Statistics Import and Export

v11 attached.

- TAP tests passing (the big glitch was that indexes that are used in
constraints should have their stats dependent on the constraint, not the
index, thanks Jeff)
- The new range-specific statistics types are now supported. I'm not happy
with the typid machinations I do to get them to work, but it is working so
far. These are stored out-of-stakind-order (7 before 6), which is odd
because all other types seem store stakinds in ascending order. It
shouldn't matter, it was just odd.
- regression tests now make simpler calls with arbitrary stats to
demonstrate the function usage more cleanly
- pg_set_*_stats function now have all of their parameters in the same
order as the table/view they pull from

Attachments:

v11-0001-Create-pg_set_relation_stats-pg_set_attribute_st.patchtext/x-patch; charset=US-ASCII; name=v11-0001-Create-pg_set_relation_stats-pg_set_attribute_st.patchDownload
From 5c63ed5748eb3817d193b64329b57dc590e0196e Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Mon, 11 Mar 2024 14:18:39 -0400
Subject: [PATCH v11 1/2] Create pg_set_relation_stats, pg_set_attribute_stats.

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

Both functions take an oid to identify the target relation that will
receive the statistics. There is nothing requiring that relation to be
the same one as the one exported, though the statistics would have to
make sense in the context of the new relation.

The parameters of pg_set_attribute_stats intentionaly mirror the columns
in the view pg_stats, with the ANYARRAY types casted to TEXT. Those
values will be cast to arrays of the basetype of the attribute, and that
operation may fail if the attribute type has changed.

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

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

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 177d81a891..f31412d4a6 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12177,4 +12177,19 @@
   proargtypes => 'int2',
   prosrc => 'gist_stratnum_identity' },
 
+# Statistics Import
+{ oid => '8048',
+  descr => 'set statistics on relation',
+  proname => 'pg_set_relation_stats', provolatile => 'v', proisstrict => 't',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'oid int4 float4 int4',
+  proargnames => '{relation,relpages,reltuples,relallvisible}',
+  prosrc => 'pg_set_relation_stats' },
+{ oid => '8049',
+  descr => 'set statistics on attribute',
+  proname => 'pg_set_attribute_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'oid name bool float4 int4 float4 text _float4 text float4 text _float4 _float4 text float4 text',
+  proargnames => '{relation,attname,inherited,null_frac,avg_width,n_distinct,most_common_vals,most_common_freqs,histogram_bounds,correlation,most_common_elems,most_common_elem_freqs,elem_count_histogram,range_length_histogram,range_empty_frac,range_bounds_histogram}',
+  prosrc => 'pg_set_attribute_stats' },
 ]
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 7f2bf18716..1dddf96576 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -127,4 +127,6 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												int nclauses);
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
+extern Datum pg_set_relation_stats(PG_FUNCTION_ARGS);
+extern Datum pg_set_attribute_stats(PG_FUNCTION_ARGS);
 #endif							/* STATISTICS_H */
diff --git a/src/backend/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..f27bfd60a3
--- /dev/null
+++ b/src/backend/statistics/statistics.c
@@ -0,0 +1,603 @@
+/*-------------------------------------------------------------------------
+ * statistics.c
+ *
+ *	  POSTGRES statistics import
+ *
+ * Code supporting the direct importation of relation statistics, similar to
+ * what is done by the ANALYZE command.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *       src/backend/statistics/statistics.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "catalog/pg_database.h"
+#include "catalog/pg_operator.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_type.h"
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "nodes/nodeFuncs.h"
+#include "parser/parse_oper.h"
+#include "statistics/statistics.h"
+#include "utils/acl.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/syscache.h"
+#include "utils/typcache.h"
+
+/*
+ * A role has privileges to vacuum or analyze the relation if any of the
+ * following are true:
+ *   - the role owns the current database and the relation is not shared
+ *   - the role has the MAINTAIN privilege on the relation
+ *
+ */
+static bool
+canModifyRelation(Oid relid, Form_pg_class reltuple)
+{
+	return ((object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId())
+			 && !reltuple->relisshared) ||
+			 pg_class_aclcheck(relid, GetUserId(), ACL_MAINTAIN) == ACLCHECK_OK);
+}
+
+/*
+ * Set statistics for a given pg_class entry.
+ *
+ * This does an in-place (i.e. non-transactional) update of pg_class, just as
+ * is done in ANALYZE.
+ *
+ */
+Datum
+pg_set_relation_stats(PG_FUNCTION_ARGS)
+{
+	enum
+	{
+		P_RELATION = 0,			/* oid */
+		P_RELPAGES,				/* int */
+		P_RELTUPLES,			/* float4 */
+		P_RELALLVISIBLE,		/* int */
+		P_NUM_PARAMS
+	};
+
+	Oid			relid;
+	Relation	rel;
+	HeapTuple	ctup;
+	Form_pg_class pgcform;
+	float4		reltuples;
+	int			relpages;
+	int			relallvisible;
+
+	/* The function is strict, but just to be safe */
+	Assert(!PG_ARGISNULL(P_RELATION) && !PG_ARGISNULL(P_RELTUPLES) &&
+		   !PG_ARGISNULL(P_RELPAGES) && !PG_ARGISNULL(P_RELALLVISIBLE));
+
+	relid = PG_GETARG_OID(P_RELATION);
+
+	/*
+	 * Open the relation, getting ShareUpdateExclusiveLock to ensure that no
+	 * other stat-setting operation can run on it concurrently.
+	 */
+	rel = table_open(RelationRelationId, ShareUpdateExclusiveLock);
+
+	/*
+	 * TODO: choose an error code appropriate for this situation.
+	 * Canidates are:
+	 * ERRCODE_INVALID_CATALOG_NAME
+	 * ERRCODE_UNDEFINED_TABLE
+	 * ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE
+	 * ERRCODE_OBJECT_IN_USE
+	 * ERRCODE_SYSTEM_ERROR
+	 * ERRCODE_INTERNAL_ERROR
+	 */
+	ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(ctup))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_IN_USE),
+				 errmsg("pg_class entry for relid %u not found", relid)));
+
+	pgcform = (Form_pg_class) GETSTRUCT(ctup);
+
+	if (!canModifyRelation(relid, pgcform))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						 RelationGetRelationName(rel))));
+
+
+	relpages = PG_GETARG_INT32(P_RELPAGES);
+	reltuples = PG_GETARG_FLOAT4(P_RELTUPLES);
+	relallvisible = PG_GETARG_INT32(P_RELALLVISIBLE);
+
+	/* Only update pg_class if there is a meaningful change */
+	if ((pgcform->reltuples != reltuples)
+		|| (pgcform->relpages != relpages)
+		|| (pgcform->relallvisible != relallvisible))
+	{
+		pgcform->relpages = PG_GETARG_INT32(P_RELPAGES);
+		pgcform->reltuples = PG_GETARG_FLOAT4(P_RELTUPLES);
+		pgcform->relallvisible = PG_GETARG_INT32(P_RELALLVISIBLE);
+
+		heap_inplace_update(rel, ctup);
+	}
+
+	table_close(rel, NoLock);
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * Convenience wrapper to confirm that the user can modify the relation.
+ * Use this when there is no need to modify the pg_class entry itself.
+ */
+static void
+checkCanModifyRelation(Relation rel)
+{
+	Oid				relid;
+	HeapTuple		tup;
+	Form_pg_class	pgcform;
+
+	relid = RelationGetRelid(rel);
+	tup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for relation %u", relid);
+
+	pgcform = (Form_pg_class) GETSTRUCT(tup);
+
+	if (!canModifyRelation(relid, pgcform))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						 RelationGetRelationName(rel))));
+
+	ReleaseSysCache(tup);
+}
+
+
+/*
+ * Perform the cast of text to some array type
+ */
+static Datum
+cast_stavalue(FmgrInfo *finfo, Datum d, Oid typid, int32 typmod)
+{
+	char   *s = TextDatumGetCString(d);
+	Datum out = FunctionCall3(finfo, CStringGetDatum(s),
+							  ObjectIdGetDatum(typid),
+							  Int32GetDatum(typmod));
+
+	pfree(s);
+
+	return out;
+}
+
+
+/*
+ * Import statistics for a given relation attribute.
+ *
+ * This will insert/replace a row in pg_statistic for the given relation and
+ * attribute name.
+ *
+ * This function will return NULL if either the relation or the attname are
+ * NULL.
+ *
+ * If there is no attribute with a matching attname in the relation, the
+ * function will return false. If there is a matching attribute but the
+ * attribute is dropped, then function will return false.
+ *
+ * The function does not specify values for staopN or stacollN parameters
+ * because those values are determined by the corresponding stakindN value and
+ * the attribute's underlying datatype.
+ *
+ * The stavaluesN parameters are text values, and must be a valid input string
+ * for an array of the basetype of the attribute. Any error generated by the
+ * array_in() function will in turn fail the function.
+ */
+Datum
+pg_set_attribute_stats(PG_FUNCTION_ARGS)
+{
+	enum
+	{
+		P_RELATION = 0,			/* oid */
+		P_ATTNAME,				/* name */
+		P_INHERITED,			/* bool */
+		P_NULL_FRAC,			/* float4 */
+		P_AVG_WIDTH,			/* int32 */
+		P_N_DISTINCT,			/* float4 */
+		P_MC_VALS,				/* text, null */
+		P_MC_FREQS,				/* float4[], null */
+		P_HIST_BOUNDS,			/* text, null */
+		P_CORRELATION,			/* float4, null */
+		P_MC_ELEMS,				/* text, null */
+		P_MC_ELEM_FREQS,		/* float4[], null */
+		P_ELEM_COUNT_HIST,		/* float4[], null */
+		P_RANGE_LENGTH_HIST,	/* text, null */
+		P_RANGE_EMPTY_FRAC,		/* float4, null */
+		P_RANGE_BOUNDS_HIST,	/* text, null */
+		P_NUM_PARAMS
+	};
+
+	/* names of columns that cannot be null */
+	const char *required_param_names[] = {
+		"relation",
+		"attname",
+		"stainherit",
+		"stanullfrac",
+		"stawidth",
+		"stadistinct"
+	};
+
+	Oid			relid;
+	Name		attname;
+	Relation	rel;
+	HeapTuple	atup;
+
+	Oid			typid;
+	int32		typmod;
+	Oid			typcoll;
+	Oid			eqopr;
+	Oid			ltopr;
+	Oid			basetypid;
+	Oid			baseeqopr;
+	Oid			baseltopr;
+
+	float4		stanullfrac;
+	int			stawidth;
+
+	Datum		values[Natts_pg_statistic] = {0};
+	bool		nulls[Natts_pg_statistic] = {false};
+
+	Relation	sd;
+	HeapTuple	oldtup;
+	CatalogIndexState indstate;
+	HeapTuple	stup;
+	Form_pg_attribute attr;
+
+	FmgrInfo	finfo;
+
+	int k = 0;
+
+	/*
+	 * This function is pseudo-strct in that a NULL for the relation oid or
+	 * the attribute name results is a NULL return value.
+	 */
+	if (PG_ARGISNULL(P_RELATION) || PG_ARGISNULL(P_ATTNAME))
+		PG_RETURN_NULL();
+
+	for (int i = P_INHERITED; i <= P_N_DISTINCT; i++)
+		if (PG_ARGISNULL(i))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be NULL", required_param_names[i])));
+
+	relid = PG_GETARG_OID(P_RELATION);
+
+	attname = PG_GETARG_NAME(P_ATTNAME);
+
+	rel = relation_open(relid, ShareUpdateExclusiveLock);
+	checkCanModifyRelation(rel);
+
+	sd = table_open(StatisticRelationId, RowExclusiveLock);
+	checkCanModifyRelation(sd);
+
+	atup = SearchSysCache2(ATTNAME,
+						   ObjectIdGetDatum(relid),
+						   NameGetDatum(attname));
+
+	if (!HeapTupleIsValid(atup))
+	{
+		/* Attribute not found nowhere to import the stats to */
+		relation_close(rel, NoLock);
+		PG_RETURN_BOOL(false);
+	}
+
+	attr = (Form_pg_attribute) GETSTRUCT(atup);
+	if (attr->attisdropped)
+	{
+		ReleaseSysCache(atup);
+		relation_close(rel, NoLock);
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * If this relation is an index and that index has expressions in it, and
+	 * the attnum specified is known to be an expression, then we must walk
+	 * the list attributes up to the specified attnum to get the right
+	 * expression.
+	 */
+	if ((rel->rd_rel->relkind == RELKIND_INDEX
+		 || (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+		&& (rel->rd_indexprs != NIL)
+		&& (rel->rd_index->indkey.values[attr->attnum - 1] == 0))
+	{
+		ListCell   *indexpr_item = list_head(rel->rd_indexprs);
+		Node	   *expr;
+
+		for (int i = 0; i < attr->attnum - 1; i++)
+			if (rel->rd_index->indkey.values[i] == 0)
+				indexpr_item = lnext(rel->rd_indexprs, indexpr_item);
+
+		if (indexpr_item == NULL)	/* shouldn't happen */
+			elog(ERROR, "too few entries in indexprs list");
+
+		expr = (Node *) lfirst(indexpr_item);
+
+		typid = exprType(expr);
+		typmod = exprTypmod(expr);
+
+		/*
+		 * If a collation has been specified for the index column, use that in
+		 * preference to anything else; but if not, fall back to whatever we
+		 * can get from the expression.
+		 */
+		if (OidIsValid(attr->attcollation))
+			typcoll = attr->attcollation;
+		else
+			typcoll = exprCollation(expr);
+	}
+	else
+	{
+		typid = attr->atttypid;
+		typmod = attr->atttypmod;
+		typcoll = attr->attcollation;
+	}
+
+	/* if it's a multirange, step down to the range type */
+	if (type_is_multirange(typid))
+		typid = get_multirange_range(typid);
+
+
+	get_sort_group_operators(typid, false, false, false,
+							 &ltopr, &eqopr, NULL, NULL);
+
+	/*
+	 * if it's a range type, swap the subtype for the base type
+	 */
+	if (type_is_range(typid))
+		basetypid = get_range_subtype(typid);
+	else
+		basetypid = get_base_element_type(typid);
+
+	if (basetypid == InvalidOid)
+	{
+		/* type is its own base type */
+		basetypid = typid;
+		baseltopr = ltopr;
+		baseeqopr = eqopr;
+	}
+	else
+		get_sort_group_operators(basetypid, false, false, false,
+								 &baseltopr, &baseeqopr, NULL, NULL);
+
+	stanullfrac = PG_GETARG_FLOAT4(P_NULL_FRAC);
+	if ((stanullfrac < 0.0) || (stanullfrac > 1.0))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("stanullfrac %f is out of range 0.0 to 1.0", stanullfrac)));
+
+	stawidth = PG_GETARG_INT32(P_AVG_WIDTH);
+	if (stawidth < 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("stawidth %d must be >= 0", stawidth)));
+
+	values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(relid);
+	values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(attr->attnum);
+	values[Anum_pg_statistic_stainherit - 1] = PG_GETARG_DATUM(P_INHERITED);
+	values[Anum_pg_statistic_stanullfrac - 1] = PG_GETARG_DATUM(P_NULL_FRAC);
+	values[Anum_pg_statistic_stawidth - 1] = PG_GETARG_DATUM(P_AVG_WIDTH);
+	values[Anum_pg_statistic_stadistinct - 1] = PG_GETARG_DATUM(P_N_DISTINCT);
+
+	fmgr_info(F_ARRAY_IN, &finfo);
+
+	/* MC_VALS || MC_FREQS => STATISTIC_KIND_MCV */
+	if ((!PG_ARGISNULL(P_MC_VALS)) || (!PG_ARGISNULL(P_MC_FREQS)))
+	{
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_MCV);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(eqopr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+
+		if (PG_ARGISNULL(P_MC_FREQS))
+			nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		else
+			values[Anum_pg_statistic_stanumbers1 - 1 + k] =
+				PG_GETARG_DATUM(P_MC_FREQS);
+
+		if (PG_ARGISNULL(P_MC_VALS))
+			nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+		else
+			values[Anum_pg_statistic_stavalues1 - 1 + k] =
+				cast_stavalue(&finfo, PG_GETARG_DATUM(P_MC_VALS), basetypid, typmod);
+
+		k++;
+	}
+
+	/* HIST_BOUNDS => STATISTIC_KIND_HISTOGRAM */
+	if (!PG_ARGISNULL(P_HIST_BOUNDS))
+	{
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_HISTOGRAM);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(ltopr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		values[Anum_pg_statistic_stavalues1 - 1 + k] =
+			cast_stavalue(&finfo, PG_GETARG_DATUM(P_HIST_BOUNDS), basetypid, typmod);
+
+		k++;
+	}
+
+	/* CORRELATION => STATISTIC_KIND_CORRELATION */
+	if (!PG_ARGISNULL(P_CORRELATION))
+	{
+		Datum		elems[] = { PG_GETARG_DATUM(P_CORRELATION) };
+        ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_CORRELATION);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(ltopr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] = PointerGetDatum(arry);
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+		k++;
+	}
+
+	/* MC_ELEMS || MC_ELEM_FREQS => STATISTIC_KIND_MCELEM */
+	if ((!PG_ARGISNULL(P_MC_ELEMS)) || (!PG_ARGISNULL(P_MC_ELEM_FREQS)))
+	{
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_MCELEM); 
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(baseeqopr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+
+		if (PG_ARGISNULL(P_MC_ELEM_FREQS))
+			nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		else
+			values[Anum_pg_statistic_stanumbers1 - 1 + k] =
+				PG_GETARG_DATUM(P_MC_ELEM_FREQS);
+
+		if (PG_ARGISNULL(P_MC_ELEMS))
+			nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+		else
+			values[Anum_pg_statistic_stavalues1 - 1 + k] =
+				cast_stavalue(&finfo, PG_GETARG_DATUM(P_MC_ELEMS),
+							  basetypid, typmod);
+
+		k++;
+	}
+
+	/* ELEM_COUNT_HIST => STATISTIC_KIND_DECHIST */
+	if (!PG_ARGISNULL(P_ELEM_COUNT_HIST))
+	{
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_DECHIST); 
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(baseeqopr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] =
+			PG_GETARG_DATUM(P_ELEM_COUNT_HIST);
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+		k++;
+	}
+
+	/*
+	 * After this point, it is theoretically possible to overflow
+	 * STATISTIC_NUM_SLOTS so we should check that before adding any
+	 * more stat entries.
+	 */
+
+	/*
+	 * ELEM_COUNT_HIST => STATISTIC_KIND_BOUNDS_HISTOGRAM
+	 *
+	 * This stakind appears before STATISTIC_KIND_BOUNDS_HISTOGRAM
+	 * even though it is numerically greater, and all other stakinds
+	 * appear in numerical order. We duplicate this quirk to make
+	 * before/after tests of pg_statistic records easier.
+	 */
+	if (!PG_ARGISNULL(P_RANGE_BOUNDS_HIST))
+	{
+		if (k >= (STATISTIC_NUM_SLOTS - 1))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("imported statistics must have a maximum of %d slots",
+							STATISTIC_NUM_SLOTS)));
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_BOUNDS_HISTOGRAM);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		values[Anum_pg_statistic_stavalues1 - 1 + k] =
+			cast_stavalue(&finfo, PG_GETARG_DATUM(P_RANGE_BOUNDS_HIST), typid, typmod);
+
+		k++;
+	}
+
+	/* P_RANGE_LENGTH_HIST || P_RANGE_EMPTY_FRAC => STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM */
+	if ((!PG_ARGISNULL(P_RANGE_LENGTH_HIST)) ||
+		(!PG_ARGISNULL(P_RANGE_EMPTY_FRAC)))
+	{
+		if (k >= (STATISTIC_NUM_SLOTS - 1))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("imported statistics must have a maximum of %d slots",
+							STATISTIC_NUM_SLOTS)));
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(Float8LessOperator);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+
+		if (PG_ARGISNULL(P_RANGE_EMPTY_FRAC))
+			nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		else
+		{
+			Datum		elems[] = { PG_GETARG_DATUM(P_RANGE_EMPTY_FRAC) };
+			ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+			values[Anum_pg_statistic_stanumbers1 - 1 + k] = PointerGetDatum(arry);
+		}
+
+		if (PG_ARGISNULL(P_RANGE_LENGTH_HIST))
+			nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+		else
+			values[Anum_pg_statistic_stavalues1 - 1 + k] =
+				cast_stavalue(&finfo, PG_GETARG_DATUM(P_RANGE_LENGTH_HIST), FLOAT8OID, typmod);
+
+		k++;
+	}
+
+	/* fill in all remaining slots */
+	for(; k < STATISTIC_NUM_SLOTS; k++)
+	{
+		values[Anum_pg_statistic_stakind1 - 1 + k] = Int16GetDatum(0);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+	}
+
+	/* Is there already a pg_statistic tuple for this attribute? */
+	oldtup = SearchSysCache3(STATRELATTINH,
+							 ObjectIdGetDatum(relid),
+							 Int16GetDatum(attr->attnum),
+							 PG_GETARG_DATUM(P_INHERITED));
+
+	indstate = CatalogOpenIndexes(sd);
+
+	if (HeapTupleIsValid(oldtup))
+	{
+		/* Yes, replace it */
+		bool		replaces[Natts_pg_statistic] = {true};
+
+		stup = heap_modify_tuple(oldtup, RelationGetDescr(sd),
+								 values, nulls, replaces);
+		ReleaseSysCache(oldtup);
+		CatalogTupleUpdateWithInfo(sd, &stup->t_self, stup, indstate);
+	}
+	else
+	{
+		/* No, insert new tuple */
+		stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
+		CatalogTupleInsertWithInfo(sd, stup, indstate);
+	}
+
+	heap_freetuple(stup);
+	CatalogCloseIndexes(indstate);
+	table_close(sd, NoLock);
+	relation_close(rel, NoLock);
+	ReleaseSysCache(atup);
+	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..2e4c7fada6
--- /dev/null
+++ b/src/test/regress/expected/stats_export_import.out
@@ -0,0 +1,283 @@
+CREATE SCHEMA stats_export_import;
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+        0 |        -1 |             0
+(1 row)
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass, 999, 3.6::real, 15000);
+ pg_set_relation_stats 
+-----------------------
+ t
+(1 row)
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+      999 |       3.6 |         15000
+(1 row)
+
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real,
+    most_common_vals => '{1,2,3}'::text,
+    most_common_freqs => '{0.1,0.2,0.3}'::real[],
+    histogram_bounds => '{1,2,3,4}'::text,
+    correlation => 0.9::real,
+    most_common_elems => '{1,3}'::text,
+    most_common_elem_freqs => '{0.3,0.2}'::real[],
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[],
+    range_length_histogram => NULL::text,
+    range_empty_frac => NULL::real,
+    range_bounds_histogram => NULL::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.1 |         2 |        0.3 | {1,2,3}          | {0.1,0.2,0.3}     | {1,2,3,4}        |         0.9 | {1,3}             | {0.3,0.2}              | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+(1 row)
+
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real,
+    most_common_vals => NULL::text,
+    most_common_freqs => NULL::real[],
+    histogram_bounds => NULL::text,
+    correlation => NULL::real,
+    most_common_elems => NULL::text,
+    most_common_elem_freqs => NULL::real[],
+    elem_count_histogram => NULL::real[],
+    range_length_histogram => '{399,499,Infinity}'::text,
+    range_empty_frac => 0.5::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT range_empty_frac, range_bounds_histogram
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND attname = 'arange';
+ range_empty_frac |        range_bounds_histogram        
+------------------+--------------------------------------
+              0.5 | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+(1 row)
+
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+   relname    | num_stats 
+--------------+-----------
+ is_odd       |         1
+ is_odd_clone |         1
+ test         |         5
+ test_clone   |         5
+(4 rows)
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 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..4788929bdd
--- /dev/null
+++ b/src/test/regress/sql/stats_export_import.sql
@@ -0,0 +1,246 @@
+CREATE SCHEMA stats_export_import;
+
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass, 999, 3.6::real, 15000);
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real,
+    most_common_vals => '{1,2,3}'::text,
+    most_common_freqs => '{0.1,0.2,0.3}'::real[],
+    histogram_bounds => '{1,2,3,4}'::text,
+    correlation => 0.9::real,
+    most_common_elems => '{1,3}'::text,
+    most_common_elem_freqs => '{0.3,0.2}'::real[],
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[],
+    range_length_histogram => NULL::text,
+    range_empty_frac => NULL::real,
+    range_bounds_histogram => NULL::text
+    );
+
+
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND attname = 'id';
+
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real,
+    most_common_vals => NULL::text,
+    most_common_freqs => NULL::real[],
+    histogram_bounds => NULL::text,
+    correlation => NULL::real,
+    most_common_elems => NULL::text,
+    most_common_elem_freqs => NULL::real[],
+    elem_count_histogram => NULL::real[],
+    range_length_histogram => '{399,499,Infinity}'::text,
+    range_empty_frac => 0.5::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+
+SELECT range_empty_frac, range_bounds_histogram
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND attname = 'arange';
+
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+
+SELECT
+    format('SELECT pg_catalog.pg_set_attribute_stats( '
+            || 'relation => %L::regclass::oid, attname => %L::name, '
+            || 'inherited => %L::boolean, null_frac => %L::real, '
+            || 'avg_width => %L::integer, n_distinct => %L::real, '
+            || 'most_common_vals => %L::text, '
+            || 'most_common_freqs => %L::real[], '
+            || 'histogram_bounds => %L::text, '
+            || 'correlation => %L::real, '
+            || 'most_common_elems => %L::text, '
+            || 'most_common_elem_freqs => %L::real[], '
+            || 'elem_count_histogram => %L::real[], '
+            || 'range_length_histogram => %L::text, '
+            || 'range_empty_frac => %L::real, '
+            || 'range_bounds_histogram => %L::text) ',
+        'stats_export_import.' || s.tablename || '_clone', s.attname,
+        s.inherited, s.null_frac,
+        s.avg_width, s.n_distinct,
+        s.most_common_vals, s.most_common_freqs, s.histogram_bounds,
+        s.correlation, s.most_common_elems, s.most_common_elem_freqs,
+        s.elem_count_histogram, s.range_length_histogram,
+        s.range_empty_frac, s.range_bounds_histogram)
+FROM pg_catalog.pg_stats AS s
+WHERE s.schemaname = 'stats_export_import'
+AND s.tablename IN ('test', 'is_odd')
+\gexec
+
+-- restore ECHO to original value
+\set ECHO :orig_ECHO
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 5b225ccf4f..75aa3e3d1c 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -28865,6 +28865,97 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
     in identifying the specific disk files associated with database objects.
    </para>
 
+   <table id="functions-admin-statsimport">
+    <title>Database Object Statistics Import Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_relation_stats</primary>
+        </indexterm>
+        <function>pg_set_relation_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>relpages</parameter> <type>integer</type> )
+         <parameter>reltuples</parameter> <type>real</type>,
+         <parameter>relallvisible</parameter> <type>integer</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Updates the <structname>pg_class</structname> row for the specified
+        <parameter>relation</parameter>, setting the values for the columns
+        <structfield>reltuples</structfield>,
+        <structfield>relpages</structfield>, and
+        <structfield>relallvisible</structfield>.
+        To avoid table bloat in <structname>pg_class</structname>, this change
+        is made with an in-place update, and therefore cannot be rolled back through
+        normal transaction processing.
+       </para>
+       <para>
+        The purpose of this function is to apply statistics values in an
+        upgrade situation that are "good enough" for system operation until
+        they are replaced by the next auto-analyze. This function is used by
+        <command>pg_upgrade</command> and <command>pg_restore</command> to
+        convey the statistics from the old system version into the new one.
+       </para>
+       </entry>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_attribute_stats</primary>
+        </indexterm>
+        <function>pg_set_attribute_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>attname</parameter> <type>name</type>,
+         <parameter>inherited</parameter> <type>boolean</type>,
+         <parameter>null_frac</parameter> <type>real</type>,
+         <parameter>avg_width</parameter> <type>integer</type>,
+         <parameter>n_distinct</parameter> <type>real</type>,
+         <parameter>most_common_vals</parameter> <type>text</type>,
+         <parameter>most_common_freqs</parameter> <type>real[]</type>,
+         <parameter>histogram_bounds</parameter> <type>text</type>,
+         <parameter>correlation</parameter> <type>real</type>,
+         <parameter>most_common_elems</parameter> <type>text</type>,
+         <parameter>most_common_elem_freqs</parameter> <type>real[]</type>,
+         <parameter>elem_count_histogram</parameter> <type>real[]</type>,
+         <parameter>range_length_histogram</parameter> <type>text</type>,
+         <parameter>range_empty_frac</parameter> <type>real</type>,
+         <parameter>range_bounds_histogram</parameter> <type>text</type>)
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Replaces the <structname>pg_statistic</structname> row for the
+        <structname>pg_attribute</structname> row specified by
+        <parameter>relation</parameter>, <parameter>attname</parameter>
+        and <parameter>inherited</parameter>.  Aside from
+        <parameter>relation</parameter>, the parameters in this are all
+        derived from <structname>pg_stats</structname>, and the values
+        given are most often extracted from there.
+       </para>
+       <para>
+        The purpose of this function is to apply statistics values in an
+        upgrade situation that are "good enough" for system operation until
+        they are replaced by the next auto-analyze. This function is used by
+        <command>pg_dump</command>, <command>pg_restore</command>, and 
+        <command>pg_upgrade</command> to convey the statistics from the old
+        system version into the new one.
+       </para>
+       </entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
    <table id="functions-admin-dblocation">
     <title>Database Object Location Functions</title>
     <tgroup cols="1">
-- 
2.44.0

v11-0002-Enable-dumping-of-table-index-stats-in-pg_dump.patchtext/x-patch; charset=US-ASCII; name=v11-0002-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From db1e0b5bbb2a8f640f45d8a7271831535c3d1cea Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH v11 2/2] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will also generate a statement
that calls all of the pg_set_relation_stats() and
pg_set_attribute_stats() calls necessary to restore the statistics of
the current system onto the destination system.

As is the pattern with pg_dump options, this can be disabled with
--no-statistics.
---
 src/include/fe_utils/stats_export.h  |  36 +++++
 src/fe_utils/Makefile                |   1 +
 src/fe_utils/meson.build             |   1 +
 src/fe_utils/stats_export.c          | 201 +++++++++++++++++++++++++++
 src/bin/pg_dump/pg_backup.h          |   2 +
 src/bin/pg_dump/pg_backup_archiver.c |   5 +
 src/bin/pg_dump/pg_dump.c            | 100 ++++++++++++-
 src/bin/pg_dump/pg_dump.h            |   1 +
 src/bin/pg_dump/pg_dumpall.c         |   5 +
 src/bin/pg_dump/pg_restore.c         |   3 +
 10 files changed, 353 insertions(+), 2 deletions(-)
 create mode 100644 src/include/fe_utils/stats_export.h
 create mode 100644 src/fe_utils/stats_export.c

diff --git a/src/include/fe_utils/stats_export.h b/src/include/fe_utils/stats_export.h
new file mode 100644
index 0000000000..f0dc7041f7
--- /dev/null
+++ b/src/include/fe_utils/stats_export.h
@@ -0,0 +1,36 @@
+/*-------------------------------------------------------------------------
+ *
+ * stats_export.h
+ *    Queries to export statistics from current and past versions.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1995, Regents of the University of California
+ *
+ * src/include/varatt.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef STATS_EXPORT_H
+#define STATS_EXPORT_H
+
+#include "postgres_fe.h"
+#include "libpq-fe.h"
+
+/*
+ * The minimum supported version number. No attempt is made to get statistics
+ * import to work on versions older than this. This version was initially chosen
+ * because that was the minimum version supported by pg_dump at the time.
+ */
+#define MIN_SERVER_NUM 90200
+
+extern bool exportStatsSupported(int server_version_num);
+extern bool exportExtStatsSupported(int server_version_num);
+
+extern const char *exportClassStatsSQL(int server_verson_num);
+extern const char *exportAttributeStatsSQL(int server_verson_num);
+
+extern char *exportRelationStatsSQL(int server_version_num);
+
+#endif
diff --git a/src/fe_utils/Makefile b/src/fe_utils/Makefile
index 946c05258f..c734f9f6d3 100644
--- a/src/fe_utils/Makefile
+++ b/src/fe_utils/Makefile
@@ -32,6 +32,7 @@ OBJS = \
 	query_utils.o \
 	recovery_gen.o \
 	simple_list.o \
+	stats_export.o \
 	string_utils.o
 
 ifeq ($(PORTNAME), win32)
diff --git a/src/fe_utils/meson.build b/src/fe_utils/meson.build
index 14d0482a2c..fce503f641 100644
--- a/src/fe_utils/meson.build
+++ b/src/fe_utils/meson.build
@@ -12,6 +12,7 @@ fe_utils_sources = files(
   'query_utils.c',
   'recovery_gen.c',
   'simple_list.c',
+  'stats_export.c',
   'string_utils.c',
 )
 
diff --git a/src/fe_utils/stats_export.c b/src/fe_utils/stats_export.c
new file mode 100644
index 0000000000..fd09e6ea8a
--- /dev/null
+++ b/src/fe_utils/stats_export.c
@@ -0,0 +1,201 @@
+/*-------------------------------------------------------------------------
+ *
+ * Utility functions for extracting object statistics for frontend code
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/fe_utils/stats_export.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include "fe_utils/stats_export.h"
+/*
+#include "libpq/libpq-fs.h"
+*/
+#include "fe_utils/string_utils.h"
+
+/*
+ * No-frills catalog queries that are named according to the statistics they
+ * fetch (relation, attribute, extended) and the earliest server version for
+ * which they work. These are presented so that if other use cases arise they
+ * can share the same base queries but utilize them in their own way.
+ *
+ * The queries themselves do not filter results, so it is up to the caller
+ * to append a WHERE clause filtering either on either c.oid or a combination
+ * of c.relname and n.nspname.
+ */
+
+const char *export_class_stats_query_v9_2 =
+	"SELECT c.oid, n.nspname, c.relname, c.relpages, c.reltuples, c.relallvisible "
+	"FROM pg_class AS c "
+	"JOIN pg_namespace AS n ON n.oid = c.relnamespace";
+
+const char *export_attribute_stats_query_v17 =
+	"SELECT c.oid, n.nspname, c.relname, a.attnum, a.attname, s.inherited, "
+	"s.null_frac, s.avg_width, s.n_distinct, "
+	"s.most_common_vals::text AS most_common_vals, s.most_common_freqs, "
+	"s.histogram_bounds::text AS histogram_bounds, s.correlation, "
+	"s.most_common_elems::text AS most_common_elems, "
+	"s.most_common_elem_freqs, s.elem_count_histogram, "
+	"s.range_length_histogram::text AS range_length_histogram, "
+	"s.range_empty_frac, "
+	"s.range_bounds_histogram::text AS range_bounds_histogram "
+	"FROM pg_class AS c "
+	"JOIN pg_namespace AS n ON n.oid = c.relnamespace "
+	"JOIN pg_attribute AS a ON a.attrelid = c.oid AND not a.attisdropped "
+	"JOIN pg_stats AS s ON s.schemaname = n.nspname AND s.tablename = c.relname";
+
+const char *export_attribute_stats_query_v9_2 =
+	"SELECT c.oid, n.nspname, c.relname, a.attnum, a.attname, s.inherited, "
+	"s.null_frac, s.avg_width, s.n_distinct, "
+	"s.most_common_vals::text AS most_common_vals, s.most_common_freqs, "
+	"s.histogram_bounds::text AS histogram_bounds, s.correlation, "
+	"s.most_common_elems::text AS most_common_elems, "
+	"s.most_common_elem_freqs, s.elem_count_histogram, "
+	"NULL::text AS range_length_histogram, NULL::real AS range_empty_frac, "
+	"NULL::text AS range_bounds_histogram "
+	"FROM pg_class AS c "
+	"JOIN pg_namespace AS n ON n.oid = c.relnamespace "
+	"JOIN pg_attribute AS a ON a.attrelid = c.oid AND not a.attisdropped "
+	"JOIN pg_stats AS s ON s.schemaname = n.nspname AND s.tablename = c.relname";
+
+/*
+ * Returns true if the server version number supports exporting regular
+ * (e.g. pg_statistic) statistics.
+ */
+bool
+exportStatsSupported(int server_version_num)
+{
+	return (server_version_num >= MIN_SERVER_NUM);
+}
+
+/*
+ * Returns true if the server version number supports exporting extended
+ * (e.g. pg_statistic_ext, pg_statitic_ext_data) statistics.
+ *
+ * Currently, none do.
+ */
+bool
+exportExtStatsSupported(int server_version_num)
+{
+	return false;
+}
+
+/*
+ * Return the query appropriate for extracting relation statistics for the
+ * given server version, if one exists.
+ */
+const char *
+exportClassStatsSQL(int server_version_num)
+{
+	if (server_version_num >= MIN_SERVER_NUM)
+		return export_class_stats_query_v9_2;
+	return NULL;
+}
+
+/*
+ * Return the query appropriate for extracting attribute statistics for the
+ * given server version, if one exists.
+ */
+const char *
+exportAttributeStatsSQL(int server_version_num)
+{
+	if (server_version_num >= 170000)
+		return export_attribute_stats_query_v17;
+	if (server_version_num >= MIN_SERVER_NUM)
+		return export_attribute_stats_query_v9_2;
+	return NULL;
+}
+
+/*
+ * Generate a SQL statement that will itself generate a SQL statement to
+ * import all regular stats from a given relation into another relation.
+ *
+ * The query generated takes two parameters.
+ *
+ * $1 is of type Oid, and represents the oid of the source relation.
+ *
+ * $2 is is a cstring, and represents the qualified name of the destination
+ * relation. If NULL, then the qualified name of the source relation will
+ * be used. In either case, the value is casted via ::regclass.
+ *
+ * The function will return NULL for invalid server version numbers.
+ * Otherwise,
+ *
+ * This function needs to work on databases back to 9.2.
+ * The format() function was introduced in 9.1.
+ * The string_agg() aggregate was introduced in 9.0.
+ *
+ */
+char *exportRelationStatsSQL(int server_version_num)
+{
+	const char *relsql = exportClassStatsSQL(server_version_num);
+	const char *attrsql = exportAttributeStatsSQL(server_version_num);
+	const char *filter = "WHERE c.oid = $1::regclass";
+	char	   *s;
+	PQExpBuffer sql;
+
+	if ((relsql == NULL) || (attrsql == NULL))
+		return NULL;
+
+	/*
+	 * Set up the initial CTEs each with the same oid filter
+	 */
+	sql = createPQExpBuffer();
+	appendPQExpBuffer(sql,
+					  "WITH r AS (%s %s), a AS (%s %s), ",
+					  relsql, filter, attrsql, filter);
+
+	/*
+	 * Generate the pg_set_relation_stats function call for the relation
+	 * and one pg_set_attribute_stats function call for each attribute with
+	 * a pg_statistic entry. Give each row an order value such that the
+	 * set relation stats call will be first, followed by the set attribute
+	 * stats calls in attnum order (even though the attributes are identified
+	 * by attname).
+	 *
+	 * Then aggregate the function calls into a single SELECT statement that
+	 * puts the calls in the order described above.
+	 */
+	appendPQExpBufferStr(sql,
+		"s(ord,sql) AS ( "
+			"SELECT 0, format('pg_catalog.pg_set_relation_stats("
+			"%L::regclass, %L::integer, %L::real, %L::integer)', "
+			"coalesce($2, format('%I.%I', r.nspname, r.relname)), "
+			"r.relpages, r.reltuples, r.relallvisible) "
+			"FROM r "
+			"UNION ALL "
+			"SELECT 1, format('pg_catalog.pg_set_attribute_stats( "
+			"relation => %L::regclass, attname => %L::name, "
+			"inherited => %L::boolean, null_frac => %L::real, "
+			"avg_width => %L::integer, n_distinct => %L::real, "
+			"most_common_vals => %L::text, "
+			"most_common_freqs => %L::real[], "
+			"histogram_bounds => %L::text, "
+			"correlation => %L::real, "
+			"most_common_elems => %L::text, "
+			"most_common_elem_freqs => %L::real[], "
+			"elem_count_histogram => %L::real[], "
+			"range_length_histogram => %L::text, "
+			"range_empty_frac => %L::real, "
+			"range_bounds_histogram => %L::text)', "
+			"coalesce($2, format('%I.%I', a.nspname, a.relname)), "
+			"a.attname, a.inherited, a.null_frac, a.avg_width, "
+			"a.n_distinct, a.most_common_vals, a.most_common_freqs, "
+			"a.histogram_bounds, a.correlation, "
+			"a.most_common_elems, a.most_common_elem_freqs, "
+			"a.elem_count_histogram, a.range_length_histogram, "
+			"a.range_empty_frac, a.range_bounds_histogram ) "
+			"FROM a "
+		") "
+		"SELECT 'SELECT ' || string_agg(s.sql, ', ' ORDER BY s.ord) "
+		"FROM s ");
+
+	s = strdup(sql->data);
+	destroyPQExpBuffer(sql);
+	return s;
+}
diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 9ef2f2017e..1db5cf52eb 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -112,6 +112,7 @@ typedef struct _restoreOptions
 	int			no_publications;	/* Skip publication entries */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
@@ -179,6 +180,7 @@ typedef struct _dumpOptions
 	int			no_security_labels;
 	int			no_publications;
 	int			no_subscriptions;
+	int			no_statistics;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index d97ebaff5b..d5f61399d9 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2833,6 +2833,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's a stats dump, maybe ignore it */
+	if (ropt->no_statistics && strcmp(te->desc, "STATISTICS") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2862,6 +2866,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a5149ca823..5db230e020 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -59,6 +59,7 @@
 #include "compress_io.h"
 #include "dumputils.h"
 #include "fe_utils/option_utils.h"
+#include "fe_utils/stats_export.h"
 #include "fe_utils/string_utils.h"
 #include "filter.h"
 #include "getopt_long.h"
@@ -425,6 +426,7 @@ main(int argc, char **argv)
 		{"no-comments", no_argument, &dopt.no_comments, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -1130,6 +1132,7 @@ help(const char *progname)
 	printf(_("  --no-comments                do not dump comments\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -6981,6 +6984,7 @@ getTables(Archive *fout, int *numTables)
 
 		/* Tables have data */
 		tblinfo[i].dobj.components |= DUMP_COMPONENT_DATA;
+		tblinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
 
 		/* Mark whether table has an ACL */
 		if (!PQgetisnull(res, i, i_relacl))
@@ -7478,6 +7482,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -10227,6 +10232,82 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const DumpableObject *dobj,
+				  const char *reltypename, DumpId dumpid)
+{
+	const char *stmtname = "relstats";
+	static bool prepared = false;
+	const char *values[2];
+	PGconn     *conn;
+	PGresult   *res;
+	PQExpBuffer query;
+	PQExpBuffer tag;
+
+	/* do nothing, if --no-statistics is supplied */
+	if (fout->dopt->no_statistics)
+		return;
+
+	conn = GetConnection(fout);
+
+	if (!prepared)
+	{
+		int		ver = PQserverVersion(conn);
+		char   *sql = exportRelationStatsSQL(ver);
+
+		if (sql == NULL)
+			pg_fatal("could not prepare stats export query for server version %d",
+					 ver);
+
+		res = PQprepare(conn, stmtname, sql, 2, NULL);
+		if (res == NULL || PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("prepared statement failed: %s",
+					 PQerrorMessage(conn));
+
+		free(sql);
+		prepared = true;
+	}
+
+	values[0] = fmtQualifiedId(dobj->namespace->dobj.name, dobj->name);
+	values[1] = NULL;
+	res = PQexecPrepared(conn, stmtname, 2, values, NULL, NULL, 0);
+
+
+	/* Result set must be 1x1 */
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		pg_fatal("error in statistics extraction: %s", PQerrorMessage(conn));
+
+	if (PQntuples(res) != 1)
+		pg_fatal("statistics extraction expected one row, but got %d rows", 
+				 PQntuples(res));
+
+	query = createPQExpBuffer();
+	appendPQExpBufferStr(query, strdup(PQgetvalue(res, 0, 0)));
+	appendPQExpBufferStr(query, ";\n");
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", reltypename,
+					  fmtId(dobj->name));
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATS IMPORT",
+							  .section = SECTION_POST_DATA,
+							  .createStmt = query->data,
+							  .deps = &dumpid,
+							  .nDeps = 1));
+
+	PQclear(res);
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -16660,6 +16741,13 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 	if (tbinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
 		dumpTableSecLabel(fout, tbinfo, reltypename);
 
+	/* Statistics are dependent on the definition, not the data */
+	/* Views don't have stats */
+	if ((tbinfo->dobj.dump & DUMP_COMPONENT_STATISTICS) &&
+		(tbinfo->relkind == RELKIND_VIEW))
+		dumpRelationStats(fout, &tbinfo->dobj, reltypename,
+						  tbinfo->dobj.dumpId);
+
 	/* Dump comments on inlined table constraints */
 	for (j = 0; j < tbinfo->ncheck; j++)
 	{
@@ -16861,6 +16949,7 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 	PQExpBuffer delq;
 	char	   *qindxname;
 	char	   *qqindxname;
+	DumpId		dumpid;
 
 	/* Do nothing in data-only dump */
 	if (dopt->dataOnly)
@@ -16973,14 +17062,21 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 		free(indstatvalsarray);
 	}
 
+	/* Comments and stats share same .dep */
+	dumpid = is_constraint ? indxinfo->indexconstraint :
+							 indxinfo->dobj.dumpId;
+
 	/* Dump Index Comments */
 	if (indxinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
 		dumpComment(fout, "INDEX", qindxname,
 					tbinfo->dobj.namespace->dobj.name,
 					tbinfo->rolname,
 					indxinfo->dobj.catId, 0,
-					is_constraint ? indxinfo->indexconstraint :
-					indxinfo->dobj.dumpId);
+					dumpid);
+
+	/* Dump Index Stats */
+	if (indxinfo->dobj.dump & DUMP_COMPONENT_STATISTICS)
+		dumpRelationStats(fout, &indxinfo->dobj, "INDEX", dumpid);
 
 	destroyPQExpBuffer(q);
 	destroyPQExpBuffer(delq);
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 9bc93520b4..d6a071ec28 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -101,6 +101,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 491311fe79..37b6ba8a49 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -105,6 +105,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -174,6 +175,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -453,6 +455,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -668,6 +672,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index c3beacdec1..2d326dec72 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -75,6 +75,7 @@ main(int argc, char **argv)
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
 	static int	no_subscriptions = 0;
+	static int  no_statistics = 0;
 	static int	strict_names = 0;
 
 	struct option cmdopts[] = {
@@ -126,6 +127,7 @@ main(int argc, char **argv)
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
+		{"no-statistics", no_argument, &no_statistics, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -358,6 +360,7 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
-- 
2.44.0

#68Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#67)
Re: Statistics Import and Export

On Tue, 2024-03-19 at 05:16 -0400, Corey Huinker wrote:

v11 attached.

Thank you.

Comments on 0001:

This test:

   +SELECT
   +    format('SELECT pg_catalog.pg_set_attribute_stats( '
   ...

seems misplaced. It's generating SQL that can be used to restore or
copy the stats -- that seems like the job of pg_dump, and shouldn't be
tested within the plain SQL regression tests.

And can the other tests use pg_stats rather than pg_statistic?

The function signature for pg_set_attribute_stats could be more
friendly -- how about there are a few required parameters, and then it
only sets the stats that are provided and the other ones are either
left to the existing value or get some reasonable default?

Make sure all error paths ReleaseSysCache().

Why are you calling checkCanModifyRelation() twice?

I'm confused about when the function should return false and when it
should throw an error. I'm inclined to think the return type should be
void and all failures should be reported as ERROR.

replaces[] is initialized to {true}, which means only the first element
is initialized to true. Try following the pattern in AlterDatabase (or
similar) which reads the catalog tuple first, then updates a few fields
selectively, setting the corresponding element of replaces[] along the
way.

The test also sets the most_common_freqs in an ascending order, which
is weird.

Relatedly, I got worried recently about the idea of plain users
updating statistics. In theory, that should be fine, and the planner
should be robust to whatever pg_statistic contains; but in practice
there's some risk of mischief there until everyone understands that the
contents of pg_stats should not be trusted. Fortunately I didn't find
any planner crashes or even errors after a brief test.

One thing we can do is some extra validation for consistency, like
checking that the arrays are properly sorted, check for negative
numbers in the wrong place, or fractions larger than 1.0, etc.

Regards,
Jeff Davis

#69Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#68)
Re: Statistics Import and Export

On Thu, Mar 21, 2024 at 2:29 AM Jeff Davis <pgsql@j-davis.com> wrote:

On Tue, 2024-03-19 at 05:16 -0400, Corey Huinker wrote:

v11 attached.

Thank you.

Comments on 0001:

This test:

+SELECT
+    format('SELECT pg_catalog.pg_set_attribute_stats( '
...

seems misplaced. It's generating SQL that can be used to restore or
copy the stats -- that seems like the job of pg_dump, and shouldn't be
tested within the plain SQL regression tests.

Fair enough.

And can the other tests use pg_stats rather than pg_statistic?

They can, but part of what I wanted to show was that the values that aren't
directly passed in as parameters (staopN, stacollN) get set to the correct
values, and those values aren't guaranteed to match across databases, hence
testing them in the regression test rather than in a TAP test. I'd still
like to be able to test that.

The function signature for pg_set_attribute_stats could be more
friendly -- how about there are a few required parameters, and then it
only sets the stats that are provided and the other ones are either
left to the existing value or get some reasonable default?

That would be problematic.

1. We'd have to compare the stats provided against the stats that are
already there, make that list in-memory, and then re-order what remains
2. There would be no way to un-set statistics of a given stakind, unless we
added an "actually set it null" boolean for each parameter that can be
null.
3. I tried that with the JSON formats, it made the code even messier than
it already was.

Make sure all error paths ReleaseSysCache().

+1

Why are you calling checkCanModifyRelation() twice?

Once for the relation itself, and once for pg_statistic.

I'm confused about when the function should return false and when it
should throw an error. I'm inclined to think the return type should be
void and all failures should be reported as ERROR.

I go back and forth on that. I can see making it void and returning an
error for everything that we currently return false for, but if we do that,
then a statement with one pg_set_relation_stats, and N
pg_set_attribute_stats (which we lump together in one command for the
locking benefits and atomic transaction) would fail entirely if one of the
set_attributes named a column that we had dropped. It's up for debate
whether that's the right behavior or not.

replaces[] is initialized to {true}, which means only the first element

is initialized to true. Try following the pattern in AlterDatabase (or
similar) which reads the catalog tuple first, then updates a few fields
selectively, setting the corresponding element of replaces[] along the
way.

+1.

The test also sets the most_common_freqs in an ascending order, which
is weird.

I pulled most of the hardcoded values from pg_stats itself. The sample set
is trivially small, and the values inserted were in-order-ish. So maybe
that's why.

Relatedly, I got worried recently about the idea of plain users
updating statistics. In theory, that should be fine, and the planner
should be robust to whatever pg_statistic contains; but in practice
there's some risk of mischief there until everyone understands that the
contents of pg_stats should not be trusted. Fortunately I didn't find
any planner crashes or even errors after a brief test.

Maybe we could have the functions restricted to a role or roles:

1. pg_write_all_stats (can modify stats on ANY table)
2. pg_write_own_stats (can modify stats on tables owned by user)

I'm iffy on the need for the first one, I list it first purely to show how
I derived the name for the second.

One thing we can do is some extra validation for consistency, like
checking that the arrays are properly sorted, check for negative
numbers in the wrong place, or fractions larger than 1.0, etc.

+1. All suggestions of validation checks welcome.

#70Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#69)
Re: Statistics Import and Export

On Thu, 2024-03-21 at 03:27 -0400, Corey Huinker wrote:

They can, but part of what I wanted to show was that the values that
aren't directly passed in as parameters (staopN, stacollN) get set to
the correct values, and those values aren't guaranteed to match
across databases, hence testing them in the regression test rather
than in a TAP test. I'd still like to be able to test that.

OK, that's fine.

The function signature for pg_set_attribute_stats could be more
friendly

...

1. We'd have to compare the stats provided against the stats that are
already there, make that list in-memory, and then re-order what
remains
2. There would be no way to un-set statistics of a given stakind,
unless we added an "actually set it null" boolean for each parameter
that can be null. 
3. I tried that with the JSON formats, it made the code even messier
than it already was.

How about just some defaults then? Many of them have a reasonable
default, like NULL or an empty array. Some are parallel arrays and
either both should be specified or neither (e.g.
most_common_vals+most_common_freqs), but you can check for that.

Why are you calling checkCanModifyRelation() twice?

Once for the relation itself, and once for pg_statistic.

Nobody has the privileges to modify pg_statistic except superuser,
right? I thought the point of a privilege check is that users could
modify statistics for their own tables, or the tables they maintain.

I can see making it void and returning an error for everything that
we currently return false for, but if we do that, then a statement
with one pg_set_relation_stats, and N pg_set_attribute_stats (which
we lump together in one command for the locking benefits and atomic
transaction) would fail entirely if one of the set_attributes named a
column that we had dropped. It's up for debate whether that's the
right behavior or not.

I'd probably make the dropped column a WARNING with a message like
"skipping dropped column whatever". Regardless, have some kind of
explanatory comment.

I pulled most of the hardcoded values from pg_stats itself. The
sample set is trivially small, and the values inserted were in-order-
ish. So maybe that's why.

In my simple test, most_common_freqs is descending:

CREATE TABLE a(i int);
INSERT INTO a VALUES(1);
INSERT INTO a VALUES(2);
INSERT INTO a VALUES(2);
INSERT INTO a VALUES(3);
INSERT INTO a VALUES(3);
INSERT INTO a VALUES(3);
INSERT INTO a VALUES(4);
INSERT INTO a VALUES(4);
INSERT INTO a VALUES(4);
INSERT INTO a VALUES(4);
ANALYZE a;
SELECT most_common_vals, most_common_freqs
FROM pg_stats WHERE tablename='a';
most_common_vals | most_common_freqs
------------------+-------------------
{4,3,2} | {0.4,0.3,0.2}
(1 row)

Can you show an example where it's not?

Maybe we could have the functions restricted to a role or roles:

1. pg_write_all_stats (can modify stats on ANY table)
2. pg_write_own_stats (can modify stats on tables owned by user)

If we go that route, we are giving up on the ability for users to
restore stats on their own tables. Let's just be careful about
validating data to mitigate this risk.

Regards,
Jeff Davis

#71Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#70)
Re: Statistics Import and Export

How about just some defaults then? Many of them have a reasonable
default, like NULL or an empty array. Some are parallel arrays and
either both should be specified or neither (e.g.
most_common_vals+most_common_freqs), but you can check for that.

+1
Default NULL has been implemented for all parameters after n_distinct.

Why are you calling checkCanModifyRelation() twice?

Once for the relation itself, and once for pg_statistic.

Nobody has the privileges to modify pg_statistic except superuser,
right? I thought the point of a privilege check is that users could
modify statistics for their own tables, or the tables they maintain.

In which case wouldn't the checkCanModify on pg_statistic would be a proxy
for is_superuser/has_special_role_we_havent_created_yet.

I can see making it void and returning an error for everything that
we currently return false for, but if we do that, then a statement
with one pg_set_relation_stats, and N pg_set_attribute_stats (which
we lump together in one command for the locking benefits and atomic
transaction) would fail entirely if one of the set_attributes named a
column that we had dropped. It's up for debate whether that's the
right behavior or not.

I'd probably make the dropped column a WARNING with a message like
"skipping dropped column whatever". Regardless, have some kind of
explanatory comment.

That's certainly do-able.

I pulled most of the hardcoded values from pg_stats itself. The
sample set is trivially small, and the values inserted were in-order-
ish. So maybe that's why.

In my simple test, most_common_freqs is descending:

CREATE TABLE a(i int);
INSERT INTO a VALUES(1);
INSERT INTO a VALUES(2);
INSERT INTO a VALUES(2);
INSERT INTO a VALUES(3);
INSERT INTO a VALUES(3);
INSERT INTO a VALUES(3);
INSERT INTO a VALUES(4);
INSERT INTO a VALUES(4);
INSERT INTO a VALUES(4);
INSERT INTO a VALUES(4);
ANALYZE a;
SELECT most_common_vals, most_common_freqs
FROM pg_stats WHERE tablename='a';
most_common_vals | most_common_freqs
------------------+-------------------
{4,3,2} | {0.4,0.3,0.2}
(1 row)

Can you show an example where it's not?

Not off hand, no.

Maybe we could have the functions restricted to a role or roles:

1. pg_write_all_stats (can modify stats on ANY table)
2. pg_write_own_stats (can modify stats on tables owned by user)

If we go that route, we are giving up on the ability for users to
restore stats on their own tables. Let's just be careful about
validating data to mitigate this risk.

A great many test cases coming in the next patch.

#72Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#71)
Re: Statistics Import and Export

On Thu, 2024-03-21 at 15:10 -0400, Corey Huinker wrote:

In which case wouldn't the checkCanModify on pg_statistic would be a
proxy for is_superuser/has_special_role_we_havent_created_yet.

So if someone pg_dumps their table and gets the statistics in the SQL,
then they will get errors loading it unless they are a member of a
special role?

If so we'd certainly need to make --no-statistics the default, and have
some way of skipping stats during reload of the dump (perhaps make the
set function a no-op based on a GUC?).

But ideally we'd just make it safe to dump and reload stats on your own
tables, and then not worry about it.

Not off hand, no.

To me it seems like inconsistent data to have most_common_freqs in
anything but descending order, and we should prevent it.

Regards,
Jeff Davis

#73Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#72)
Re: Statistics Import and Export

But ideally we'd just make it safe to dump and reload stats on your own
tables, and then not worry about it.

That is my strong preference, yes.

Not off hand, no.

To me it seems like inconsistent data to have most_common_freqs in
anything but descending order, and we should prevent it.

Sorry, I misunderstood, I thought we were talking about values, not the
frequencies. Yes, the frequencies should only be monotonically
non-increasing (i.e. it can go down or flatline from N->N+1). I'll add a
test case for that.

#74Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#73)
2 attachment(s)
Re: Statistics Import and Export

v12 attached.

0001 -

The functions pg_set_relation_stats() and pg_set_attribute_stats() now
return void. There just weren't enough conditions where a condition was
considered recoverable to justify having it. This may mean that combining
multiple pg_set_attribute_stats calls into one compound statement may no
longer be desirable, but that's just one of the places where I'd like
feedback on how pg_dump/pg_restore use these functions.

The function pg_set_attribute_stats() now has NULL defaults for all
stakind-based statistics types. Thus, you can set statistics on a more
terse basis, like so:

SELECT pg_catalog.pg_set_attribute_stats(
relation => 'stats_export_import.test'::regclass,
attname => 'id'::name,
inherited => false::boolean,
null_frac => 0.5::real,
avg_width => 2::integer,
n_distinct => -0.1::real,
most_common_vals => '{2,1,3}'::text,
most_common_freqs => '{0.3,0.25,0.05}'::real[]
);

This would generate a pg_statistic row with exactly one stakind in it, and
replaces whatever statistics previously existed for that attribute.

It now checks for many types of data inconsistencies, and most (35) of
those have test coverage in the regression. There's a few areas still
uncovered, mostly surrounding histograms where the datatype is dependent on
the attribute.

The functions both require that the caller be the owner of the table/index.

The function pg_set_relation_stats is largely unchanged from previous
versions.

Key areas where I'm seeking feedback:

- What additional checks can be made to ensure validity of statistics?
- What additional regression tests would be desirable?
- What extra information can we add to the error messages to give the user
an idea of how to fix the error?
- What are some edge cases we should test concerning putting bad stats in a
table to get an undesirable outcome?

0002 -

This patch concerns invoking the functions in 0001 via
pg_restore/pg_upgrade. Little has changed here. Dumping statistics is
currently the default for pg_dump/pg_restore/pg_upgrade, and can be
switched off with the switch --no-statistics. Some have expressed concern
about whether stats dumping should be the default. I have a slight
preference for making it the default, for the following reasons:

- The existing commandline switches are all --no-something based, and this
follows the pattern.
- Complaints about poor performance post-upgrade are often the result of
the user not knowing about vacuumdb --analyze-in-stages or the need to
manually ANALYZE. If they don't know about that, how can we expect them to
know about about new switches in pg_upgrade?
- The failure condition means that the user has a table with no stats in it
(or possibly partial stats, if we change how we make the calls), which is
exactly where they were before they made the call.
- Any performance regressions will be remedied with the next autovacuum or
manual ANALYZE.
- If we had a positive flag (e.g. --with-statistics or just --statistics),
and we then changed the default, that could be considered a POLA violation.

Key areas where I'm seeking feedback:

- What level of errors in a restore will a user tolerate, and what should
be done to the error messages to indicate that the data itself is fine, but
a manual operation to update stats on that particular table is now
warranted?
- To what degree could pg_restore/pg_upgrade take that recovery action
automatically?
- Should the individual attribute/class set function calls be grouped by
relation, so that they all succeed/fail together, or should they be called
separately, each able to succeed or fail on their own?
- Any other concerns about how to best use these new functions.

Attachments:

v12-0001-Create-pg_set_relation_stats-pg_set_attribute_st.patchtext/x-patch; charset=US-ASCII; name=v12-0001-Create-pg_set_relation_stats-pg_set_attribute_st.patchDownload
From e2f1bce46a265d7c76ce1d48c1f46cae41c20286 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 21 Mar 2024 18:04:54 -0400
Subject: [PATCH v12 1/2] Create pg_set_relation_stats, pg_set_attribute_stats.

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

Both functions take an oid to identify the target relation that will
receive the statistics. There is nothing requiring that relation to be
the same one as the one exported, though the statistics have to make
sense in the context of the new relation.

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

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

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

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

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

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index ea45b300b8..180be2c768 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12180,4 +12180,19 @@
   proargtypes => 'int2',
   prosrc => 'gist_stratnum_identity' },
 
+# Statistics Import
+{ oid => '8048',
+  descr => 'set statistics on relation',
+  proname => 'pg_set_relation_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'void',
+  proargtypes => 'oid int4 float4 int4',
+  proargnames => '{relation,relpages,reltuples,relallvisible}',
+  prosrc => 'pg_set_relation_stats' },
+{ oid => '8049',
+  descr => 'set statistics on attribute',
+  proname => 'pg_set_attribute_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'void',
+  proargtypes => 'oid name bool float4 int4 float4 text _float4 text float4 text _float4 _float4 text float4 text',
+  proargnames => '{relation,attname,inherited,null_frac,avg_width,n_distinct,most_common_vals,most_common_freqs,histogram_bounds,correlation,most_common_elems,most_common_elem_freqs,elem_count_histogram,range_length_histogram,range_empty_frac,range_bounds_histogram}',
+  prosrc => 'pg_set_attribute_stats' },
 ]
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 7f2bf18716..1dddf96576 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -127,4 +127,6 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												int nclauses);
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
+extern Datum pg_set_relation_stats(PG_FUNCTION_ARGS);
+extern Datum pg_set_attribute_stats(PG_FUNCTION_ARGS);
 #endif							/* STATISTICS_H */
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index fe2bb50f46..22be7e6653 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -636,6 +636,24 @@ LANGUAGE INTERNAL
 CALLED ON NULL INPUT VOLATILE PARALLEL SAFE
 AS 'pg_stat_reset_slru';
 
+CREATE OR REPLACE FUNCTION
+  pg_set_attribute_stats(relation oid, attname name, inherited bool,
+                         null_frac real, avg_width integer, n_distinct real,
+                         most_common_vals text DEFAULT NULL,
+                         most_common_freqs real[] DEFAULT NULL,
+                         histogram_bounds text DEFAULT NULL,
+                         correlation real DEFAULT NULL,
+                         most_common_elems text DEFAULT NULL,
+                         most_common_elem_freqs real[] DEFAULT NULL,
+                         elem_count_histogram real[] DEFAULT NULL,
+                         range_length_histogram text DEFAULT NULL,
+                         range_empty_frac real DEFAULT NULL,
+                         range_bounds_histogram text DEFAULT NULL)
+RETURNS void
+LANGUAGE INTERNAL
+CALLED ON NULL INPUT VOLATILE
+AS 'pg_set_attribute_stats';
+
 --
 -- The default permissions for functions mean that anyone can execute them.
 -- A number of functions shouldn't be executable by just anyone, but rather
diff --git a/src/backend/statistics/Makefile b/src/backend/statistics/Makefile
index 89cf8c2797..e4f8ab7c4f 100644
--- a/src/backend/statistics/Makefile
+++ b/src/backend/statistics/Makefile
@@ -16,6 +16,7 @@ OBJS = \
 	dependencies.o \
 	extended_stats.o \
 	mcv.o \
-	mvdistinct.o
+	mvdistinct.o \
+	statistics.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/statistics/meson.build b/src/backend/statistics/meson.build
index 73b29a3d50..331e82c776 100644
--- a/src/backend/statistics/meson.build
+++ b/src/backend/statistics/meson.build
@@ -5,4 +5,5 @@ backend_sources += files(
   'extended_stats.c',
   'mcv.c',
   'mvdistinct.c',
+  'statistics.c',
 )
diff --git a/src/backend/statistics/statistics.c b/src/backend/statistics/statistics.c
new file mode 100644
index 0000000000..4149cab8d1
--- /dev/null
+++ b/src/backend/statistics/statistics.c
@@ -0,0 +1,934 @@
+/*-------------------------------------------------------------------------
+ * statistics.c
+ *
+ *	  POSTGRES statistics import
+ *
+ * Code supporting the direct importation of relation statistics, similar to
+ * what is done by the ANALYZE command.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *       src/backend/statistics/statistics.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "catalog/pg_database.h"
+#include "catalog/pg_operator.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_type.h"
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "nodes/nodeFuncs.h"
+#include "parser/parse_oper.h"
+#include "statistics/statistics.h"
+#include "utils/acl.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/float.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/syscache.h"
+#include "utils/typcache.h"
+
+/*
+ * A role has privileges to vacuum or analyze the relation if any of the
+ * following are true:
+ *   - the role owns the current database and the relation is not shared
+ *   - the role has the MAINTAIN privilege on the relation
+ *
+ */
+static bool
+canModifyRelation(Oid relid, Form_pg_class reltuple)
+{
+	return ((object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId())
+			 && !reltuple->relisshared) ||
+			pg_class_aclcheck(relid, GetUserId(), ACL_MAINTAIN) == ACLCHECK_OK);
+}
+
+/*
+ * Set statistics for a given pg_class entry.
+ *
+ * This does an in-place (i.e. non-transactional) update of pg_class, just as
+ * is done in ANALYZE.
+ *
+ */
+Datum
+pg_set_relation_stats(PG_FUNCTION_ARGS)
+{
+	enum
+	{
+		P_RELATION = 0,			/* oid */
+		P_RELPAGES,				/* int */
+		P_RELTUPLES,			/* float4 */
+		P_RELALLVISIBLE,		/* int */
+		P_NUM_PARAMS
+	};
+
+	const char *param_names[] = {
+		"relation",
+		"relpages",
+		"reltuples",
+		"relallvisible"
+	};
+
+	Oid			relid;
+	Relation	rel;
+	HeapTuple	ctup;
+	Form_pg_class pgcform;
+	float4		reltuples;
+	int			relpages;
+	int			relallvisible;
+
+	/* Any NULL parameter is an error */
+	for (int i = P_RELATION; i < P_NUM_PARAMS; i++)
+		if (PG_ARGISNULL(i))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be NULL", param_names[i])));
+
+	relid = PG_GETARG_OID(P_RELATION);
+
+	/*
+	 * Open the relation, getting ShareUpdateExclusiveLock to ensure that no
+	 * other stat-setting operation can run on it concurrently.
+	 */
+	rel = table_open(RelationRelationId, ShareUpdateExclusiveLock);
+
+	/*
+	 * TODO: choose an error code appropriate for this situation. Canidates
+	 * are: ERRCODE_INVALID_CATALOG_NAME ERRCODE_UNDEFINED_TABLE
+	 * ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE ERRCODE_OBJECT_IN_USE
+	 * ERRCODE_SYSTEM_ERROR ERRCODE_INTERNAL_ERROR
+	 */
+	ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(ctup))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_IN_USE),
+				 errmsg("pg_class entry for relid %u not found", relid)));
+
+	pgcform = (Form_pg_class) GETSTRUCT(ctup);
+
+	if (!canModifyRelation(relid, pgcform))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						RelationGetRelationName(rel))));
+
+
+	relpages = PG_GETARG_INT32(P_RELPAGES);
+	reltuples = PG_GETARG_FLOAT4(P_RELTUPLES);
+	relallvisible = PG_GETARG_INT32(P_RELALLVISIBLE);
+
+	/* Only update pg_class if there is a meaningful change */
+	if ((pgcform->reltuples != reltuples)
+		|| (pgcform->relpages != relpages)
+		|| (pgcform->relallvisible != relallvisible))
+	{
+		pgcform->relpages = PG_GETARG_INT32(P_RELPAGES);
+		pgcform->reltuples = PG_GETARG_FLOAT4(P_RELTUPLES);
+		pgcform->relallvisible = PG_GETARG_INT32(P_RELALLVISIBLE);
+
+		heap_inplace_update(rel, ctup);
+	}
+
+	table_close(rel, NoLock);
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Perform the cast of text to some array type
+ */
+static Datum
+cast_stavalue(FmgrInfo *finfo, Datum d, Oid typid, int32 typmod)
+{
+	char	   *s = TextDatumGetCString(d);
+	Datum		out = FunctionCall3(finfo, CStringGetDatum(s),
+									ObjectIdGetDatum(typid),
+									Int32GetDatum(typmod));
+
+	pfree(s);
+
+	return out;
+}
+
+/*
+ * Convenience routine to handle a common pattern where two function
+ * parameters must either both be NULL or both NOT NULL.
+ */
+static bool
+has_arg_pair(FunctionCallInfo fcinfo, const char **pnames, int p1, int p2)
+{
+	/* if on param is NULL and the other NOT NULL, report an error */
+	if (PG_ARGISNULL(p1) != PG_ARGISNULL(p2))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL when %s is NOT NULL",
+					 	pnames[(PG_ARGISNULL(p1)) ? p1 : p2],
+					 	pnames[(PG_ARGISNULL(p1)) ? p2 : p1])));
+
+	return (!PG_ARGISNULL(p1));
+}
+
+
+/*
+ * Import statistics for a given relation attribute.
+ *
+ * This will insert/replace a row in pg_statistic for the given relation and
+ * attribute name.
+ *
+ * The function takes input parameters that correspond to columns in the view
+ * pg_stats.
+ *
+ * Of those, the columns attname, inherited, null_frac, avg_width, and
+ * n_distinct all correspond to NOT NULL columns in pg_statistic. These
+ * parameters have no default value and passing NULL to them will result
+ * in an error.
+ *
+ * If there is no attribute with a matching attname in the relation, the
+ * function will raise an error. Likewise for setting inherited statistics
+ * on a table that is not partitioned.
+ *
+ * The remaining parameters all belong to a specific stakind. Some stakinds
+ * have multiple parameters, and in those cases both parameters must be
+ * NOT NULL or both NULL, otherwise an error will be raised.
+ *
+ * Omitting a parameter or explicitly passing NULL means that that particular
+ * stakind is not associated with the attribute.
+ *
+ * Parameters that are NOT NULL will be inspected for consistency checks,
+ * any of which can raise an error.
+ *
+ * Parameters corresponding to ANYARRAY columns are instead passed in as text
+ * values, which is a valid input string for an array of the type or basetype
+ * of the attribute. Any error generated by the array_in() function will in
+ * turn fail the function.
+ */
+Datum
+pg_set_attribute_stats(PG_FUNCTION_ARGS)
+{
+	enum
+	{
+		P_RELATION = 0,			/* oid */
+		P_ATTNAME,				/* name */
+		P_INHERITED,			/* bool */
+		P_NULL_FRAC,			/* float4 */
+		P_AVG_WIDTH,			/* int32 */
+		P_N_DISTINCT,			/* float4 */
+		P_MC_VALS,				/* text, null */
+		P_MC_FREQS,				/* float4[], null */
+		P_HIST_BOUNDS,			/* text, null */
+		P_CORRELATION,			/* float4, null */
+		P_MC_ELEMS,				/* text, null */
+		P_MC_ELEM_FREQS,		/* float4[], null */
+		P_ELEM_COUNT_HIST,		/* float4[], null */
+		P_RANGE_LENGTH_HIST,	/* text, null */
+		P_RANGE_EMPTY_FRAC,		/* float4, null */
+		P_RANGE_BOUNDS_HIST,	/* text, null */
+		P_NUM_PARAMS
+	};
+
+	/* names of columns that cannot be null */
+	const char *param_names[] = {
+		"relation",
+		"attname",
+		"inherited",
+		"null_frac",
+		"avg_width",
+		"n_distinct",
+		"most_common_vals",
+		"most_common_freqs",
+		"histogram_bounds",
+		"correlation",
+		"most_common_elems",
+		"most_common_elem_freqs",
+		"elem_count_histogram",
+		"range_length_histogram",
+		"range_empty_frac",
+		"range_bounds_histogram"
+	};
+
+	Oid			relid;
+	Name		attname;
+	bool		inherited;
+	Relation	rel;
+	HeapTuple	ctup;
+	HeapTuple	atup;
+	Form_pg_class pgcform;
+
+	Oid			typid;
+	int32		typmod;
+	Oid			typcoll;
+	Oid			eqopr;
+	Oid			ltopr;
+	Oid			basetypid;
+	Oid			baseeqopr;
+	Oid			baseltopr;
+
+	const float4 frac_min = 0.0;
+	const float4 frac_max = 1.0;
+	float4		null_frac;
+	const int	avg_width_min = 0;
+	int			avg_width;
+	const float4 n_distinct_min = -1.0;
+	float4		n_distinct;
+
+	Datum		values[Natts_pg_statistic] = {0};
+	bool		nulls[Natts_pg_statistic] = {false};
+
+	Relation	sd;
+	HeapTuple	oldtup;
+	CatalogIndexState indstate;
+	HeapTuple	stup;
+	Form_pg_attribute attr;
+
+	FmgrInfo	finfo;
+
+	bool		has_mcv;
+	bool		has_mc_elems;
+	bool		has_rl_hist;
+	int			stakind_count;
+
+	int			k = 0;
+
+	/*
+	 * A null in a required parameter is an error.
+	 */
+	for (int i = P_RELATION; i <= P_N_DISTINCT; i++)
+		if (PG_ARGISNULL(i))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be NULL", param_names[i])));
+
+	/*
+	 * Check all parameter pairs up front.
+	 */
+	has_mcv = has_arg_pair(fcinfo, param_names,
+						   P_MC_VALS, P_MC_FREQS);
+	has_mc_elems = has_arg_pair(fcinfo, param_names,
+								P_MC_ELEMS, P_MC_ELEM_FREQS);
+	has_rl_hist = has_arg_pair(fcinfo, param_names,
+							   P_RANGE_LENGTH_HIST, P_RANGE_EMPTY_FRAC);
+
+	/*
+	 * If a caller specifies more stakind-stats than we have slots to store
+	 * them, raise an error.
+	 */
+	stakind_count = (int)has_mcv + (int)has_mc_elems + (int)has_rl_hist +
+		(int)!PG_ARGISNULL(P_HIST_BOUNDS) +
+		(int)!PG_ARGISNULL(P_CORRELATION) +
+		(int)!PG_ARGISNULL(P_ELEM_COUNT_HIST) +
+		(int)!PG_ARGISNULL(P_RANGE_BOUNDS_HIST);
+
+	if (stakind_count > STATISTIC_NUM_SLOTS)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("imported statistics must have a maximum of %d slots "
+					 	"but %d given",
+						STATISTIC_NUM_SLOTS, stakind_count)));
+
+	relid = PG_GETARG_OID(P_RELATION);
+
+	rel = relation_open(relid, ShareUpdateExclusiveLock);
+
+	/* Test existence of Relation */
+	ctup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+
+	if (!HeapTupleIsValid(ctup))
+		elog(ERROR, "cache lookup failed for relation %u", relid);
+
+	pgcform = (Form_pg_class) GETSTRUCT(ctup);
+
+	if (!canModifyRelation(relid, pgcform))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						RelationGetRelationName(rel))));
+
+	ReleaseSysCache(ctup);
+
+	/*
+	 * Test existence of attribute
+	 */
+	attname = PG_GETARG_NAME(P_ATTNAME);
+	atup = SearchSysCache2(ATTNAME,
+						   ObjectIdGetDatum(relid),
+						   NameGetDatum(attname));
+
+	/* Attribute not found nowhere to import the stats to */
+	if (!HeapTupleIsValid(atup))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s has no attname %s",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+
+	attr = (Form_pg_attribute) GETSTRUCT(atup);
+	if (attr->attisdropped)
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s attname %s is dropped",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+
+	/* Test inherited */
+	inherited = PG_GETARG_BOOL(P_INHERITED);
+	if (inherited &&
+		(rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE) &&
+		(rel->rd_rel->relkind != RELKIND_PARTITIONED_INDEX))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s is not partitioned, cannot accept inherited stats",
+						RelationGetRelationName(rel))));
+
+	/*
+	 * Fetch datatype information, this is needed to derive the proper staopN
+	 * and stacollN values.
+	 *
+	 * If this relation is an index and that index has expressions in it, and
+	 * the attnum specified is known to be an expression, then we must walk
+	 * the list attributes up to the specified attnum to get the right
+	 * expression.
+	 */
+	if ((rel->rd_rel->relkind == RELKIND_INDEX
+		 || (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+		&& (rel->rd_indexprs != NIL)
+		&& (rel->rd_index->indkey.values[attr->attnum - 1] == 0))
+	{
+		ListCell   *indexpr_item = list_head(rel->rd_indexprs);
+		Node	   *expr;
+
+		for (int i = 0; i < attr->attnum - 1; i++)
+			if (rel->rd_index->indkey.values[i] == 0)
+				indexpr_item = lnext(rel->rd_indexprs, indexpr_item);
+
+		if (indexpr_item == NULL)	/* shouldn't happen */
+			elog(ERROR, "too few entries in indexprs list");
+
+		expr = (Node *) lfirst(indexpr_item);
+
+		typid = exprType(expr);
+		typmod = exprTypmod(expr);
+
+		/*
+		 * If a collation has been specified for the index column, use that in
+		 * preference to anything else; but if not, fall back to whatever we
+		 * can get from the expression.
+		 */
+		if (OidIsValid(attr->attcollation))
+			typcoll = attr->attcollation;
+		else
+			typcoll = exprCollation(expr);
+	}
+	else
+	{
+		typid = attr->atttypid;
+		typmod = attr->atttypmod;
+		typcoll = attr->attcollation;
+	}
+
+	/* if it's a multirange, step down to the range type */
+	if (type_is_multirange(typid))
+		typid = get_multirange_range(typid);
+
+	get_sort_group_operators(typid, false, false, false,
+							 &ltopr, &eqopr, NULL, NULL);
+
+	/*
+	 * if it's a range type, swap the subtype for the base type
+	 */
+	if (type_is_range(typid))
+		basetypid = get_range_subtype(typid);
+	else
+		basetypid = get_base_element_type(typid);
+
+	if (basetypid == InvalidOid)
+	{
+		/* type is its own base type */
+		basetypid = typid;
+		baseltopr = ltopr;
+		baseeqopr = eqopr;
+	}
+	else
+		get_sort_group_operators(basetypid, false, false, false,
+								 &baseltopr, &baseeqopr, NULL, NULL);
+
+	/*
+	 * Statistical parameters that must pass data validity tests
+	 */
+	null_frac = PG_GETARG_FLOAT4(P_NULL_FRAC);
+	if ((null_frac < frac_min) || (null_frac > frac_max))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s %f is out of range %.1f to %.1f",
+						param_names[P_NULL_FRAC], null_frac,
+						frac_min, frac_max)));
+
+	avg_width = PG_GETARG_INT32(P_AVG_WIDTH);
+	if (avg_width < avg_width_min)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s %d must be >= %d",
+						param_names[P_AVG_WIDTH], avg_width, avg_width_min)));
+
+	n_distinct = PG_GETARG_FLOAT4(P_N_DISTINCT);
+	if (n_distinct < n_distinct_min)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s %f must be >= %.1f",
+						param_names[P_N_DISTINCT], n_distinct,
+						n_distinct_min)));
+
+	values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(relid);
+	values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(attr->attnum);
+	values[Anum_pg_statistic_stainherit - 1] = PG_GETARG_DATUM(P_INHERITED);
+	values[Anum_pg_statistic_stanullfrac - 1] = PG_GETARG_DATUM(P_NULL_FRAC);
+	values[Anum_pg_statistic_stawidth - 1] = PG_GETARG_DATUM(P_AVG_WIDTH);
+	values[Anum_pg_statistic_stadistinct - 1] = PG_GETARG_DATUM(P_N_DISTINCT);
+
+	fmgr_info(F_ARRAY_IN, &finfo);
+
+	/* MC_VALS && MC_FREQS => STATISTIC_KIND_MCV */
+	if (has_mcv)
+	{
+		Datum		freqs = PG_GETARG_DATUM(P_MC_FREQS);
+		Datum		vals = cast_stavalue(&finfo, PG_GETARG_DATUM(P_MC_VALS),
+										 basetypid, typmod);
+
+		ExpandedArrayHeader *freqsarr = DatumGetExpandedArray(freqs);
+		ExpandedArrayHeader *valsarr = DatumGetExpandedArray(vals);
+
+		int			freqsdims = freqsarr->ndims;
+		int			valsdims = valsarr->ndims;
+		int			nfreqs = freqsarr->dims[0];
+		int			nvals = valsarr->dims[0];
+
+		const float4 freqsummax = 1.1;
+		float4		freqsum = 0.0;
+		float4		prev = get_float4_infinity();
+
+		if (freqsdims != 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be a multidimensional array",
+							param_names[P_MC_FREQS])));
+		if (valsdims != 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be a multidimensional array",
+							param_names[P_MC_VALS])));
+
+		if (nfreqs != nvals)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s has %d elements, but %s has %d elements, "
+							"but they must be equal",
+							param_names[P_MC_FREQS], nfreqs,
+							param_names[P_MC_VALS], nvals)));
+
+		/*
+		 * check that freqs sum to <= 1.0 or some number slightly higer to
+		 * allow for compounded rounding errors.
+		 */
+
+		deconstruct_expanded_array(freqsarr);
+
+		/* if there's a nulls array, all values must be false */
+		if (freqsarr->dnulls != NULL)
+			for (int i = 0; i < nfreqs; i++)
+				if (freqsarr->dnulls[i])
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("%s array cannot contain NULL values",
+									param_names[P_MC_FREQS])));
+
+		for (int i = 0; i < nfreqs; i++)
+		{
+			float4		f = DatumGetFloat4(freqsarr->dvalues[i]);
+
+			if (f > prev)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s array values must be in descending "
+								"order, but %f > %f",
+								param_names[P_MC_FREQS], f, prev)));
+
+			freqsum += f;
+			prev = f;
+		}
+
+		if (freqsum > freqsummax)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("The sum of elements in %s must not exceed "
+							"%.2f but is %f",
+							param_names[P_MC_FREQS],
+							freqsummax, freqsum)));
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_MCV);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(eqopr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] = freqs;
+		values[Anum_pg_statistic_stavalues1 - 1 + k] = vals;
+
+		k++;
+	}
+
+	/* HIST_BOUNDS => STATISTIC_KIND_HISTOGRAM */
+	if (!PG_ARGISNULL(P_HIST_BOUNDS))
+	{
+		/*
+		 * This is a histogram, which means that the values must be in
+		 * monotonically non-decreasing order, the effort required to
+		 * verify that isn't practical.
+		 */
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_HISTOGRAM);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(ltopr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		values[Anum_pg_statistic_stavalues1 - 1 + k] =
+			cast_stavalue(&finfo, PG_GETARG_DATUM(P_HIST_BOUNDS), basetypid, typmod);
+
+		k++;
+	}
+
+	/* CORRELATION => STATISTIC_KIND_CORRELATION */
+	if (!PG_ARGISNULL(P_CORRELATION))
+	{
+		Datum		elem = PG_GETARG_DATUM(P_CORRELATION);
+		Datum		elems[] = {elem};
+		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+
+		const float4 corr_min = -1.0;
+		const float4 corr_max = 1.0;
+		float4		corr = PG_GETARG_FLOAT4(P_CORRELATION);
+
+		if ((corr < corr_min) || (corr > corr_max))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s %f is out of range %.1f to %.1f",
+							param_names[P_CORRELATION], corr, corr_min,
+							corr_max)));
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_CORRELATION);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(ltopr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] = PointerGetDatum(arry);
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+		k++;
+	}
+
+	/* MC_ELEMS && MC_ELEM_FREQS => STATISTIC_KIND_MCELEM */
+	if (has_mc_elems)
+	{
+		Datum		freqs = PG_GETARG_DATUM(P_MC_ELEM_FREQS);
+		Datum		vals = cast_stavalue(&finfo, PG_GETARG_DATUM(P_MC_ELEMS),
+										 basetypid, typmod);
+
+		ExpandedArrayHeader *freqsarr = DatumGetExpandedArray(freqs);
+		ExpandedArrayHeader *valsarr = DatumGetExpandedArray(vals);
+
+		int			freqsdims = freqsarr->ndims;
+		int			valsdims = valsarr->ndims;
+		int			nfreqs = freqsarr->dims[0];
+		int			nvals = valsarr->dims[0];
+
+		/*
+		 * The mcelem freqs array has either 2 or 3 additional values: the min
+		 * frequency, the max frequency, the optional null frequency.
+		 */
+		int			nfreqsmin = nvals + 2;
+		int			nfreqsmax = nvals + 3;
+
+		float4		freqlowbound;
+		float4		freqhighbound;
+
+		if (freqsdims != 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be a multidimensional array",
+							param_names[P_MC_ELEM_FREQS])));
+		if (valsdims != 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be a multidimensional array",
+							param_names[P_MC_ELEMS])));
+
+		if (nfreqs > 0)
+		{
+			float4		prev = get_float4_infinity();
+
+			if ((nfreqs < nfreqsmin) || (nfreqs > nfreqsmax))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s has %d elements, but must have between "
+								"%d and %d because %s has %d elements",
+								param_names[P_MC_ELEM_FREQS], nfreqs,
+								nfreqsmin, nfreqsmax, param_names[P_MC_ELEMS],
+								nvals)));
+
+			deconstruct_expanded_array(freqsarr);
+
+			/* if there's a nulls array, all values must be false */
+			if (freqsarr->dnulls != NULL)
+				for (int i = 0; i < nfreqs; i++)
+					if (freqsarr->dnulls[i])
+						ereport(ERROR,
+								(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+								 errmsg("%s array cannot contain NULL values",
+										param_names[P_MC_ELEM_FREQS])));
+
+			/*
+			 * the freqlowbound and freqhighbound must themselves be valid
+			 * percentages
+			 */
+
+			/* first freq element past the length of the values is the min */
+			freqlowbound = DatumGetFloat4(freqsarr->dvalues[nvals]);
+			if ((freqlowbound < frac_min) || (freqlowbound > frac_max))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s %s frequency %f is out of range %.1f to %.1f",
+								param_names[P_MC_ELEM_FREQS], "minimum",
+								freqlowbound, frac_min, frac_max)));
+
+			/* second freq element past the length of the values is the max */
+			freqhighbound = DatumGetFloat4(freqsarr->dvalues[nvals + 1]);
+			if ((freqhighbound < frac_min) || (freqhighbound > frac_max))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s %s frequency %f is out of range %.1f to %.1f",
+								param_names[P_MC_ELEM_FREQS], "maximum", freqhighbound,
+								frac_min, frac_max)));
+
+			/* low bound must be < high bound */
+			if (freqlowbound > freqhighbound)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s frequency low bound %f cannot be greater "
+								"than high bound %f",
+								param_names[P_MC_ELEM_FREQS], freqlowbound,
+								freqhighbound)));
+
+			/*
+			 * third freq element past the length of the values is the null
+			 * frac
+			 */
+			if (nfreqs == nvals + 3)
+			{
+				float4		freqnullpct = DatumGetFloat4(freqsarr->dvalues[nvals + 2]);
+
+				if ((freqnullpct < frac_min) || (freqnullpct > frac_max))
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("%s %s frequency %f is out of range %.1f to %.1f",
+									param_names[P_MC_ELEM_FREQS], "null", freqnullpct,
+									frac_min, frac_max)));
+			}
+
+			/*
+			 * All the freqs that match up to a val must bet between low/high
+			 * bounds (which is never less strict than frac_min/frac_max) and
+			 * must be in monotonically non-increasing order.
+			 *
+			 * Also, these frequencies do not sum to a number <= 1.0 as is the
+			 * case with MC_FREQS.
+			 */
+			for (int i = 0; i < nvals; i++)
+			{
+				float4		f = DatumGetFloat4(freqsarr->dvalues[i]);
+
+				if ((f < freqlowbound) || (f > freqhighbound))
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("%s frequency %f is out of range %f to %f",
+									param_names[P_MC_ELEM_FREQS], f, freqlowbound,
+									freqhighbound)));
+				if (f > prev)
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("%s array values must be in descending "
+									"order, but %f > %f",
+									param_names[P_MC_ELEM_FREQS], f, prev)));
+
+				prev = f;
+			}
+		}
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_MCELEM);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(baseeqopr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] =
+			PG_GETARG_DATUM(P_MC_ELEM_FREQS);
+		values[Anum_pg_statistic_stavalues1 - 1 + k] =
+			cast_stavalue(&finfo, PG_GETARG_DATUM(P_MC_ELEMS),
+						  basetypid, typmod);
+
+		k++;
+	}
+
+	/* ELEM_COUNT_HIST => STATISTIC_KIND_DECHIST */
+	if (!PG_ARGISNULL(P_ELEM_COUNT_HIST))
+	{
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_DECHIST);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(baseeqopr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] =
+			PG_GETARG_DATUM(P_ELEM_COUNT_HIST);
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+		k++;
+	}
+
+	/*
+	 * RANGE_BOUNDS_HIST => STATISTIC_KIND_BOUNDS_HISTOGRAM
+	 *
+	 * This stakind appears before STATISTIC_KIND_BOUNDS_HISTOGRAM even though
+	 * it is numerically greater, and all other stakinds appear in numerical
+	 * order. We duplicate this quirk to make before/after tests of
+	 * pg_statistic records easier.
+	 */
+	if (!PG_ARGISNULL(P_RANGE_BOUNDS_HIST))
+	{
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_BOUNDS_HISTOGRAM);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		values[Anum_pg_statistic_stavalues1 - 1 + k] =
+			cast_stavalue(&finfo, PG_GETARG_DATUM(P_RANGE_BOUNDS_HIST), typid, typmod);
+
+		k++;
+	}
+
+	/*
+	 * P_RANGE_LENGTH_HIST && P_RANGE_EMPTY_FRAC =>
+	 * STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM
+	 */
+	if (has_rl_hist)
+	{
+		Datum		elem = PG_GETARG_DATUM(P_RANGE_EMPTY_FRAC);
+		Datum		elems[] = {elem};
+		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+		float8		prev = -get_float8_infinity();
+		float4		frac = DatumGetFloat4(elem);
+
+		Datum		stavalue;
+
+		ExpandedArrayHeader	   *arr;
+
+		int			dims;
+		int			nelems;
+
+		if ((frac < frac_min) || (frac > frac_max))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("range_empty_frac %f is out of range %.1f to %.1f",
+						 	frac, frac_min, frac_max)));
+
+		/*
+		 * RANGE_LENGTH_HIST is stored in an anyarray, but it is known to be
+		 * of type float8[]. It is also a histogram, so it must be in
+		 * monotonically nondecreasing order.
+		 */
+		stavalue = cast_stavalue(&finfo,
+								 PG_GETARG_DATUM(P_RANGE_LENGTH_HIST),
+								 FLOAT8OID, typmod);
+
+		arr = DatumGetExpandedArray(stavalue);
+
+		deconstruct_expanded_array(arr);
+		dims = arr->ndims;
+		nelems = arr->dims[0];
+
+		if (dims != 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be a multidimensional array",
+							param_names[P_RANGE_LENGTH_HIST])));
+
+		for (int i = 0; i < nelems; i++)
+		{
+			float8		f = DatumGetFloat8(arr->dvalues[i]);
+
+			if (f < prev)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s array values must be in ascending "
+								"order, but %f > %f",
+								param_names[P_RANGE_LENGTH_HIST], prev, f)));
+
+			prev = f;
+		}
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(Float8LessOperator);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] = PointerGetDatum(arry);
+		values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalue;
+		k++;
+	}
+
+	/* fill in all remaining slots */
+	for (; k < STATISTIC_NUM_SLOTS; k++)
+	{
+		values[Anum_pg_statistic_stakind1 - 1 + k] = Int16GetDatum(0);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+	}
+
+	/* Is there already a pg_statistic tuple for this attribute? */
+	oldtup = SearchSysCache3(STATRELATTINH,
+							 ObjectIdGetDatum(relid),
+							 Int16GetDatum(attr->attnum),
+							 PG_GETARG_DATUM(P_INHERITED));
+
+	sd = table_open(StatisticRelationId, RowExclusiveLock);
+	indstate = CatalogOpenIndexes(sd);
+
+	if (HeapTupleIsValid(oldtup))
+	{
+		/* Yes, replace it */
+		bool		replaces[Natts_pg_statistic];
+
+		for (int i = 0; i < Natts_pg_statistic; i++)
+			replaces[i] = true;
+
+		stup = heap_modify_tuple(oldtup, RelationGetDescr(sd),
+								 values, nulls, replaces);
+		ReleaseSysCache(oldtup);
+		CatalogTupleUpdateWithInfo(sd, &stup->t_self, stup, indstate);
+	}
+	else
+	{
+		/* No, insert new tuple */
+		stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
+		CatalogTupleInsertWithInfo(sd, stup, indstate);
+	}
+
+	heap_freetuple(stup);
+	CatalogCloseIndexes(indstate);
+	table_close(sd, NoLock);
+	relation_close(rel, NoLock);
+	ReleaseSysCache(atup);
+	PG_RETURN_VOID();
+}
diff --git a/src/test/regress/expected/stats_export_import.out b/src/test/regress/expected/stats_export_import.out
new file mode 100644
index 0000000000..a30a6fdd19
--- /dev/null
+++ b/src/test/regress/expected/stats_export_import.out
@@ -0,0 +1,827 @@
+CREATE SCHEMA stats_export_import;
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+        0 |        -1 |             0
+(1 row)
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass, 999, 3.6::real, 15000);
+ pg_set_relation_stats 
+-----------------------
+ 
+(1 row)
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+      999 |       3.6 |         15000
+(1 row)
+
+-- error: relpages null
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass, NULL, 3.6::real, 15000);
+ERROR:  relpages cannot be NULL
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+      999 |       3.6 |         15000
+(1 row)
+
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => '0'::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  could not open relation with OID 0
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => NULL::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  relation cannot be NULL
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => NULL::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  attname cannot be NULL
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => NULL::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  inherited cannot be NULL
+-- error: inherited true on nonpartitioned table
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => true::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  Relation test is not partitioned, cannot accept inherited stats
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => NULL::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  null_frac cannot be NULL
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => NULL::integer,
+    n_distinct => 0.3::real);
+ERROR:  avg_width cannot be NULL
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => NULL::real);
+ERROR:  n_distinct cannot be NULL
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.1 |         2 |        0.3 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- error: null_frac < 0
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => -0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  null_frac -0.100000 is out of range 0.0 to 1.0
+-- error: null_frac > 1
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 1.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  null_frac 1.100000 is out of range 0.0 to 1.0
+-- error: avg_width < 0
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => -1::integer,
+    n_distinct => 0.3::real);
+ERROR:  avg_width -1 must be >= 0
+-- error: n_distinct < -1
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -1.1::real);
+ERROR:  n_distinct -1.100000 must be >= -1.0
+-- error: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => NULL::text,
+    most_common_freqs => '{0.1,0.2,0.3}'::real[]
+    );
+ERROR:  most_common_vals cannot be NULL when most_common_freqs is NOT NULL
+-- error: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{1,2,3}'::text,
+    most_common_freqs => NULL::real[]
+    );
+ERROR:  most_common_freqs cannot be NULL when most_common_vals is NOT NULL
+-- error: mcv / mcf length mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.2,0.1}'::real[]
+    );
+ERROR:  most_common_freqs has 2 elements, but most_common_vals has 3 elements, but they must be equal
+-- error: mcf sum bad
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.6,0.5,0.3}'::real[]
+    );
+ERROR:  The sum of elements in most_common_freqs must not exceed 1.10 but is 1.400000
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,2,3,4}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- error: correlation low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => -1.1::real);
+ERROR:  correlation -1.100000 is out of range -1.0 to 1.0
+-- error: correlation high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 1.1::real);
+ERROR:  correlation 1.100000 is out of range -1.0 to 1.0
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 0.5::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |         0.5 |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- error: mcelem / mcelem null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{1,3}'::text,
+    most_common_elem_freqs => NULL::real[]
+    );
+ERROR:  most_common_elem_freqs cannot be NULL when most_common_elems is NOT NULL
+-- error: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => NULL::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
+    );
+ERROR:  most_common_elems cannot be NULL when most_common_elem_freqs is NOT NULL
+-- error: mcelem / mcelem length mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2}'::real[]
+    );
+ERROR:  most_common_elem_freqs has 2 elements, but must have between 4 and 5 because most_common_elems has 2 elements
+-- error: mcelem freq element out of bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.1,0.2,0.3,0.0}'::real[]
+    );
+ERROR:  most_common_elem_freqs frequency 0.100000 is out of range 0.200000 to 0.300000
+-- error: mcelem freq low-high mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.4,0.3,0.0}'::real[]
+    );
+ERROR:  most_common_elem_freqs frequency low bound 0.400000 cannot be greater than high bound 0.300000
+-- error: mcelem freq null pct invalid
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,-0.0001}'::real[]
+    );
+ERROR:  most_common_elem_freqs null frequency -0.000100 is out of range 0.0 to 1.0
+-- error: mcelem freq bad low bound low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,-0.15,0.3,0.1}'::real[]
+    );
+ERROR:  most_common_elem_freqs minimum frequency -0.150000 is out of range 0.0 to 1.0
+-- error: mcelem freq bad low bound high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,1.5,0.3,0.1}'::real[]
+    );
+ERROR:  most_common_elem_freqs minimum frequency 1.500000 is out of range 0.0 to 1.0
+-- error: mcelem freq bad high bound low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.4,-0.3,0.1}'::real[]
+    );
+ERROR:  most_common_elem_freqs maximum frequency -0.300000 is out of range 0.0 to 1.0
+-- error: mcelem freq bad high bound high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.4,3.0,0.1}'::real[]
+    );
+ERROR:  most_common_elem_freqs maximum frequency 3.000000 is out of range 0.0 to 1.0
+-- error mcelem freq must be non-increasing
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.4,0.2,0.3,0.0}'::real[]
+    );
+ERROR:  most_common_elem_freqs frequency 0.400000 is out of range 0.200000 to 0.300000
+-- ok: mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             | {3,1}             | {0.3,0.2,0.2,0.3,0}    |                      |                        |                  | 
+(1 row)
+
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+(1 row)
+
+-- error: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ERROR:  range_empty_frac cannot be NULL when range_length_histogram is NOT NULL
+-- error: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real
+    );
+ERROR:  range_length_histogram cannot be NULL when range_empty_frac is NOT NULL
+-- error: range_empty_frac low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => -0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ERROR:  range_empty_frac -0.500000 is out of range 0.0 to 1.0
+-- error: range_empty_frac high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 1.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ERROR:  range_empty_frac 1.500000 is out of range 0.0 to 1.0
+-- error: range_length_histogram not ascending
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,Infinity,499}'::text
+    );
+ERROR:  range_length_histogram array values must be in ascending order, but Infinity > 499.000000
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
+(1 row)
+
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
+ stats_export_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      |                        |                  | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+(1 row)
+
+-- error: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[],
+    histogram_bounds => '{1,2,3,4}'::text,
+    correlation => 1.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    range_empty_frac => -0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ERROR:  imported statistics must have a maximum of 5 slots but 6 given
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+   relname    | num_stats 
+--------------+-----------
+ is_odd       |         1
+ is_odd_clone |         1
+ test         |         5
+ test_clone   |         5
+(4 rows)
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 5ac6e871f5..a7a4dfd411 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,8 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc stats_export_import
+
 
 # ----------
 # Load huge amounts of data
diff --git a/src/test/regress/sql/stats_export_import.sql b/src/test/regress/sql/stats_export_import.sql
new file mode 100644
index 0000000000..6292159665
--- /dev/null
+++ b/src/test/regress/sql/stats_export_import.sql
@@ -0,0 +1,730 @@
+CREATE SCHEMA stats_export_import;
+
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass, 999, 3.6::real, 15000);
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+-- error: relpages null
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass, NULL, 3.6::real, 15000);
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => '0'::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => NULL::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => NULL::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => NULL::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: inherited true on nonpartitioned table
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => true::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => NULL::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => NULL::integer,
+    n_distinct => 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => NULL::real);
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- error: null_frac < 0
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => -0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: null_frac > 1
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 1.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: avg_width < 0
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => -1::integer,
+    n_distinct => 0.3::real);
+
+-- error: n_distinct < -1
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -1.1::real);
+
+-- error: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => NULL::text,
+    most_common_freqs => '{0.1,0.2,0.3}'::real[]
+    );
+
+-- error: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{1,2,3}'::text,
+    most_common_freqs => NULL::real[]
+    );
+
+-- error: mcv / mcf length mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.2,0.1}'::real[]
+    );
+
+-- error: mcf sum bad
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.6,0.5,0.3}'::real[]
+    );
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,2,3,4}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- error: correlation low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => -1.1::real);
+
+-- error: correlation high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 1.1::real);
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 0.5::real);
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- error: mcelem / mcelem null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{1,3}'::text,
+    most_common_elem_freqs => NULL::real[]
+    );
+
+-- error: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => NULL::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
+    );
+
+-- error: mcelem / mcelem length mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2}'::real[]
+    );
+
+-- error: mcelem freq element out of bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.1,0.2,0.3,0.0}'::real[]
+    );
+
+-- error: mcelem freq low-high mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.4,0.3,0.0}'::real[]
+    );
+
+-- error: mcelem freq null pct invalid
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,-0.0001}'::real[]
+    );
+
+-- error: mcelem freq bad low bound low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,-0.15,0.3,0.1}'::real[]
+    );
+
+-- error: mcelem freq bad low bound high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,1.5,0.3,0.1}'::real[]
+    );
+
+-- error: mcelem freq bad high bound low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.4,-0.3,0.1}'::real[]
+    );
+
+-- error: mcelem freq bad high bound high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.4,3.0,0.1}'::real[]
+    );
+
+-- error mcelem freq must be non-increasing
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.4,0.2,0.3,0.0}'::real[]
+    );
+
+-- ok: mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- error: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+-- error: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real
+    );
+-- error: range_empty_frac low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => -0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+-- error: range_empty_frac high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 1.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+-- error: range_length_histogram not ascending
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,Infinity,499}'::text
+    );
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- error: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[],
+    histogram_bounds => '{1,2,3,4}'::text,
+    correlation => 1.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    range_empty_frac => -0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+
+SELECT
+    format('SELECT pg_catalog.pg_set_attribute_stats( '
+            || 'relation => %L::regclass::oid, attname => %L::name, '
+            || 'inherited => %L::boolean, null_frac => %L::real, '
+            || 'avg_width => %L::integer, n_distinct => %L::real, '
+            || 'most_common_vals => %L::text, '
+            || 'most_common_freqs => %L::real[], '
+            || 'histogram_bounds => %L::text, '
+            || 'correlation => %L::real, '
+            || 'most_common_elems => %L::text, '
+            || 'most_common_elem_freqs => %L::real[], '
+            || 'elem_count_histogram => %L::real[], '
+            || 'range_length_histogram => %L::text, '
+            || 'range_empty_frac => %L::real, '
+            || 'range_bounds_histogram => %L::text) ',
+        'stats_export_import.' || s.tablename || '_clone', s.attname,
+        s.inherited, s.null_frac,
+        s.avg_width, s.n_distinct,
+        s.most_common_vals, s.most_common_freqs, s.histogram_bounds,
+        s.correlation, s.most_common_elems, s.most_common_elem_freqs,
+        s.elem_count_histogram, s.range_length_histogram,
+        s.range_empty_frac, s.range_bounds_histogram)
+FROM pg_catalog.pg_stats AS s
+WHERE s.schemaname = 'stats_export_import'
+AND s.tablename IN ('test', 'is_odd')
+\gexec
+
+-- restore ECHO to original value
+\set ECHO :orig_ECHO
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 8ecc02f2b9..72711d86d3 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29090,6 +29090,116 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
     in identifying the specific disk files associated with database objects.
    </para>
 
+   <table id="functions-admin-statsimport">
+    <title>Database Object Statistics Import Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_relation_stats</primary>
+        </indexterm>
+        <function>pg_set_relation_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>relpages</parameter> <type>integer</type>,
+         <parameter>reltuples</parameter> <type>real</type>,
+         <parameter>relallvisible</parameter> <type>integer</type> )
+        <returnvalue>void</returnvalue>
+       </para>
+       <para>
+        Updates the <structname>pg_class</structname> row for the specified
+        <parameter>relation</parameter>, setting the values for the columns
+        <structfield>reltuples</structfield>,
+        <structfield>relpages</structfield>, and
+        <structfield>relallvisible</structfield>.
+        To avoid table bloat in <structname>pg_class</structname>, this change
+        is made with an in-place update, and therefore cannot be rolled back through
+        normal transaction processing.
+       </para>
+       <para>
+        The purpose of this function is to apply statistics values in an
+        upgrade situation that are "good enough" for system operation until
+        they are replaced by the next <command>ANALYZE</command>, usually via
+        <command>autovacuum</command> This function is used by
+        <command>pg_upgrade</command> and <command>pg_restore</command> to
+        convey the statistics from the old system version into the new one.
+       </para>
+       <para>
+        The caller must either be the owner of the relation, or have superuser
+        privileges.
+       </para>
+       </entry>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_attribute_stats</primary>
+        </indexterm>
+        <function>pg_set_attribute_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>
+         , <parameter>attname</parameter> <type>name</type>
+         , <parameter>inherited</parameter> <type>boolean</type>
+         , <parameter>null_frac</parameter> <type>real</type>
+         , <parameter>avg_width</parameter> <type>integer</type>
+         , <parameter>n_distinct</parameter> <type>real</type>
+         [, <parameter>most_common_vals</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_freqs</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>histogram_bounds</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>correlation</parameter> <type>real</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_elems</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_elem_freqs</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>elem_count_histogram</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_length_histogram</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_empty_frac</parameter> <type>real</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_bounds_histogram</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ] )
+        <returnvalue>void</returnvalue>
+       </para>
+       <para>
+        Replaces the <structname>pg_statistic</structname> row for the
+        <structname>pg_attribute</structname> row specified by
+        <parameter>relation</parameter>, <parameter>attname</parameter>
+        and <parameter>inherited</parameter>.  Aside from
+        <parameter>relation</parameter>, the parameters in this are all
+        derived from <structname>pg_stats</structname>, and the values
+        given are most often extracted from there.
+       </para>
+       <para>
+        The purpose of this function is to apply statistics values in an
+        upgrade situation that are "good enough" for system operation until
+        they are replaced by the next <command>ANALYZE</command>, usually via
+        <command>autovacuum</command> This function is used by
+        <command>pg_upgrade</command> and <command>pg_restore</command> to
+        convey the statistics from the old system version into the new one.
+       </para>
+       <para>
+        The caller must either be the owner of the relation, or have superuser
+        privileges.
+       </para>
+       </entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
    <table id="functions-admin-dblocation">
     <title>Database Object Location Functions</title>
     <tgroup cols="1">
-- 
2.44.0

v12-0002-Enable-dumping-of-table-index-stats-in-pg_dump.patchtext/x-patch; charset=US-ASCII; name=v12-0002-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From 29edf2917f3ee05819b544073a701b8898030411 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH v12 2/2] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will also generate a statement
that calls all of the pg_set_relation_stats() and
pg_set_attribute_stats() calls necessary to restore the statistics of
the current system onto the destination system.

As is the pattern with pg_dump options, this can be disabled with
--no-statistics.
---
 src/include/fe_utils/stats_export.h  |  36 +++++
 src/fe_utils/Makefile                |   1 +
 src/fe_utils/meson.build             |   1 +
 src/fe_utils/stats_export.c          | 201 +++++++++++++++++++++++++++
 src/bin/pg_dump/pg_backup.h          |   2 +
 src/bin/pg_dump/pg_backup_archiver.c |   5 +
 src/bin/pg_dump/pg_dump.c            | 100 ++++++++++++-
 src/bin/pg_dump/pg_dump.h            |   1 +
 src/bin/pg_dump/pg_dumpall.c         |   5 +
 src/bin/pg_dump/pg_restore.c         |   3 +
 10 files changed, 353 insertions(+), 2 deletions(-)
 create mode 100644 src/include/fe_utils/stats_export.h
 create mode 100644 src/fe_utils/stats_export.c

diff --git a/src/include/fe_utils/stats_export.h b/src/include/fe_utils/stats_export.h
new file mode 100644
index 0000000000..f0dc7041f7
--- /dev/null
+++ b/src/include/fe_utils/stats_export.h
@@ -0,0 +1,36 @@
+/*-------------------------------------------------------------------------
+ *
+ * stats_export.h
+ *    Queries to export statistics from current and past versions.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1995, Regents of the University of California
+ *
+ * src/include/varatt.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef STATS_EXPORT_H
+#define STATS_EXPORT_H
+
+#include "postgres_fe.h"
+#include "libpq-fe.h"
+
+/*
+ * The minimum supported version number. No attempt is made to get statistics
+ * import to work on versions older than this. This version was initially chosen
+ * because that was the minimum version supported by pg_dump at the time.
+ */
+#define MIN_SERVER_NUM 90200
+
+extern bool exportStatsSupported(int server_version_num);
+extern bool exportExtStatsSupported(int server_version_num);
+
+extern const char *exportClassStatsSQL(int server_verson_num);
+extern const char *exportAttributeStatsSQL(int server_verson_num);
+
+extern char *exportRelationStatsSQL(int server_version_num);
+
+#endif
diff --git a/src/fe_utils/Makefile b/src/fe_utils/Makefile
index 946c05258f..c734f9f6d3 100644
--- a/src/fe_utils/Makefile
+++ b/src/fe_utils/Makefile
@@ -32,6 +32,7 @@ OBJS = \
 	query_utils.o \
 	recovery_gen.o \
 	simple_list.o \
+	stats_export.o \
 	string_utils.o
 
 ifeq ($(PORTNAME), win32)
diff --git a/src/fe_utils/meson.build b/src/fe_utils/meson.build
index 14d0482a2c..fce503f641 100644
--- a/src/fe_utils/meson.build
+++ b/src/fe_utils/meson.build
@@ -12,6 +12,7 @@ fe_utils_sources = files(
   'query_utils.c',
   'recovery_gen.c',
   'simple_list.c',
+  'stats_export.c',
   'string_utils.c',
 )
 
diff --git a/src/fe_utils/stats_export.c b/src/fe_utils/stats_export.c
new file mode 100644
index 0000000000..fd09e6ea8a
--- /dev/null
+++ b/src/fe_utils/stats_export.c
@@ -0,0 +1,201 @@
+/*-------------------------------------------------------------------------
+ *
+ * Utility functions for extracting object statistics for frontend code
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/fe_utils/stats_export.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include "fe_utils/stats_export.h"
+/*
+#include "libpq/libpq-fs.h"
+*/
+#include "fe_utils/string_utils.h"
+
+/*
+ * No-frills catalog queries that are named according to the statistics they
+ * fetch (relation, attribute, extended) and the earliest server version for
+ * which they work. These are presented so that if other use cases arise they
+ * can share the same base queries but utilize them in their own way.
+ *
+ * The queries themselves do not filter results, so it is up to the caller
+ * to append a WHERE clause filtering either on either c.oid or a combination
+ * of c.relname and n.nspname.
+ */
+
+const char *export_class_stats_query_v9_2 =
+	"SELECT c.oid, n.nspname, c.relname, c.relpages, c.reltuples, c.relallvisible "
+	"FROM pg_class AS c "
+	"JOIN pg_namespace AS n ON n.oid = c.relnamespace";
+
+const char *export_attribute_stats_query_v17 =
+	"SELECT c.oid, n.nspname, c.relname, a.attnum, a.attname, s.inherited, "
+	"s.null_frac, s.avg_width, s.n_distinct, "
+	"s.most_common_vals::text AS most_common_vals, s.most_common_freqs, "
+	"s.histogram_bounds::text AS histogram_bounds, s.correlation, "
+	"s.most_common_elems::text AS most_common_elems, "
+	"s.most_common_elem_freqs, s.elem_count_histogram, "
+	"s.range_length_histogram::text AS range_length_histogram, "
+	"s.range_empty_frac, "
+	"s.range_bounds_histogram::text AS range_bounds_histogram "
+	"FROM pg_class AS c "
+	"JOIN pg_namespace AS n ON n.oid = c.relnamespace "
+	"JOIN pg_attribute AS a ON a.attrelid = c.oid AND not a.attisdropped "
+	"JOIN pg_stats AS s ON s.schemaname = n.nspname AND s.tablename = c.relname";
+
+const char *export_attribute_stats_query_v9_2 =
+	"SELECT c.oid, n.nspname, c.relname, a.attnum, a.attname, s.inherited, "
+	"s.null_frac, s.avg_width, s.n_distinct, "
+	"s.most_common_vals::text AS most_common_vals, s.most_common_freqs, "
+	"s.histogram_bounds::text AS histogram_bounds, s.correlation, "
+	"s.most_common_elems::text AS most_common_elems, "
+	"s.most_common_elem_freqs, s.elem_count_histogram, "
+	"NULL::text AS range_length_histogram, NULL::real AS range_empty_frac, "
+	"NULL::text AS range_bounds_histogram "
+	"FROM pg_class AS c "
+	"JOIN pg_namespace AS n ON n.oid = c.relnamespace "
+	"JOIN pg_attribute AS a ON a.attrelid = c.oid AND not a.attisdropped "
+	"JOIN pg_stats AS s ON s.schemaname = n.nspname AND s.tablename = c.relname";
+
+/*
+ * Returns true if the server version number supports exporting regular
+ * (e.g. pg_statistic) statistics.
+ */
+bool
+exportStatsSupported(int server_version_num)
+{
+	return (server_version_num >= MIN_SERVER_NUM);
+}
+
+/*
+ * Returns true if the server version number supports exporting extended
+ * (e.g. pg_statistic_ext, pg_statitic_ext_data) statistics.
+ *
+ * Currently, none do.
+ */
+bool
+exportExtStatsSupported(int server_version_num)
+{
+	return false;
+}
+
+/*
+ * Return the query appropriate for extracting relation statistics for the
+ * given server version, if one exists.
+ */
+const char *
+exportClassStatsSQL(int server_version_num)
+{
+	if (server_version_num >= MIN_SERVER_NUM)
+		return export_class_stats_query_v9_2;
+	return NULL;
+}
+
+/*
+ * Return the query appropriate for extracting attribute statistics for the
+ * given server version, if one exists.
+ */
+const char *
+exportAttributeStatsSQL(int server_version_num)
+{
+	if (server_version_num >= 170000)
+		return export_attribute_stats_query_v17;
+	if (server_version_num >= MIN_SERVER_NUM)
+		return export_attribute_stats_query_v9_2;
+	return NULL;
+}
+
+/*
+ * Generate a SQL statement that will itself generate a SQL statement to
+ * import all regular stats from a given relation into another relation.
+ *
+ * The query generated takes two parameters.
+ *
+ * $1 is of type Oid, and represents the oid of the source relation.
+ *
+ * $2 is is a cstring, and represents the qualified name of the destination
+ * relation. If NULL, then the qualified name of the source relation will
+ * be used. In either case, the value is casted via ::regclass.
+ *
+ * The function will return NULL for invalid server version numbers.
+ * Otherwise,
+ *
+ * This function needs to work on databases back to 9.2.
+ * The format() function was introduced in 9.1.
+ * The string_agg() aggregate was introduced in 9.0.
+ *
+ */
+char *exportRelationStatsSQL(int server_version_num)
+{
+	const char *relsql = exportClassStatsSQL(server_version_num);
+	const char *attrsql = exportAttributeStatsSQL(server_version_num);
+	const char *filter = "WHERE c.oid = $1::regclass";
+	char	   *s;
+	PQExpBuffer sql;
+
+	if ((relsql == NULL) || (attrsql == NULL))
+		return NULL;
+
+	/*
+	 * Set up the initial CTEs each with the same oid filter
+	 */
+	sql = createPQExpBuffer();
+	appendPQExpBuffer(sql,
+					  "WITH r AS (%s %s), a AS (%s %s), ",
+					  relsql, filter, attrsql, filter);
+
+	/*
+	 * Generate the pg_set_relation_stats function call for the relation
+	 * and one pg_set_attribute_stats function call for each attribute with
+	 * a pg_statistic entry. Give each row an order value such that the
+	 * set relation stats call will be first, followed by the set attribute
+	 * stats calls in attnum order (even though the attributes are identified
+	 * by attname).
+	 *
+	 * Then aggregate the function calls into a single SELECT statement that
+	 * puts the calls in the order described above.
+	 */
+	appendPQExpBufferStr(sql,
+		"s(ord,sql) AS ( "
+			"SELECT 0, format('pg_catalog.pg_set_relation_stats("
+			"%L::regclass, %L::integer, %L::real, %L::integer)', "
+			"coalesce($2, format('%I.%I', r.nspname, r.relname)), "
+			"r.relpages, r.reltuples, r.relallvisible) "
+			"FROM r "
+			"UNION ALL "
+			"SELECT 1, format('pg_catalog.pg_set_attribute_stats( "
+			"relation => %L::regclass, attname => %L::name, "
+			"inherited => %L::boolean, null_frac => %L::real, "
+			"avg_width => %L::integer, n_distinct => %L::real, "
+			"most_common_vals => %L::text, "
+			"most_common_freqs => %L::real[], "
+			"histogram_bounds => %L::text, "
+			"correlation => %L::real, "
+			"most_common_elems => %L::text, "
+			"most_common_elem_freqs => %L::real[], "
+			"elem_count_histogram => %L::real[], "
+			"range_length_histogram => %L::text, "
+			"range_empty_frac => %L::real, "
+			"range_bounds_histogram => %L::text)', "
+			"coalesce($2, format('%I.%I', a.nspname, a.relname)), "
+			"a.attname, a.inherited, a.null_frac, a.avg_width, "
+			"a.n_distinct, a.most_common_vals, a.most_common_freqs, "
+			"a.histogram_bounds, a.correlation, "
+			"a.most_common_elems, a.most_common_elem_freqs, "
+			"a.elem_count_histogram, a.range_length_histogram, "
+			"a.range_empty_frac, a.range_bounds_histogram ) "
+			"FROM a "
+		") "
+		"SELECT 'SELECT ' || string_agg(s.sql, ', ' ORDER BY s.ord) "
+		"FROM s ");
+
+	s = strdup(sql->data);
+	destroyPQExpBuffer(sql);
+	return s;
+}
diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 9ef2f2017e..1db5cf52eb 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -112,6 +112,7 @@ typedef struct _restoreOptions
 	int			no_publications;	/* Skip publication entries */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
@@ -179,6 +180,7 @@ typedef struct _dumpOptions
 	int			no_security_labels;
 	int			no_publications;
 	int			no_subscriptions;
+	int			no_statistics;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index d97ebaff5b..d5f61399d9 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2833,6 +2833,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's a stats dump, maybe ignore it */
+	if (ropt->no_statistics && strcmp(te->desc, "STATISTICS") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2862,6 +2866,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index d275b31605..e368614745 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -59,6 +59,7 @@
 #include "compress_io.h"
 #include "dumputils.h"
 #include "fe_utils/option_utils.h"
+#include "fe_utils/stats_export.h"
 #include "fe_utils/string_utils.h"
 #include "filter.h"
 #include "getopt_long.h"
@@ -428,6 +429,7 @@ main(int argc, char **argv)
 		{"no-comments", no_argument, &dopt.no_comments, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -1144,6 +1146,7 @@ help(const char *progname)
 	printf(_("  --no-comments                do not dump comments\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -7001,6 +7004,7 @@ getTables(Archive *fout, int *numTables)
 
 		/* Tables have data */
 		tblinfo[i].dobj.components |= DUMP_COMPONENT_DATA;
+		tblinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
 
 		/* Mark whether table has an ACL */
 		if (!PQgetisnull(res, i, i_relacl))
@@ -7498,6 +7502,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -10247,6 +10252,82 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const DumpableObject *dobj,
+				  const char *reltypename, DumpId dumpid)
+{
+	const char *stmtname = "relstats";
+	static bool prepared = false;
+	const char *values[2];
+	PGconn     *conn;
+	PGresult   *res;
+	PQExpBuffer query;
+	PQExpBuffer tag;
+
+	/* do nothing, if --no-statistics is supplied */
+	if (fout->dopt->no_statistics)
+		return;
+
+	conn = GetConnection(fout);
+
+	if (!prepared)
+	{
+		int		ver = PQserverVersion(conn);
+		char   *sql = exportRelationStatsSQL(ver);
+
+		if (sql == NULL)
+			pg_fatal("could not prepare stats export query for server version %d",
+					 ver);
+
+		res = PQprepare(conn, stmtname, sql, 2, NULL);
+		if (res == NULL || PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("prepared statement failed: %s",
+					 PQerrorMessage(conn));
+
+		free(sql);
+		prepared = true;
+	}
+
+	values[0] = fmtQualifiedId(dobj->namespace->dobj.name, dobj->name);
+	values[1] = NULL;
+	res = PQexecPrepared(conn, stmtname, 2, values, NULL, NULL, 0);
+
+
+	/* Result set must be 1x1 */
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		pg_fatal("error in statistics extraction: %s", PQerrorMessage(conn));
+
+	if (PQntuples(res) != 1)
+		pg_fatal("statistics extraction expected one row, but got %d rows", 
+				 PQntuples(res));
+
+	query = createPQExpBuffer();
+	appendPQExpBufferStr(query, strdup(PQgetvalue(res, 0, 0)));
+	appendPQExpBufferStr(query, ";\n");
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", reltypename,
+					  fmtId(dobj->name));
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATS IMPORT",
+							  .section = SECTION_POST_DATA,
+							  .createStmt = query->data,
+							  .deps = &dumpid,
+							  .nDeps = 1));
+
+	PQclear(res);
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -16680,6 +16761,13 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 	if (tbinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
 		dumpTableSecLabel(fout, tbinfo, reltypename);
 
+	/* Statistics are dependent on the definition, not the data */
+	/* Views don't have stats */
+	if ((tbinfo->dobj.dump & DUMP_COMPONENT_STATISTICS) &&
+		(tbinfo->relkind == RELKIND_VIEW))
+		dumpRelationStats(fout, &tbinfo->dobj, reltypename,
+						  tbinfo->dobj.dumpId);
+
 	/* Dump comments on inlined table constraints */
 	for (j = 0; j < tbinfo->ncheck; j++)
 	{
@@ -16881,6 +16969,7 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 	PQExpBuffer delq;
 	char	   *qindxname;
 	char	   *qqindxname;
+	DumpId		dumpid;
 
 	/* Do nothing in data-only dump */
 	if (dopt->dataOnly)
@@ -16993,14 +17082,21 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 		free(indstatvalsarray);
 	}
 
+	/* Comments and stats share same .dep */
+	dumpid = is_constraint ? indxinfo->indexconstraint :
+							 indxinfo->dobj.dumpId;
+
 	/* Dump Index Comments */
 	if (indxinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
 		dumpComment(fout, "INDEX", qindxname,
 					tbinfo->dobj.namespace->dobj.name,
 					tbinfo->rolname,
 					indxinfo->dobj.catId, 0,
-					is_constraint ? indxinfo->indexconstraint :
-					indxinfo->dobj.dumpId);
+					dumpid);
+
+	/* Dump Index Stats */
+	if (indxinfo->dobj.dump & DUMP_COMPONENT_STATISTICS)
+		dumpRelationStats(fout, &indxinfo->dobj, "INDEX", dumpid);
 
 	destroyPQExpBuffer(q);
 	destroyPQExpBuffer(delq);
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 9bc93520b4..d6a071ec28 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -101,6 +101,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 046c0dc3b3..69652aa205 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -105,6 +105,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -174,6 +175,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -453,6 +455,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -668,6 +672,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index c3beacdec1..2d326dec72 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -75,6 +75,7 @@ main(int argc, char **argv)
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
 	static int	no_subscriptions = 0;
+	static int  no_statistics = 0;
 	static int	strict_names = 0;
 
 	struct option cmdopts[] = {
@@ -126,6 +127,7 @@ main(int argc, char **argv)
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
+		{"no-statistics", no_argument, &no_statistics, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -358,6 +360,7 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
-- 
2.44.0

#75Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#74)
2 attachment(s)
Re: Statistics Import and Export

On Fri, Mar 22, 2024 at 9:51 PM Corey Huinker <corey.huinker@gmail.com>
wrote:

v12 attached.

v13 attached. All the same features as v12, but with a lot more type
checking, bounds checking, value inspection, etc. Perhaps the most notable
feature is that we're now ensuring that histogram values are in ascending
order. This could come in handy for detecting when we are applying stats to
a column of the wrong type, or the right type but with a different
collation. It's not a guarantee of validity, of course, but it would detect
egregious changes in sort order.

Attachments:

v13-0001-Create-pg_set_relation_stats-pg_set_attribute_st.patchtext/x-patch; charset=US-ASCII; name=v13-0001-Create-pg_set_relation_stats-pg_set_attribute_st.patchDownload
From 574d522503d36119886d95c91460d2260b10f2f6 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 21 Mar 2024 18:04:54 -0400
Subject: [PATCH v13 1/2] Create pg_set_relation_stats, pg_set_attribute_stats.

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

Both functions take an oid to identify the target relation that will
receive the statistics. There is nothing requiring that relation to be
the same one as the one exported, though the statistics have to make
sense in the context of the new relation.

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

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

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

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

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

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 71c74350a0..8b7a0d9b6e 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12180,4 +12180,19 @@
   proargtypes => 'int2',
   prosrc => 'gist_stratnum_identity' },
 
+# Statistics Import
+{ oid => '8048',
+  descr => 'set statistics on relation',
+  proname => 'pg_set_relation_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'void',
+  proargtypes => 'oid int4 float4 int4',
+  proargnames => '{relation,relpages,reltuples,relallvisible}',
+  prosrc => 'pg_set_relation_stats' },
+{ oid => '8049',
+  descr => 'set statistics on attribute',
+  proname => 'pg_set_attribute_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'void',
+  proargtypes => 'oid name bool float4 int4 float4 text _float4 text float4 text _float4 _float4 text float4 text',
+  proargnames => '{relation,attname,inherited,null_frac,avg_width,n_distinct,most_common_vals,most_common_freqs,histogram_bounds,correlation,most_common_elems,most_common_elem_freqs,elem_count_histogram,range_length_histogram,range_empty_frac,range_bounds_histogram}',
+  prosrc => 'pg_set_attribute_stats' },
 ]
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 7f2bf18716..1dddf96576 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -127,4 +127,6 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												int nclauses);
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
+extern Datum pg_set_relation_stats(PG_FUNCTION_ARGS);
+extern Datum pg_set_attribute_stats(PG_FUNCTION_ARGS);
 #endif							/* STATISTICS_H */
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index fe2bb50f46..22be7e6653 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -636,6 +636,24 @@ LANGUAGE INTERNAL
 CALLED ON NULL INPUT VOLATILE PARALLEL SAFE
 AS 'pg_stat_reset_slru';
 
+CREATE OR REPLACE FUNCTION
+  pg_set_attribute_stats(relation oid, attname name, inherited bool,
+                         null_frac real, avg_width integer, n_distinct real,
+                         most_common_vals text DEFAULT NULL,
+                         most_common_freqs real[] DEFAULT NULL,
+                         histogram_bounds text DEFAULT NULL,
+                         correlation real DEFAULT NULL,
+                         most_common_elems text DEFAULT NULL,
+                         most_common_elem_freqs real[] DEFAULT NULL,
+                         elem_count_histogram real[] DEFAULT NULL,
+                         range_length_histogram text DEFAULT NULL,
+                         range_empty_frac real DEFAULT NULL,
+                         range_bounds_histogram text DEFAULT NULL)
+RETURNS void
+LANGUAGE INTERNAL
+CALLED ON NULL INPUT VOLATILE
+AS 'pg_set_attribute_stats';
+
 --
 -- The default permissions for functions mean that anyone can execute them.
 -- A number of functions shouldn't be executable by just anyone, but rather
diff --git a/src/backend/statistics/Makefile b/src/backend/statistics/Makefile
index 89cf8c2797..e4f8ab7c4f 100644
--- a/src/backend/statistics/Makefile
+++ b/src/backend/statistics/Makefile
@@ -16,6 +16,7 @@ OBJS = \
 	dependencies.o \
 	extended_stats.o \
 	mcv.o \
-	mvdistinct.o
+	mvdistinct.o \
+	statistics.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/statistics/meson.build b/src/backend/statistics/meson.build
index 73b29a3d50..331e82c776 100644
--- a/src/backend/statistics/meson.build
+++ b/src/backend/statistics/meson.build
@@ -5,4 +5,5 @@ backend_sources += files(
   'extended_stats.c',
   'mcv.c',
   'mvdistinct.c',
+  'statistics.c',
 )
diff --git a/src/backend/statistics/statistics.c b/src/backend/statistics/statistics.c
new file mode 100644
index 0000000000..3987425ea4
--- /dev/null
+++ b/src/backend/statistics/statistics.c
@@ -0,0 +1,1108 @@
+/*-------------------------------------------------------------------------
+ * statistics.c
+ *
+ *	  POSTGRES statistics import
+ *
+ * Code supporting the direct importation of relation statistics, similar to
+ * what is done by the ANALYZE command.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *       src/backend/statistics/statistics.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "catalog/pg_database.h"
+#include "catalog/pg_operator.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_type.h"
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "nodes/nodeFuncs.h"
+#include "statistics/statistics.h"
+#include "utils/acl.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/float.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/rangetypes.h"
+#include "utils/syscache.h"
+#include "utils/typcache.h"
+
+/*
+ * A role has privileges to vacuum or analyze the relation if any of the
+ * following are true:
+ *   - the role owns the current database and the relation is not shared
+ *   - the role has the MAINTAIN privilege on the relation
+ *
+ */
+static bool
+canModifyRelation(Oid relid, Form_pg_class reltuple)
+{
+	return ((object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId())
+			 && !reltuple->relisshared) ||
+			pg_class_aclcheck(relid, GetUserId(), ACL_MAINTAIN) == ACLCHECK_OK);
+}
+
+/*
+ * Set statistics for a given pg_class entry.
+ *
+ * This does an in-place (i.e. non-transactional) update of pg_class, just as
+ * is done in ANALYZE.
+ *
+ */
+Datum
+pg_set_relation_stats(PG_FUNCTION_ARGS)
+{
+	enum
+	{
+		P_RELATION = 0,			/* oid */
+		P_RELPAGES,				/* int */
+		P_RELTUPLES,			/* float4 */
+		P_RELALLVISIBLE,		/* int */
+		P_NUM_PARAMS
+	};
+
+	const char *param_names[] = {
+		"relation",
+		"relpages",
+		"reltuples",
+		"relallvisible"
+	};
+
+	Oid			relid;
+	Relation	rel;
+	HeapTuple	ctup;
+	Form_pg_class pgcform;
+	float4		reltuples;
+	int			relpages;
+	int			relallvisible;
+
+	/* Any NULL parameter is an error */
+	for (int i = P_RELATION; i < P_NUM_PARAMS; i++)
+		if (PG_ARGISNULL(i))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be NULL", param_names[i])));
+
+	relid = PG_GETARG_OID(P_RELATION);
+
+	/*
+	 * Open the relation, getting ShareUpdateExclusiveLock to ensure that no
+	 * other stat-setting operation can run on it concurrently.
+	 */
+	rel = table_open(RelationRelationId, ShareUpdateExclusiveLock);
+
+	ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(ctup))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_IN_USE),
+				 errmsg("pg_class entry for relid %u not found", relid)));
+
+	pgcform = (Form_pg_class) GETSTRUCT(ctup);
+
+	if (!canModifyRelation(relid, pgcform))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						RelationGetRelationName(rel))));
+
+
+	relpages = PG_GETARG_INT32(P_RELPAGES);
+	reltuples = PG_GETARG_FLOAT4(P_RELTUPLES);
+	relallvisible = PG_GETARG_INT32(P_RELALLVISIBLE);
+
+	/* Only update pg_class if there is a meaningful change */
+	if ((pgcform->reltuples != reltuples)
+		|| (pgcform->relpages != relpages)
+		|| (pgcform->relallvisible != relallvisible))
+	{
+		pgcform->relpages = PG_GETARG_INT32(P_RELPAGES);
+		pgcform->reltuples = PG_GETARG_FLOAT4(P_RELTUPLES);
+		pgcform->relallvisible = PG_GETARG_INT32(P_RELALLVISIBLE);
+
+		heap_inplace_update(rel, ctup);
+	}
+
+	table_close(rel, NoLock);
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Perform the cast of text to some array type
+ */
+static Datum
+cast_stavalue(FmgrInfo *finfo, Datum d, Oid typid, int32 typmod)
+{
+	char	   *s = TextDatumGetCString(d);
+	Datum		out = FunctionCall3(finfo, CStringGetDatum(s),
+									ObjectIdGetDatum(typid),
+									Int32GetDatum(typmod));
+
+	pfree(s);
+
+	return out;
+}
+
+/*
+ * Convenience routine to handle a common pattern where two function
+ * parameters must either both be NULL or both NOT NULL.
+ */
+static bool
+has_arg_pair(FunctionCallInfo fcinfo, const char **pnames, int p1, int p2)
+{
+	/* if on param is NULL and the other NOT NULL, report an error */
+	if (PG_ARGISNULL(p1) != PG_ARGISNULL(p2))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL when %s is NOT NULL",
+						pnames[(PG_ARGISNULL(p1)) ? p1 : p2],
+						pnames[(PG_ARGISNULL(p1)) ? p2 : p1])));
+
+	return (!PG_ARGISNULL(p1));
+}
+
+/*
+ * Test if the type is a scalar for MCELM purposes
+ */
+static bool
+type_is_scalar(Oid typid)
+{
+	HeapTuple	tp;
+	bool		result = false;
+
+	tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+	if (HeapTupleIsValid(tp))
+	{
+		Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+
+		result = (!OidIsValid(typtup->typanalyze));
+		ReleaseSysCache(tp);
+	}
+	return result;
+}
+
+static int
+value_array_len(ExpandedArrayHeader *arr, const char *name)
+{
+	if (arr->ndims != 1)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be a multidimensional array", name)));
+
+	return arr->dims[0];
+}
+
+/*
+ * Convenience routine to encapsulate all of the steps needed for any
+ * value array.
+ */
+static int
+value_not_null_array_len(ExpandedArrayHeader *arr, const char *name)
+{
+	const int	nelems = value_array_len(arr, name);
+
+	if (nelems > 0)
+	{
+		deconstruct_expanded_array(arr);
+
+		/* if there's a nulls array, all values must be false */
+		if (arr->dnulls != NULL)
+			for (int i = 0; i < nelems; i++)
+				if (arr->dnulls[i])
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("%s array cannot contain NULL values", name)));
+	}
+
+	return nelems;
+}
+
+/*
+ * Import statistics for a given relation attribute.
+ *
+ * This will insert/replace a row in pg_statistic for the given relation and
+ * attribute name.
+ *
+ * The function takes input parameters that correspond to columns in the view
+ * pg_stats.
+ *
+ * Of those, the columns attname, inherited, null_frac, avg_width, and
+ * n_distinct all correspond to NOT NULL columns in pg_statistic. These
+ * parameters have no default value and passing NULL to them will result
+ * in an error.
+ *
+ * If there is no attribute with a matching attname in the relation, the
+ * function will raise an error. Likewise for setting inherited statistics
+ * on a table that is not partitioned.
+ *
+ * The remaining parameters all belong to a specific stakind. Some stakinds
+ * have multiple parameters, and in those cases both parameters must be
+ * NOT NULL or both NULL, otherwise an error will be raised.
+ *
+ * Omitting a parameter or explicitly passing NULL means that that particular
+ * stakind is not associated with the attribute.
+ *
+ * Parameters that are NOT NULL will be inspected for consistency checks,
+ * any of which can raise an error.
+ *
+ * Parameters corresponding to ANYARRAY columns are instead passed in as text
+ * values, which is a valid input string for an array of the type or basetype
+ * of the attribute. Any error generated by the array_in() function will in
+ * turn fail the function.
+ */
+Datum
+pg_set_attribute_stats(PG_FUNCTION_ARGS)
+{
+	enum
+	{
+		P_RELATION = 0,			/* oid */
+		P_ATTNAME,				/* name */
+		P_INHERITED,			/* bool */
+		P_NULL_FRAC,			/* float4 */
+		P_AVG_WIDTH,			/* int32 */
+		P_N_DISTINCT,			/* float4 */
+		P_MC_VALS,				/* text, null */
+		P_MC_FREQS,				/* float4[], null */
+		P_HIST_BOUNDS,			/* text, null */
+		P_CORRELATION,			/* float4, null */
+		P_MC_ELEMS,				/* text, null */
+		P_MC_ELEM_FREQS,		/* float4[], null */
+		P_ELEM_COUNT_HIST,		/* float4[], null */
+		P_RANGE_LENGTH_HIST,	/* text, null */
+		P_RANGE_EMPTY_FRAC,		/* float4, null */
+		P_RANGE_BOUNDS_HIST,	/* text, null */
+		P_NUM_PARAMS
+	};
+
+	/* names of columns that cannot be null */
+	const char *param_names[] = {
+		"relation",
+		"attname",
+		"inherited",
+		"null_frac",
+		"avg_width",
+		"n_distinct",
+		"most_common_vals",
+		"most_common_freqs",
+		"histogram_bounds",
+		"correlation",
+		"most_common_elems",
+		"most_common_elem_freqs",
+		"elem_count_histogram",
+		"range_length_histogram",
+		"range_empty_frac",
+		"range_bounds_histogram"
+	};
+
+	Oid			relid;
+	Name		attname;
+	bool		inherited;
+	Relation	rel;
+	HeapTuple	ctup;
+	HeapTuple	atup;
+	Form_pg_class pgcform;
+
+	TypeCacheEntry *typcache;
+	const int	operator_flags = TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR;
+
+	Oid			typid;
+	int32		typmod;
+	Oid			typcoll;
+	Oid			eqopr;
+	Oid			ltopr;
+	Oid			basetypid;
+	Oid			baseeqopr;
+	Oid			baseltopr;
+
+	const float4 frac_min = 0.0;
+	const float4 frac_max = 1.0;
+	float4		null_frac;
+	const int	avg_width_min = 0;
+	int			avg_width;
+	const float4 n_distinct_min = -1.0;
+	float4		n_distinct;
+
+	Datum		values[Natts_pg_statistic] = {0};
+	bool		nulls[Natts_pg_statistic] = {false};
+
+	Relation	sd;
+	HeapTuple	oldtup;
+	CatalogIndexState indstate;
+	HeapTuple	stup;
+	Form_pg_attribute attr;
+
+	FmgrInfo	finfo;
+
+	bool		has_mcv;
+	bool		has_mc_elems;
+	bool		has_rl_hist;
+	int			stakind_count;
+
+	int			k = 0;
+
+	/*
+	 * A null in a required parameter is an error.
+	 */
+	for (int i = P_RELATION; i <= P_N_DISTINCT; i++)
+		if (PG_ARGISNULL(i))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be NULL", param_names[i])));
+
+	/*
+	 * Check all parameter pairs up front.
+	 */
+	has_mcv = has_arg_pair(fcinfo, param_names,
+						   P_MC_VALS, P_MC_FREQS);
+	has_mc_elems = has_arg_pair(fcinfo, param_names,
+								P_MC_ELEMS, P_MC_ELEM_FREQS);
+	has_rl_hist = has_arg_pair(fcinfo, param_names,
+							   P_RANGE_LENGTH_HIST, P_RANGE_EMPTY_FRAC);
+
+	/*
+	 * If a caller specifies more stakind-stats than we have slots to store
+	 * them, raise an error.
+	 */
+	stakind_count = (int) has_mcv + (int) has_mc_elems + (int) has_rl_hist +
+		(int) !PG_ARGISNULL(P_HIST_BOUNDS) +
+		(int) !PG_ARGISNULL(P_CORRELATION) +
+		(int) !PG_ARGISNULL(P_ELEM_COUNT_HIST) +
+		(int) !PG_ARGISNULL(P_RANGE_BOUNDS_HIST);
+
+	if (stakind_count > STATISTIC_NUM_SLOTS)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("imported statistics must have a maximum of %d slots "
+						"but %d given",
+						STATISTIC_NUM_SLOTS, stakind_count)));
+
+	relid = PG_GETARG_OID(P_RELATION);
+
+	rel = relation_open(relid, ShareUpdateExclusiveLock);
+
+	/* Test existence of Relation */
+	ctup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+
+	if (!HeapTupleIsValid(ctup))
+		elog(ERROR, "cache lookup failed for relation %u", relid);
+
+	pgcform = (Form_pg_class) GETSTRUCT(ctup);
+
+	if (!canModifyRelation(relid, pgcform))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						RelationGetRelationName(rel))));
+
+	ReleaseSysCache(ctup);
+
+	/*
+	 * Test existence of attribute
+	 */
+	attname = PG_GETARG_NAME(P_ATTNAME);
+	atup = SearchSysCache2(ATTNAME,
+						   ObjectIdGetDatum(relid),
+						   NameGetDatum(attname));
+
+	/* Attribute not found nowhere to import the stats to */
+	if (!HeapTupleIsValid(atup))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s has no attname %s",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+
+	attr = (Form_pg_attribute) GETSTRUCT(atup);
+	if (attr->attisdropped)
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s attname %s is dropped",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+
+	/* Test inherited */
+	inherited = PG_GETARG_BOOL(P_INHERITED);
+	if (inherited &&
+		(rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE) &&
+		(rel->rd_rel->relkind != RELKIND_PARTITIONED_INDEX))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s is not partitioned, cannot accept inherited stats",
+						RelationGetRelationName(rel))));
+
+	/*
+	 * Fetch datatype information, this is needed to derive the proper staopN
+	 * and stacollN values.
+	 *
+	 * If this relation is an index and that index has expressions in it, and
+	 * the attnum specified is known to be an expression, then we must walk
+	 * the list attributes up to the specified attnum to get the right
+	 * expression.
+	 */
+	if ((rel->rd_rel->relkind == RELKIND_INDEX
+		 || (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+		&& (rel->rd_indexprs != NIL)
+		&& (rel->rd_index->indkey.values[attr->attnum - 1] == 0))
+	{
+		ListCell   *indexpr_item = list_head(rel->rd_indexprs);
+		Node	   *expr;
+
+		for (int i = 0; i < attr->attnum - 1; i++)
+			if (rel->rd_index->indkey.values[i] == 0)
+				indexpr_item = lnext(rel->rd_indexprs, indexpr_item);
+
+		if (indexpr_item == NULL)	/* shouldn't happen */
+			elog(ERROR, "too few entries in indexprs list");
+
+		expr = (Node *) lfirst(indexpr_item);
+
+		typid = exprType(expr);
+		typmod = exprTypmod(expr);
+
+		/*
+		 * If a collation has been specified for the index column, use that in
+		 * preference to anything else; but if not, fall back to whatever we
+		 * can get from the expression.
+		 */
+		if (OidIsValid(attr->attcollation))
+			typcoll = attr->attcollation;
+		else
+			typcoll = exprCollation(expr);
+	}
+	else
+	{
+		typid = attr->atttypid;
+		typmod = attr->atttypmod;
+		typcoll = attr->attcollation;
+	}
+
+	/* if it's a multirange, step down to the range type */
+	if (type_is_multirange(typid))
+		typid = get_multirange_range(typid);
+
+	typcache = lookup_type_cache(typid, operator_flags);
+	ltopr = typcache->lt_opr;
+	eqopr = typcache->eq_opr;
+
+	/*
+	 * if it's a range type, swap the subtype for the base type, otherwise get
+	 * the base element type
+	 */
+	if (type_is_range(typid))
+		basetypid = get_range_subtype(typid);
+	else
+		basetypid = get_base_element_type(typid);
+
+	if (basetypid == InvalidOid)
+	{
+		/* type is its own base type */
+		basetypid = typid;
+		baseltopr = ltopr;
+		baseeqopr = eqopr;
+	}
+	else
+	{
+		TypeCacheEntry *bentry = lookup_type_cache(basetypid, operator_flags);
+
+		baseltopr = bentry->lt_opr;
+		baseeqopr = bentry->eq_opr;
+	}
+
+	/* P_HIST_BOUNDS and P_CORRELATION must have a < operator */
+	if (baseltopr == InvalidOid)
+		for (int i = P_HIST_BOUNDS; i <= P_CORRELATION; i++)
+			if (!PG_ARGISNULL(i))
+				ereport(ERROR,
+						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+						 errmsg("Relation %s attname %s cannot "
+								"have stats of type %s",
+								RelationGetRelationName(rel),
+								NameStr(*attname),
+								param_names[i])));
+
+	/* Scalar types can't have P_MC_ELEMS, P_MC_ELEM_FREQS, P_ELEM_COUNT_HIST */
+	/* TODO any other types we can exclude? */
+	if (type_is_scalar(typid))
+		for (int i = P_MC_ELEMS; i <= P_ELEM_COUNT_HIST; i++)
+			if (!PG_ARGISNULL(i))
+				ereport(ERROR,
+						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+						 errmsg("Relation %s attname %s is a scalar type, "
+								"cannot have stats of type %s",
+								RelationGetRelationName(rel),
+								NameStr(*attname),
+								param_names[i])));
+
+	/* Only range types can have P_RANGE_x */
+	if ((!type_is_range(typid)) && (!type_is_multirange(typid)))
+		for (int i = P_RANGE_LENGTH_HIST; i <= P_RANGE_BOUNDS_HIST; i++)
+			if (!PG_ARGISNULL(i))
+				ereport(ERROR,
+						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+						 errmsg("Relation %s attname %s is not a range type, "
+								"cannot have stats of type %s",
+								RelationGetRelationName(rel),
+								NameStr(*attname),
+								param_names[i])));
+
+	/*
+	 * Statistical parameters that must pass data validity tests
+	 */
+	null_frac = PG_GETARG_FLOAT4(P_NULL_FRAC);
+	if ((null_frac < frac_min) || (null_frac > frac_max))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s %f is out of range %.1f to %.1f",
+						param_names[P_NULL_FRAC], null_frac,
+						frac_min, frac_max)));
+
+	avg_width = PG_GETARG_INT32(P_AVG_WIDTH);
+	if (avg_width < avg_width_min)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s %d must be >= %d",
+						param_names[P_AVG_WIDTH], avg_width, avg_width_min)));
+
+	n_distinct = PG_GETARG_FLOAT4(P_N_DISTINCT);
+	if (n_distinct < n_distinct_min)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s %f must be >= %.1f",
+						param_names[P_N_DISTINCT], n_distinct,
+						n_distinct_min)));
+
+	values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(relid);
+	values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(attr->attnum);
+	values[Anum_pg_statistic_stainherit - 1] = PG_GETARG_DATUM(P_INHERITED);
+	values[Anum_pg_statistic_stanullfrac - 1] = PG_GETARG_DATUM(P_NULL_FRAC);
+	values[Anum_pg_statistic_stawidth - 1] = PG_GETARG_DATUM(P_AVG_WIDTH);
+	values[Anum_pg_statistic_stadistinct - 1] = PG_GETARG_DATUM(P_N_DISTINCT);
+
+	fmgr_info(F_ARRAY_IN, &finfo);
+
+	/* MC_VALS && MC_FREQS => STATISTIC_KIND_MCV */
+	if (has_mcv)
+	{
+		const char *freqsname = param_names[P_MC_FREQS];
+		const char *valsname = param_names[P_MC_VALS];
+		Datum		freqs = PG_GETARG_DATUM(P_MC_FREQS);
+		Datum		vals = cast_stavalue(&finfo, PG_GETARG_DATUM(P_MC_VALS),
+										 basetypid, typmod);
+
+		ExpandedArrayHeader *freqsarr = DatumGetExpandedArray(freqs);
+		ExpandedArrayHeader *valsarr = DatumGetExpandedArray(vals);
+
+		int			nvals = value_array_len(valsarr, valsname);
+		int			nfreqs = value_not_null_array_len(freqsarr, freqsname);
+
+		if (nfreqs != nvals)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s has %d elements, but %s has %d elements, "
+							"but they must be equal",
+							freqsname, nfreqs,
+							valsname, nvals)));
+
+		/*
+		 * check that freqs sum to <= 1.0 or some number slightly higer to
+		 * allow for compounded rounding errors.
+		 */
+
+
+		if (nfreqs >= 1)
+		{
+			const float4 freqsummax = 1.1;
+
+			float4		prev = DatumGetFloat4(freqsarr->dvalues[0]);
+			float4		freqsum = prev;
+
+			for (int i = 1; i < nfreqs; i++)
+			{
+				float4		f = DatumGetFloat4(freqsarr->dvalues[i]);
+
+				if (f > prev)
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("%s array values must be in descending "
+									"order, but %f > %f",
+									freqsname, f, prev)));
+
+				freqsum += f;
+				prev = f;
+			}
+
+			if (freqsum > freqsummax)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("The sum of elements in %s must not exceed "
+								"%.2f but is %f",
+								freqsname, freqsummax, freqsum)));
+		}
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_MCV);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(eqopr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] = freqs;
+		values[Anum_pg_statistic_stavalues1 - 1 + k] = vals;
+
+		k++;
+	}
+
+
+	/* HIST_BOUNDS => STATISTIC_KIND_HISTOGRAM */
+	if (!PG_ARGISNULL(P_HIST_BOUNDS))
+	{
+		const char *statname = param_names[P_HIST_BOUNDS];
+		Datum		strvalue = PG_GETARG_DATUM(P_HIST_BOUNDS);
+		Datum		stavalues = cast_stavalue(&finfo, strvalue, basetypid, typmod);
+
+		ExpandedArrayHeader *arr = DatumGetExpandedArray(stavalues);
+		SortSupportData ssupd;
+
+		int			nelems = value_not_null_array_len(arr, statname);
+
+
+
+		memset(&ssupd, 0, sizeof(ssupd));
+		ssupd.ssup_cxt = CurrentMemoryContext;
+		ssupd.ssup_collation = typcoll;
+		ssupd.ssup_nulls_first = false;
+		ssupd.abbreviate = false;
+
+		PrepareSortSupportFromOrderingOp(baseltopr, &ssupd);
+
+		/*
+		 * This is a histogram, which means that the values must be in
+		 * monotonically non-decreasing order. If we every find a case where
+		 * [n] > [n+1], raise an error.
+		 */
+		for (int i = 1; i < nelems; i++)
+		{
+			Datum		a = arr->dvalues[i - 1];
+			Datum		b = arr->dvalues[i];
+
+			if (ssupd.comparator(a, b, &ssupd) > 0)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s values must be in ascending order %s",
+								statname, TextDatumGetCString(strvalue))));
+
+		}
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_HISTOGRAM);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(ltopr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+		k++;
+	}
+
+	/* CORRELATION => STATISTIC_KIND_CORRELATION */
+	if (!PG_ARGISNULL(P_CORRELATION))
+	{
+		const char *statname = param_names[P_CORRELATION];
+		Datum		elem = PG_GETARG_DATUM(P_CORRELATION);
+		Datum		elems[] = {elem};
+		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+
+		const float4 corr_min = -1.0;
+		const float4 corr_max = 1.0;
+		float4		corr = PG_GETARG_FLOAT4(P_CORRELATION);
+
+		if ((corr < corr_min) || (corr > corr_max))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s %f is out of range %.1f to %.1f",
+							statname, corr, corr_min, corr_max)));
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_CORRELATION);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(ltopr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] = PointerGetDatum(arry);
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+		k++;
+	}
+
+	/* MC_ELEMS && MC_ELEM_FREQS => STATISTIC_KIND_MCELEM */
+	if (has_mc_elems)
+	{
+		const char *elemsname = param_names[P_MC_ELEMS];
+		const char *freqsname = param_names[P_MC_ELEM_FREQS];
+		Datum		freqs = PG_GETARG_DATUM(P_MC_ELEM_FREQS);
+		Datum		vals = cast_stavalue(&finfo, PG_GETARG_DATUM(P_MC_ELEMS),
+										 basetypid, typmod);
+
+		ExpandedArrayHeader *freqsarr = DatumGetExpandedArray(freqs);
+		ExpandedArrayHeader *valsarr = DatumGetExpandedArray(vals);
+
+		int			nfreqs = value_not_null_array_len(freqsarr, freqsname);
+		int			nvals = value_not_null_array_len(valsarr, elemsname);
+
+		/*
+		 * The mcelem freqs array has either 2 or 3 additional values: the min
+		 * frequency, the max frequency, the optional null frequency.
+		 */
+		int			nfreqsmin = nvals + 2;
+		int			nfreqsmax = nvals + 3;
+
+		float4		freqlowbound;
+		float4		freqhighbound;
+
+		if (nfreqs > 0)
+		{
+			if ((nfreqs < nfreqsmin) || (nfreqs > nfreqsmax))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s has %d elements, but must have between "
+								"%d and %d because %s has %d elements",
+								freqsname, nfreqs, nfreqsmin, nfreqsmax,
+								elemsname, nvals)));
+
+			/*
+			 * the freqlowbound and freqhighbound must themselves be valid
+			 * percentages
+			 */
+
+			/* first freq element past the length of the values is the min */
+			freqlowbound = DatumGetFloat4(freqsarr->dvalues[nvals]);
+			if ((freqlowbound < frac_min) || (freqlowbound > frac_max))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s %s frequency %f is out of "
+								"range %.1f to %.1f",
+								freqsname, "minimum", freqlowbound,
+								frac_min, frac_max)));
+
+			/* second freq element past the length of the values is the max */
+			freqhighbound = DatumGetFloat4(freqsarr->dvalues[nvals + 1]);
+			if ((freqhighbound < frac_min) || (freqhighbound > frac_max))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s %s frequency %f is out of "
+								"range %.1f to %.1f",
+								freqsname, "maximum", freqhighbound,
+								frac_min, frac_max)));
+
+			/* low bound must be < high bound */
+			if (freqlowbound > freqhighbound)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s frequency low bound %f cannot be greater "
+								"than high bound %f",
+								freqsname, freqlowbound, freqhighbound)));
+
+			/*
+			 * third freq element past the length of the values is the null
+			 * frac
+			 */
+			if (nfreqs == nvals + 3)
+			{
+				float4		freqnullpct;
+
+				freqnullpct = DatumGetFloat4(freqsarr->dvalues[nvals + 2]);
+
+				if ((freqnullpct < frac_min) || (freqnullpct > frac_max))
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("%s %s frequency %f is out of "
+									"range %.1f to %.1f",
+									freqsname, "null", freqnullpct,
+									frac_min, frac_max)));
+			}
+
+			/*
+			 * All the freqs that match up to a val must bet between low/high
+			 * bounds (which is never less strict than frac_min/frac_max) and
+			 * must be in monotonically non-increasing order.
+			 *
+			 * Also, these frequencies do not sum to a number <= 1.0 as is the
+			 * case with MC_FREQS.
+			 */
+			if (nvals > 1)
+			{
+				float4		prev = DatumGetFloat4(freqsarr->dvalues[0]);
+
+				for (int i = 1; i < nvals; i++)
+				{
+					float4		f = DatumGetFloat4(freqsarr->dvalues[i]);
+
+					if ((f < freqlowbound) || (f > freqhighbound))
+						ereport(ERROR,
+								(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+								 errmsg("%s frequency %f is out of range "
+										"%f to %f",
+										freqsname, f,
+										freqlowbound, freqhighbound)));
+					if (f > prev)
+						ereport(ERROR,
+								(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+								 errmsg("%s array values must be in "
+										"descending order, but %f > %f",
+										freqsname, f, prev)));
+
+					prev = f;
+				}
+			}
+		}
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_MCELEM);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(baseeqopr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] =
+			PG_GETARG_DATUM(P_MC_ELEM_FREQS);
+		values[Anum_pg_statistic_stavalues1 - 1 + k] =
+			cast_stavalue(&finfo, PG_GETARG_DATUM(P_MC_ELEMS),
+						  basetypid, typmod);
+
+		k++;
+	}
+
+	/* ELEM_COUNT_HIST => STATISTIC_KIND_DECHIST */
+	if (!PG_ARGISNULL(P_ELEM_COUNT_HIST))
+	{
+		const char *statname = param_names[P_ELEM_COUNT_HIST];
+		Datum		stanumbers = PG_GETARG_DATUM(P_ELEM_COUNT_HIST);
+
+		ExpandedArrayHeader *arr = DatumGetExpandedArray(stanumbers);
+
+		const float4 last_min = 0.0;
+
+		int			nelems = value_not_null_array_len(arr, statname);
+		float4		last;
+
+		/* Last element must be >= 0 */
+		last = DatumGetFloat4(arr->dvalues[nelems - 1]);
+		if (last < last_min)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s has last element %f < %.1f",
+							statname, last, last_min)));
+
+		/* all other elements must be monotonically nondecreasing */
+		if (nelems > 1)
+		{
+			float4		prev = DatumGetFloat4(arr->dvalues[0]);
+
+			for (int i = 1; i < nelems - 1; i++)
+			{
+				float4		f = DatumGetFloat4(arr->dvalues[i]);
+
+				if (f < prev)
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("%s array values must be in ascending "
+									"order, but %f > %f",
+									statname, prev, f)));
+
+				prev = f;
+			}
+		}
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_DECHIST);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(baseeqopr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+		k++;
+	}
+
+	/*
+	 * RANGE_BOUNDS_HIST => STATISTIC_KIND_BOUNDS_HISTOGRAM
+	 *
+	 * This stakind appears before STATISTIC_KIND_BOUNDS_HISTOGRAM even though
+	 * it is numerically greater, and all other stakinds appear in numerical
+	 * order. We duplicate this quirk to make before/after tests of
+	 * pg_statistic records easier.
+	 *
+	 */
+	if (!PG_ARGISNULL(P_RANGE_BOUNDS_HIST))
+	{
+		const char *statname = param_names[P_RANGE_BOUNDS_HIST];
+		Datum		strvalue = PG_GETARG_DATUM(P_RANGE_BOUNDS_HIST);
+		Datum		stavalues = cast_stavalue(&finfo, strvalue, typid, typmod);
+
+		ExpandedArrayHeader *arr = DatumGetExpandedArray(stavalues);
+
+		int			nelems = value_not_null_array_len(arr, statname);
+
+		/*
+		 * The values in this array are range types, but in fact it's using
+		 * range types to have two parallel arrays with inclusive/exclusive
+		 * bounds, and those two arrays must each be monotonically
+		 * nondecreasing. So basically we want to test that: lower_bound(N) <=
+		 * lower_bound(N+1) and upper_bound(N) <= upper_bound(N+1)
+		 */
+		if (nelems > 1)
+		{
+			RangeType  *prevrange = DatumGetRangeTypeP(arr->dvalues[0]);
+			RangeBound	prevlower;
+			RangeBound	prevupper;
+			bool		empty;
+
+			range_deserialize(typcache, prevrange, &prevlower,
+							  &prevupper, &empty);
+
+
+			for (int i = 1; i < nelems; i++)
+			{
+				RangeType  *range = DatumGetRangeTypeP(arr->dvalues[i]);
+				RangeBound	lower;
+				RangeBound	upper;
+
+				range_deserialize(typcache, range, &lower, &upper, &empty);
+
+				if (range_cmp_bounds(typcache, &prevlower, &lower) == 1)
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("%s array %s bounds must be in ascending order",
+									statname, "lower")));
+
+				if (range_cmp_bounds(typcache, &prevupper, &upper) == 1)
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("%s array %s bounds must be in ascending order",
+									statname, "upper")));
+
+				memcpy(&lower, &prevlower, sizeof(RangeBound));
+				memcpy(&upper, &prevupper, sizeof(RangeBound));
+			}
+		}
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_BOUNDS_HISTOGRAM);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+		k++;
+	}
+
+	/*
+	 * P_RANGE_LENGTH_HIST && P_RANGE_EMPTY_FRAC =>
+	 * STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM
+	 */
+	if (has_rl_hist)
+	{
+		const char *histname = param_names[P_RANGE_LENGTH_HIST];
+		const char *fracname = param_names[P_RANGE_EMPTY_FRAC];
+		Datum		elem = PG_GETARG_DATUM(P_RANGE_EMPTY_FRAC);
+		Datum		rlhist = PG_GETARG_DATUM(P_RANGE_LENGTH_HIST);
+		Datum		elems[] = {elem};
+		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+		float4		frac = DatumGetFloat4(elem);
+		Datum		stavalue;
+
+		ExpandedArrayHeader *arr;
+		int			nelems;
+
+		if ((frac < frac_min) || (frac > frac_max))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s %f is out of range %.1f to %.1f",
+							fracname, frac, frac_min, frac_max)));
+
+		/*
+		 * RANGE_LENGTH_HIST is stored in an anyarray, but it is known to be
+		 * of type float8[]. It is also a histogram, so it must be in
+		 * monotonically nondecreasing order.
+		 */
+		stavalue = cast_stavalue(&finfo, rlhist, FLOAT8OID, typmod);
+
+		arr = DatumGetExpandedArray(stavalue);
+		nelems = value_not_null_array_len(arr, histname);
+
+		if (nelems > 1)
+		{
+			float8		prev = DatumGetFloat8(arr->dvalues[0]);
+
+			for (int i = 1; i < nelems; i++)
+			{
+				float8		f = DatumGetFloat8(arr->dvalues[i]);
+
+				if (f < prev)
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("%s array values must be in ascending "
+									"order, but %f > %f",
+									histname, prev, f)));
+
+				prev = f;
+			}
+		}
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(Float8LessOperator);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] = PointerGetDatum(arry);
+		values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalue;
+		k++;
+	}
+
+	/* fill in all remaining slots */
+	for (; k < STATISTIC_NUM_SLOTS; k++)
+	{
+		values[Anum_pg_statistic_stakind1 - 1 + k] = Int16GetDatum(0);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+	}
+
+	/* Is there already a pg_statistic tuple for this attribute? */
+	oldtup = SearchSysCache3(STATRELATTINH,
+							 ObjectIdGetDatum(relid),
+							 Int16GetDatum(attr->attnum),
+							 PG_GETARG_DATUM(P_INHERITED));
+
+	sd = table_open(StatisticRelationId, RowExclusiveLock);
+	indstate = CatalogOpenIndexes(sd);
+
+	if (HeapTupleIsValid(oldtup))
+	{
+		/* Yes, replace it */
+		bool		replaces[Natts_pg_statistic];
+
+		for (int i = 0; i < Natts_pg_statistic; i++)
+			replaces[i] = true;
+
+		stup = heap_modify_tuple(oldtup, RelationGetDescr(sd),
+								 values, nulls, replaces);
+		ReleaseSysCache(oldtup);
+		CatalogTupleUpdateWithInfo(sd, &stup->t_self, stup, indstate);
+	}
+	else
+	{
+		/* No, insert new tuple */
+		stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
+		CatalogTupleInsertWithInfo(sd, stup, indstate);
+	}
+
+	heap_freetuple(stup);
+	CatalogCloseIndexes(indstate);
+	table_close(sd, NoLock);
+	relation_close(rel, NoLock);
+	ReleaseSysCache(atup);
+	PG_RETURN_VOID();
+}
diff --git a/src/test/regress/expected/stats_export_import.out b/src/test/regress/expected/stats_export_import.out
new file mode 100644
index 0000000000..32b9e7ca11
--- /dev/null
+++ b/src/test/regress/expected/stats_export_import.out
@@ -0,0 +1,939 @@
+CREATE SCHEMA stats_export_import;
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+        0 |        -1 |             0
+(1 row)
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass, 999, 3.6::real, 15000);
+ pg_set_relation_stats 
+-----------------------
+ 
+(1 row)
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+      999 |       3.6 |         15000
+(1 row)
+
+-- error: relpages null
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass, NULL, 3.6::real, 15000);
+ERROR:  relpages cannot be NULL
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+      999 |       3.6 |         15000
+(1 row)
+
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => '0'::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  could not open relation with OID 0
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => NULL::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  relation cannot be NULL
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => NULL::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  attname cannot be NULL
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => NULL::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  inherited cannot be NULL
+-- error: inherited true on nonpartitioned table
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => true::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  Relation test is not partitioned, cannot accept inherited stats
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => NULL::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  null_frac cannot be NULL
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => NULL::integer,
+    n_distinct => 0.3::real);
+ERROR:  avg_width cannot be NULL
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => NULL::real);
+ERROR:  n_distinct cannot be NULL
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.1 |         2 |        0.3 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- error: null_frac < 0
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => -0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  null_frac -0.100000 is out of range 0.0 to 1.0
+-- error: null_frac > 1
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 1.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  null_frac 1.100000 is out of range 0.0 to 1.0
+-- error: avg_width < 0
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => -1::integer,
+    n_distinct => 0.3::real);
+ERROR:  avg_width -1 must be >= 0
+-- error: n_distinct < -1
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -1.1::real);
+ERROR:  n_distinct -1.100000 must be >= -1.0
+-- error: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => NULL::text,
+    most_common_freqs => '{0.1,0.2,0.3}'::real[]
+    );
+ERROR:  most_common_vals cannot be NULL when most_common_freqs is NOT NULL
+-- error: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{1,2,3}'::text,
+    most_common_freqs => NULL::real[]
+    );
+ERROR:  most_common_freqs cannot be NULL when most_common_vals is NOT NULL
+-- error: mcv / mcf length mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.2,0.1}'::real[]
+    );
+ERROR:  most_common_freqs has 2 elements, but most_common_vals has 3 elements, but they must be equal
+-- error: mcf sum bad
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.6,0.5,0.3}'::real[]
+    );
+ERROR:  The sum of elements in most_common_freqs must not exceed 1.10 but is 1.400000
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- error: histogram elements null value
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,NULL,3,4}'::text
+    );
+ERROR:  histogram_bounds array cannot contain NULL values
+-- error: histogram elements must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,20,3,4}'::text
+    );
+ERROR:  histogram_bounds values must be in ascending order {1,20,3,4}
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,2,3,4}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- error: correlation low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => -1.1::real);
+ERROR:  correlation -1.100000 is out of range -1.0 to 1.0
+-- error: correlation high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 1.1::real);
+ERROR:  correlation 1.100000 is out of range -1.0 to 1.0
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 0.5::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |         0.5 |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- error: scalars can't have mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{1,3}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+ERROR:  Relation test attname id is a scalar type, cannot have stats of type most_common_elems
+-- error: mcelem / mcelem null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,two}'::text,
+    most_common_elem_freqs => NULL::real[]
+    );
+ERROR:  most_common_elem_freqs cannot be NULL when most_common_elems is NOT NULL
+-- error: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => NULL::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
+    );
+ERROR:  most_common_elems cannot be NULL when most_common_elem_freqs is NOT NULL
+-- error: mcelem / mcelem length mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.2}'::real[]
+    );
+ERROR:  most_common_elem_freqs has 2 elements, but must have between 4 and 5 because most_common_elems has 2 elements
+-- error: mcelem freq element out of bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.1,0.2,0.3,0.0}'::real[]
+    );
+ERROR:  most_common_elem_freqs frequency 0.100000 is out of range 0.200000 to 0.300000
+-- error: mcelem freq low-high mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.4,0.3,0.0}'::real[]
+    );
+ERROR:  most_common_elem_freqs frequency low bound 0.400000 cannot be greater than high bound 0.300000
+-- error: mcelem freq null pct invalid
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,-0.0001}'::real[]
+    );
+ERROR:  most_common_elem_freqs null frequency -0.000100 is out of range 0.0 to 1.0
+-- error: mcelem freq bad low bound low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.2,-0.15,0.3,0.1}'::real[]
+    );
+ERROR:  most_common_elem_freqs minimum frequency -0.150000 is out of range 0.0 to 1.0
+-- error: mcelem freq bad low bound high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.2,1.5,0.3,0.1}'::real[]
+    );
+ERROR:  most_common_elem_freqs minimum frequency 1.500000 is out of range 0.0 to 1.0
+-- error: mcelem freq bad high bound low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.4,-0.3,0.1}'::real[]
+    );
+ERROR:  most_common_elem_freqs maximum frequency -0.300000 is out of range 0.0 to 1.0
+-- error: mcelem freq bad high bound high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.4,3.0,0.1}'::real[]
+    );
+ERROR:  most_common_elem_freqs maximum frequency 3.000000 is out of range 0.0 to 1.0
+-- error mcelem freq must be non-increasing
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.4,0.2,0.3,0.0}'::real[]
+    );
+ERROR:  most_common_elem_freqs frequency 0.400000 is out of range 0.200000 to 0.300000
+-- ok: mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             | {three,one}       | {0.3,0.2,0.2,0.3,0}    |                      |                        |                  | 
+(1 row)
+
+-- error: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ERROR:  Relation test attname id is a scalar type, cannot have stats of type elem_count_histogram
+-- error: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ERROR:  elem_count_histogram array cannot contain NULL values
+-- error: elem_count_histogram must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ERROR:  elem_count_histogram array values must be in ascending order, but 1.000000 > 0.000000
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+(1 row)
+
+-- error: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ERROR:  Relation test attname id is not a range type, cannot have stats of type range_length_histogram
+-- error: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ERROR:  range_empty_frac cannot be NULL when range_length_histogram is NOT NULL
+-- error: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real
+    );
+ERROR:  range_length_histogram cannot be NULL when range_empty_frac is NOT NULL
+-- error: range_empty_frac low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => -0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ERROR:  range_empty_frac -0.500000 is out of range 0.0 to 1.0
+-- error: range_empty_frac high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 1.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ERROR:  range_empty_frac 1.500000 is out of range 0.0 to 1.0
+-- error: range_length_histogram not ascending
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,Infinity,499}'::text
+    );
+ERROR:  range_length_histogram array values must be in ascending order, but Infinity > 499.000000
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
+(1 row)
+
+-- error: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ERROR:  Relation test attname id is not a range type, cannot have stats of type range_bounds_histogram
+-- error: range_bound_hist low bounds must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[-2,4)","[1,4)","[1,100)"}'::text
+    );
+ERROR:  range_bounds_histogram array lower bounds must be in ascending order
+-- error: range_bound_hist high bounds must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,11)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ERROR:  range_bounds_histogram array upper bounds must be in ascending order
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
+ stats_export_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      |                        |                  | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+(1 row)
+
+-- error: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[],
+    histogram_bounds => '{1,2,3,4}'::text,
+    correlation => 1.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    range_empty_frac => -0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ERROR:  imported statistics must have a maximum of 5 slots but 6 given
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+   relname    | num_stats 
+--------------+-----------
+ is_odd       |         1
+ is_odd_clone |         1
+ test         |         5
+ test_clone   |         5
+(4 rows)
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 5ac6e871f5..a7a4dfd411 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,8 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc stats_export_import
+
 
 # ----------
 # Load huge amounts of data
diff --git a/src/test/regress/sql/stats_export_import.sql b/src/test/regress/sql/stats_export_import.sql
new file mode 100644
index 0000000000..b3f17373d3
--- /dev/null
+++ b/src/test/regress/sql/stats_export_import.sql
@@ -0,0 +1,834 @@
+CREATE SCHEMA stats_export_import;
+
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass, 999, 3.6::real, 15000);
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+-- error: relpages null
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass, NULL, 3.6::real, 15000);
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => '0'::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => NULL::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => NULL::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => NULL::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: inherited true on nonpartitioned table
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => true::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => NULL::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => NULL::integer,
+    n_distinct => 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => NULL::real);
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- error: null_frac < 0
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => -0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: null_frac > 1
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 1.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: avg_width < 0
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => -1::integer,
+    n_distinct => 0.3::real);
+
+-- error: n_distinct < -1
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -1.1::real);
+
+-- error: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => NULL::text,
+    most_common_freqs => '{0.1,0.2,0.3}'::real[]
+    );
+
+-- error: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{1,2,3}'::text,
+    most_common_freqs => NULL::real[]
+    );
+
+-- error: mcv / mcf length mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.2,0.1}'::real[]
+    );
+
+-- error: mcf sum bad
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.6,0.5,0.3}'::real[]
+    );
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- error: histogram elements null value
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,NULL,3,4}'::text
+    );
+-- error: histogram elements must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,20,3,4}'::text
+    );
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,2,3,4}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- error: correlation low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => -1.1::real);
+
+-- error: correlation high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 1.1::real);
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 0.5::real);
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- error: scalars can't have mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{1,3}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+-- error: mcelem / mcelem null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,two}'::text,
+    most_common_elem_freqs => NULL::real[]
+    );
+
+-- error: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => NULL::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
+    );
+
+-- error: mcelem / mcelem length mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.2}'::real[]
+    );
+
+-- error: mcelem freq element out of bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.1,0.2,0.3,0.0}'::real[]
+    );
+
+-- error: mcelem freq low-high mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.4,0.3,0.0}'::real[]
+    );
+
+-- error: mcelem freq null pct invalid
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,-0.0001}'::real[]
+    );
+
+-- error: mcelem freq bad low bound low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.2,-0.15,0.3,0.1}'::real[]
+    );
+
+-- error: mcelem freq bad low bound high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.2,1.5,0.3,0.1}'::real[]
+    );
+
+-- error: mcelem freq bad high bound low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.4,-0.3,0.1}'::real[]
+    );
+
+-- error: mcelem freq bad high bound high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.4,3.0,0.1}'::real[]
+    );
+
+-- error mcelem freq must be non-increasing
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.4,0.2,0.3,0.0}'::real[]
+    );
+
+-- ok: mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- error: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- error: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- error: elem_count_histogram must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- error: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+-- error: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+-- error: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real
+    );
+-- error: range_empty_frac low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => -0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+-- error: range_empty_frac high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 1.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+-- error: range_length_histogram not ascending
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,Infinity,499}'::text
+    );
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- error: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+-- error: range_bound_hist low bounds must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[-2,4)","[1,4)","[1,100)"}'::text
+    );
+-- error: range_bound_hist high bounds must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,11)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- error: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[],
+    histogram_bounds => '{1,2,3,4}'::text,
+    correlation => 1.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    range_empty_frac => -0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+
+SELECT
+    format('SELECT pg_catalog.pg_set_attribute_stats( '
+            || 'relation => %L::regclass::oid, attname => %L::name, '
+            || 'inherited => %L::boolean, null_frac => %L::real, '
+            || 'avg_width => %L::integer, n_distinct => %L::real, '
+            || 'most_common_vals => %L::text, '
+            || 'most_common_freqs => %L::real[], '
+            || 'histogram_bounds => %L::text, '
+            || 'correlation => %L::real, '
+            || 'most_common_elems => %L::text, '
+            || 'most_common_elem_freqs => %L::real[], '
+            || 'elem_count_histogram => %L::real[], '
+            || 'range_length_histogram => %L::text, '
+            || 'range_empty_frac => %L::real, '
+            || 'range_bounds_histogram => %L::text) ',
+        'stats_export_import.' || s.tablename || '_clone', s.attname,
+        s.inherited, s.null_frac,
+        s.avg_width, s.n_distinct,
+        s.most_common_vals, s.most_common_freqs, s.histogram_bounds,
+        s.correlation, s.most_common_elems, s.most_common_elem_freqs,
+        s.elem_count_histogram, s.range_length_histogram,
+        s.range_empty_frac, s.range_bounds_histogram)
+FROM pg_catalog.pg_stats AS s
+WHERE s.schemaname = 'stats_export_import'
+AND s.tablename IN ('test', 'is_odd')
+\gexec
+
+-- restore ECHO to original value
+\set ECHO :orig_ECHO
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 8ecc02f2b9..72711d86d3 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29090,6 +29090,116 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
     in identifying the specific disk files associated with database objects.
    </para>
 
+   <table id="functions-admin-statsimport">
+    <title>Database Object Statistics Import Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_relation_stats</primary>
+        </indexterm>
+        <function>pg_set_relation_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>relpages</parameter> <type>integer</type>,
+         <parameter>reltuples</parameter> <type>real</type>,
+         <parameter>relallvisible</parameter> <type>integer</type> )
+        <returnvalue>void</returnvalue>
+       </para>
+       <para>
+        Updates the <structname>pg_class</structname> row for the specified
+        <parameter>relation</parameter>, setting the values for the columns
+        <structfield>reltuples</structfield>,
+        <structfield>relpages</structfield>, and
+        <structfield>relallvisible</structfield>.
+        To avoid table bloat in <structname>pg_class</structname>, this change
+        is made with an in-place update, and therefore cannot be rolled back through
+        normal transaction processing.
+       </para>
+       <para>
+        The purpose of this function is to apply statistics values in an
+        upgrade situation that are "good enough" for system operation until
+        they are replaced by the next <command>ANALYZE</command>, usually via
+        <command>autovacuum</command> This function is used by
+        <command>pg_upgrade</command> and <command>pg_restore</command> to
+        convey the statistics from the old system version into the new one.
+       </para>
+       <para>
+        The caller must either be the owner of the relation, or have superuser
+        privileges.
+       </para>
+       </entry>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_attribute_stats</primary>
+        </indexterm>
+        <function>pg_set_attribute_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>
+         , <parameter>attname</parameter> <type>name</type>
+         , <parameter>inherited</parameter> <type>boolean</type>
+         , <parameter>null_frac</parameter> <type>real</type>
+         , <parameter>avg_width</parameter> <type>integer</type>
+         , <parameter>n_distinct</parameter> <type>real</type>
+         [, <parameter>most_common_vals</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_freqs</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>histogram_bounds</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>correlation</parameter> <type>real</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_elems</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_elem_freqs</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>elem_count_histogram</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_length_histogram</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_empty_frac</parameter> <type>real</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_bounds_histogram</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ] )
+        <returnvalue>void</returnvalue>
+       </para>
+       <para>
+        Replaces the <structname>pg_statistic</structname> row for the
+        <structname>pg_attribute</structname> row specified by
+        <parameter>relation</parameter>, <parameter>attname</parameter>
+        and <parameter>inherited</parameter>.  Aside from
+        <parameter>relation</parameter>, the parameters in this are all
+        derived from <structname>pg_stats</structname>, and the values
+        given are most often extracted from there.
+       </para>
+       <para>
+        The purpose of this function is to apply statistics values in an
+        upgrade situation that are "good enough" for system operation until
+        they are replaced by the next <command>ANALYZE</command>, usually via
+        <command>autovacuum</command> This function is used by
+        <command>pg_upgrade</command> and <command>pg_restore</command> to
+        convey the statistics from the old system version into the new one.
+       </para>
+       <para>
+        The caller must either be the owner of the relation, or have superuser
+        privileges.
+       </para>
+       </entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
    <table id="functions-admin-dblocation">
     <title>Database Object Location Functions</title>
     <tgroup cols="1">
-- 
2.44.0

v13-0002-Enable-dumping-of-table-index-stats-in-pg_dump.patchtext/x-patch; charset=US-ASCII; name=v13-0002-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From ce99e03de965e4a14f0c5f9487ba484b3b91d544 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH v13 2/2] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will also generate a statement
that calls all of the pg_set_relation_stats() and
pg_set_attribute_stats() calls necessary to restore the statistics of
the current system onto the destination system.

As is the pattern with pg_dump options, this can be disabled with
--no-statistics.
---
 src/include/fe_utils/stats_export.h  |  36 +++++
 src/fe_utils/Makefile                |   1 +
 src/fe_utils/meson.build             |   1 +
 src/fe_utils/stats_export.c          | 201 +++++++++++++++++++++++++++
 src/bin/pg_dump/pg_backup.h          |   2 +
 src/bin/pg_dump/pg_backup_archiver.c |   5 +
 src/bin/pg_dump/pg_dump.c            | 100 ++++++++++++-
 src/bin/pg_dump/pg_dump.h            |   1 +
 src/bin/pg_dump/pg_dumpall.c         |   5 +
 src/bin/pg_dump/pg_restore.c         |   3 +
 10 files changed, 353 insertions(+), 2 deletions(-)
 create mode 100644 src/include/fe_utils/stats_export.h
 create mode 100644 src/fe_utils/stats_export.c

diff --git a/src/include/fe_utils/stats_export.h b/src/include/fe_utils/stats_export.h
new file mode 100644
index 0000000000..f0dc7041f7
--- /dev/null
+++ b/src/include/fe_utils/stats_export.h
@@ -0,0 +1,36 @@
+/*-------------------------------------------------------------------------
+ *
+ * stats_export.h
+ *    Queries to export statistics from current and past versions.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1995, Regents of the University of California
+ *
+ * src/include/varatt.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef STATS_EXPORT_H
+#define STATS_EXPORT_H
+
+#include "postgres_fe.h"
+#include "libpq-fe.h"
+
+/*
+ * The minimum supported version number. No attempt is made to get statistics
+ * import to work on versions older than this. This version was initially chosen
+ * because that was the minimum version supported by pg_dump at the time.
+ */
+#define MIN_SERVER_NUM 90200
+
+extern bool exportStatsSupported(int server_version_num);
+extern bool exportExtStatsSupported(int server_version_num);
+
+extern const char *exportClassStatsSQL(int server_verson_num);
+extern const char *exportAttributeStatsSQL(int server_verson_num);
+
+extern char *exportRelationStatsSQL(int server_version_num);
+
+#endif
diff --git a/src/fe_utils/Makefile b/src/fe_utils/Makefile
index 946c05258f..c734f9f6d3 100644
--- a/src/fe_utils/Makefile
+++ b/src/fe_utils/Makefile
@@ -32,6 +32,7 @@ OBJS = \
 	query_utils.o \
 	recovery_gen.o \
 	simple_list.o \
+	stats_export.o \
 	string_utils.o
 
 ifeq ($(PORTNAME), win32)
diff --git a/src/fe_utils/meson.build b/src/fe_utils/meson.build
index 14d0482a2c..fce503f641 100644
--- a/src/fe_utils/meson.build
+++ b/src/fe_utils/meson.build
@@ -12,6 +12,7 @@ fe_utils_sources = files(
   'query_utils.c',
   'recovery_gen.c',
   'simple_list.c',
+  'stats_export.c',
   'string_utils.c',
 )
 
diff --git a/src/fe_utils/stats_export.c b/src/fe_utils/stats_export.c
new file mode 100644
index 0000000000..fd09e6ea8a
--- /dev/null
+++ b/src/fe_utils/stats_export.c
@@ -0,0 +1,201 @@
+/*-------------------------------------------------------------------------
+ *
+ * Utility functions for extracting object statistics for frontend code
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/fe_utils/stats_export.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include "fe_utils/stats_export.h"
+/*
+#include "libpq/libpq-fs.h"
+*/
+#include "fe_utils/string_utils.h"
+
+/*
+ * No-frills catalog queries that are named according to the statistics they
+ * fetch (relation, attribute, extended) and the earliest server version for
+ * which they work. These are presented so that if other use cases arise they
+ * can share the same base queries but utilize them in their own way.
+ *
+ * The queries themselves do not filter results, so it is up to the caller
+ * to append a WHERE clause filtering either on either c.oid or a combination
+ * of c.relname and n.nspname.
+ */
+
+const char *export_class_stats_query_v9_2 =
+	"SELECT c.oid, n.nspname, c.relname, c.relpages, c.reltuples, c.relallvisible "
+	"FROM pg_class AS c "
+	"JOIN pg_namespace AS n ON n.oid = c.relnamespace";
+
+const char *export_attribute_stats_query_v17 =
+	"SELECT c.oid, n.nspname, c.relname, a.attnum, a.attname, s.inherited, "
+	"s.null_frac, s.avg_width, s.n_distinct, "
+	"s.most_common_vals::text AS most_common_vals, s.most_common_freqs, "
+	"s.histogram_bounds::text AS histogram_bounds, s.correlation, "
+	"s.most_common_elems::text AS most_common_elems, "
+	"s.most_common_elem_freqs, s.elem_count_histogram, "
+	"s.range_length_histogram::text AS range_length_histogram, "
+	"s.range_empty_frac, "
+	"s.range_bounds_histogram::text AS range_bounds_histogram "
+	"FROM pg_class AS c "
+	"JOIN pg_namespace AS n ON n.oid = c.relnamespace "
+	"JOIN pg_attribute AS a ON a.attrelid = c.oid AND not a.attisdropped "
+	"JOIN pg_stats AS s ON s.schemaname = n.nspname AND s.tablename = c.relname";
+
+const char *export_attribute_stats_query_v9_2 =
+	"SELECT c.oid, n.nspname, c.relname, a.attnum, a.attname, s.inherited, "
+	"s.null_frac, s.avg_width, s.n_distinct, "
+	"s.most_common_vals::text AS most_common_vals, s.most_common_freqs, "
+	"s.histogram_bounds::text AS histogram_bounds, s.correlation, "
+	"s.most_common_elems::text AS most_common_elems, "
+	"s.most_common_elem_freqs, s.elem_count_histogram, "
+	"NULL::text AS range_length_histogram, NULL::real AS range_empty_frac, "
+	"NULL::text AS range_bounds_histogram "
+	"FROM pg_class AS c "
+	"JOIN pg_namespace AS n ON n.oid = c.relnamespace "
+	"JOIN pg_attribute AS a ON a.attrelid = c.oid AND not a.attisdropped "
+	"JOIN pg_stats AS s ON s.schemaname = n.nspname AND s.tablename = c.relname";
+
+/*
+ * Returns true if the server version number supports exporting regular
+ * (e.g. pg_statistic) statistics.
+ */
+bool
+exportStatsSupported(int server_version_num)
+{
+	return (server_version_num >= MIN_SERVER_NUM);
+}
+
+/*
+ * Returns true if the server version number supports exporting extended
+ * (e.g. pg_statistic_ext, pg_statitic_ext_data) statistics.
+ *
+ * Currently, none do.
+ */
+bool
+exportExtStatsSupported(int server_version_num)
+{
+	return false;
+}
+
+/*
+ * Return the query appropriate for extracting relation statistics for the
+ * given server version, if one exists.
+ */
+const char *
+exportClassStatsSQL(int server_version_num)
+{
+	if (server_version_num >= MIN_SERVER_NUM)
+		return export_class_stats_query_v9_2;
+	return NULL;
+}
+
+/*
+ * Return the query appropriate for extracting attribute statistics for the
+ * given server version, if one exists.
+ */
+const char *
+exportAttributeStatsSQL(int server_version_num)
+{
+	if (server_version_num >= 170000)
+		return export_attribute_stats_query_v17;
+	if (server_version_num >= MIN_SERVER_NUM)
+		return export_attribute_stats_query_v9_2;
+	return NULL;
+}
+
+/*
+ * Generate a SQL statement that will itself generate a SQL statement to
+ * import all regular stats from a given relation into another relation.
+ *
+ * The query generated takes two parameters.
+ *
+ * $1 is of type Oid, and represents the oid of the source relation.
+ *
+ * $2 is is a cstring, and represents the qualified name of the destination
+ * relation. If NULL, then the qualified name of the source relation will
+ * be used. In either case, the value is casted via ::regclass.
+ *
+ * The function will return NULL for invalid server version numbers.
+ * Otherwise,
+ *
+ * This function needs to work on databases back to 9.2.
+ * The format() function was introduced in 9.1.
+ * The string_agg() aggregate was introduced in 9.0.
+ *
+ */
+char *exportRelationStatsSQL(int server_version_num)
+{
+	const char *relsql = exportClassStatsSQL(server_version_num);
+	const char *attrsql = exportAttributeStatsSQL(server_version_num);
+	const char *filter = "WHERE c.oid = $1::regclass";
+	char	   *s;
+	PQExpBuffer sql;
+
+	if ((relsql == NULL) || (attrsql == NULL))
+		return NULL;
+
+	/*
+	 * Set up the initial CTEs each with the same oid filter
+	 */
+	sql = createPQExpBuffer();
+	appendPQExpBuffer(sql,
+					  "WITH r AS (%s %s), a AS (%s %s), ",
+					  relsql, filter, attrsql, filter);
+
+	/*
+	 * Generate the pg_set_relation_stats function call for the relation
+	 * and one pg_set_attribute_stats function call for each attribute with
+	 * a pg_statistic entry. Give each row an order value such that the
+	 * set relation stats call will be first, followed by the set attribute
+	 * stats calls in attnum order (even though the attributes are identified
+	 * by attname).
+	 *
+	 * Then aggregate the function calls into a single SELECT statement that
+	 * puts the calls in the order described above.
+	 */
+	appendPQExpBufferStr(sql,
+		"s(ord,sql) AS ( "
+			"SELECT 0, format('pg_catalog.pg_set_relation_stats("
+			"%L::regclass, %L::integer, %L::real, %L::integer)', "
+			"coalesce($2, format('%I.%I', r.nspname, r.relname)), "
+			"r.relpages, r.reltuples, r.relallvisible) "
+			"FROM r "
+			"UNION ALL "
+			"SELECT 1, format('pg_catalog.pg_set_attribute_stats( "
+			"relation => %L::regclass, attname => %L::name, "
+			"inherited => %L::boolean, null_frac => %L::real, "
+			"avg_width => %L::integer, n_distinct => %L::real, "
+			"most_common_vals => %L::text, "
+			"most_common_freqs => %L::real[], "
+			"histogram_bounds => %L::text, "
+			"correlation => %L::real, "
+			"most_common_elems => %L::text, "
+			"most_common_elem_freqs => %L::real[], "
+			"elem_count_histogram => %L::real[], "
+			"range_length_histogram => %L::text, "
+			"range_empty_frac => %L::real, "
+			"range_bounds_histogram => %L::text)', "
+			"coalesce($2, format('%I.%I', a.nspname, a.relname)), "
+			"a.attname, a.inherited, a.null_frac, a.avg_width, "
+			"a.n_distinct, a.most_common_vals, a.most_common_freqs, "
+			"a.histogram_bounds, a.correlation, "
+			"a.most_common_elems, a.most_common_elem_freqs, "
+			"a.elem_count_histogram, a.range_length_histogram, "
+			"a.range_empty_frac, a.range_bounds_histogram ) "
+			"FROM a "
+		") "
+		"SELECT 'SELECT ' || string_agg(s.sql, ', ' ORDER BY s.ord) "
+		"FROM s ");
+
+	s = strdup(sql->data);
+	destroyPQExpBuffer(sql);
+	return s;
+}
diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 9ef2f2017e..1db5cf52eb 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -112,6 +112,7 @@ typedef struct _restoreOptions
 	int			no_publications;	/* Skip publication entries */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
@@ -179,6 +180,7 @@ typedef struct _dumpOptions
 	int			no_security_labels;
 	int			no_publications;
 	int			no_subscriptions;
+	int			no_statistics;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index d97ebaff5b..d5f61399d9 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2833,6 +2833,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's a stats dump, maybe ignore it */
+	if (ropt->no_statistics && strcmp(te->desc, "STATISTICS") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2862,6 +2866,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index d275b31605..e368614745 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -59,6 +59,7 @@
 #include "compress_io.h"
 #include "dumputils.h"
 #include "fe_utils/option_utils.h"
+#include "fe_utils/stats_export.h"
 #include "fe_utils/string_utils.h"
 #include "filter.h"
 #include "getopt_long.h"
@@ -428,6 +429,7 @@ main(int argc, char **argv)
 		{"no-comments", no_argument, &dopt.no_comments, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -1144,6 +1146,7 @@ help(const char *progname)
 	printf(_("  --no-comments                do not dump comments\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -7001,6 +7004,7 @@ getTables(Archive *fout, int *numTables)
 
 		/* Tables have data */
 		tblinfo[i].dobj.components |= DUMP_COMPONENT_DATA;
+		tblinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
 
 		/* Mark whether table has an ACL */
 		if (!PQgetisnull(res, i, i_relacl))
@@ -7498,6 +7502,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -10247,6 +10252,82 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const DumpableObject *dobj,
+				  const char *reltypename, DumpId dumpid)
+{
+	const char *stmtname = "relstats";
+	static bool prepared = false;
+	const char *values[2];
+	PGconn     *conn;
+	PGresult   *res;
+	PQExpBuffer query;
+	PQExpBuffer tag;
+
+	/* do nothing, if --no-statistics is supplied */
+	if (fout->dopt->no_statistics)
+		return;
+
+	conn = GetConnection(fout);
+
+	if (!prepared)
+	{
+		int		ver = PQserverVersion(conn);
+		char   *sql = exportRelationStatsSQL(ver);
+
+		if (sql == NULL)
+			pg_fatal("could not prepare stats export query for server version %d",
+					 ver);
+
+		res = PQprepare(conn, stmtname, sql, 2, NULL);
+		if (res == NULL || PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("prepared statement failed: %s",
+					 PQerrorMessage(conn));
+
+		free(sql);
+		prepared = true;
+	}
+
+	values[0] = fmtQualifiedId(dobj->namespace->dobj.name, dobj->name);
+	values[1] = NULL;
+	res = PQexecPrepared(conn, stmtname, 2, values, NULL, NULL, 0);
+
+
+	/* Result set must be 1x1 */
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		pg_fatal("error in statistics extraction: %s", PQerrorMessage(conn));
+
+	if (PQntuples(res) != 1)
+		pg_fatal("statistics extraction expected one row, but got %d rows", 
+				 PQntuples(res));
+
+	query = createPQExpBuffer();
+	appendPQExpBufferStr(query, strdup(PQgetvalue(res, 0, 0)));
+	appendPQExpBufferStr(query, ";\n");
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", reltypename,
+					  fmtId(dobj->name));
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATS IMPORT",
+							  .section = SECTION_POST_DATA,
+							  .createStmt = query->data,
+							  .deps = &dumpid,
+							  .nDeps = 1));
+
+	PQclear(res);
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -16680,6 +16761,13 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 	if (tbinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
 		dumpTableSecLabel(fout, tbinfo, reltypename);
 
+	/* Statistics are dependent on the definition, not the data */
+	/* Views don't have stats */
+	if ((tbinfo->dobj.dump & DUMP_COMPONENT_STATISTICS) &&
+		(tbinfo->relkind == RELKIND_VIEW))
+		dumpRelationStats(fout, &tbinfo->dobj, reltypename,
+						  tbinfo->dobj.dumpId);
+
 	/* Dump comments on inlined table constraints */
 	for (j = 0; j < tbinfo->ncheck; j++)
 	{
@@ -16881,6 +16969,7 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 	PQExpBuffer delq;
 	char	   *qindxname;
 	char	   *qqindxname;
+	DumpId		dumpid;
 
 	/* Do nothing in data-only dump */
 	if (dopt->dataOnly)
@@ -16993,14 +17082,21 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 		free(indstatvalsarray);
 	}
 
+	/* Comments and stats share same .dep */
+	dumpid = is_constraint ? indxinfo->indexconstraint :
+							 indxinfo->dobj.dumpId;
+
 	/* Dump Index Comments */
 	if (indxinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
 		dumpComment(fout, "INDEX", qindxname,
 					tbinfo->dobj.namespace->dobj.name,
 					tbinfo->rolname,
 					indxinfo->dobj.catId, 0,
-					is_constraint ? indxinfo->indexconstraint :
-					indxinfo->dobj.dumpId);
+					dumpid);
+
+	/* Dump Index Stats */
+	if (indxinfo->dobj.dump & DUMP_COMPONENT_STATISTICS)
+		dumpRelationStats(fout, &indxinfo->dobj, "INDEX", dumpid);
 
 	destroyPQExpBuffer(q);
 	destroyPQExpBuffer(delq);
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 9bc93520b4..d6a071ec28 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -101,6 +101,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 046c0dc3b3..69652aa205 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -105,6 +105,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -174,6 +175,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -453,6 +455,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -668,6 +672,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index c3beacdec1..2d326dec72 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -75,6 +75,7 @@ main(int argc, char **argv)
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
 	static int	no_subscriptions = 0;
+	static int  no_statistics = 0;
 	static int	strict_names = 0;
 
 	struct option cmdopts[] = {
@@ -126,6 +127,7 @@ main(int argc, char **argv)
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
+		{"no-statistics", no_argument, &no_statistics, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -358,6 +360,7 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
-- 
2.44.0

#76Ashutosh Bapat
ashutosh.bapat.oss@gmail.com
In reply to: Corey Huinker (#74)
Re: Statistics Import and Export

Hi Corey,

On Sat, Mar 23, 2024 at 7:21 AM Corey Huinker <corey.huinker@gmail.com>
wrote:

v12 attached.

0001 -

Some random comments

+SELECT
+    format('SELECT pg_catalog.pg_set_attribute_stats( '
+            || 'relation => %L::regclass::oid, attname => %L::name, '
+            || 'inherited => %L::boolean, null_frac => %L::real, '
+            || 'avg_width => %L::integer, n_distinct => %L::real, '
+            || 'most_common_vals => %L::text, '
+            || 'most_common_freqs => %L::real[], '
+            || 'histogram_bounds => %L::text, '
+            || 'correlation => %L::real, '
+            || 'most_common_elems => %L::text, '
+            || 'most_common_elem_freqs => %L::real[], '
+            || 'elem_count_histogram => %L::real[], '
+            || 'range_length_histogram => %L::text, '
+            || 'range_empty_frac => %L::real, '
+            || 'range_bounds_histogram => %L::text) ',
+        'stats_export_import.' || s.tablename || '_clone', s.attname,
+        s.inherited, s.null_frac,
+        s.avg_width, s.n_distinct,
+        s.most_common_vals, s.most_common_freqs, s.histogram_bounds,
+        s.correlation, s.most_common_elems, s.most_common_elem_freqs,
+        s.elem_count_histogram, s.range_length_histogram,
+        s.range_empty_frac, s.range_bounds_histogram)
+FROM pg_catalog.pg_stats AS s
+WHERE s.schemaname = 'stats_export_import'
+AND s.tablename IN ('test', 'is_odd')
+\gexec

Why do we need to construct the command and execute? Can we instead execute
the function directly? That would also avoid ECHO magic.

+   <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>

COMMENT: The functions throw many validation errors. Do we want to list the
acceptable/unacceptable input values in the documentation corresponding to
those? I don't expect one line per argument validation. Something like
"these, these and these arguments can not be NULL" or "both arguments in
each of the pairs x and y, a and b, and c and d should be non-NULL or NULL
respectively".

The functions pg_set_relation_stats() and pg_set_attribute_stats() now
return void. There just weren't enough conditions where a condition was
considered recoverable to justify having it. This may mean that combining
multiple pg_set_attribute_stats calls into one compound statement may no
longer be desirable, but that's just one of the places where I'd like
feedback on how pg_dump/pg_restore use these functions.

0002 -

This patch concerns invoking the functions in 0001 via
pg_restore/pg_upgrade. Little has changed here. Dumping statistics is
currently the default for pg_dump/pg_restore/pg_upgrade, and can be
switched off with the switch --no-statistics. Some have expressed concern
about whether stats dumping should be the default. I have a slight
preference for making it the default, for the following reasons:

+ /* Statistics are dependent on the definition, not the data */
+ /* Views don't have stats */
+ if ((tbinfo->dobj.dump & DUMP_COMPONENT_STATISTICS) &&
+ (tbinfo->relkind == RELKIND_VIEW))
+ dumpRelationStats(fout, &tbinfo->dobj, reltypename,
+  tbinfo->dobj.dumpId);
+

Statistics are about data. Whenever pg_dump dumps some filtered data, the
statistics collected for the whole table are uselss. We should avoide
dumping
statistics in such a case. E.g. when only schema is dumped what good is
statistics? Similarly the statistics on a partitioned table may not be
useful
if some its partitions are not dumped. Said that dumping statistics on
foreign
table makes sense since they do not contain data but the statistics still
makes sense.

Key areas where I'm seeking feedback:

- What level of errors in a restore will a user tolerate, and what should
be done to the error messages to indicate that the data itself is fine, but
a manual operation to update stats on that particular table is now
warranted?
- To what degree could pg_restore/pg_upgrade take that recovery action
automatically?
- Should the individual attribute/class set function calls be grouped by
relation, so that they all succeed/fail together, or should they be called
separately, each able to succeed or fail on their own?
- Any other concerns about how to best use these new functions.

Whether or not I pass --no-statistics, there is no difference in the dump
output. Am I missing something?
$ pg_dump -d postgres > /tmp/dump_no_arguments.out
$ pg_dump -d postgres --no-statistics > /tmp/dump_no_statistics.out
$ diff /tmp/dump_no_arguments.out /tmp/dump_no_statistics.out
$

IIUC, pg_dump includes statistics by default. That means all our pg_dump
related tests will have statistics output by default. That's good since the
functionality will always be tested. 1. We need additional tests to ensure
that the statistics is installed after restore. 2. Some of those tests
compare dumps before and after restore. In case the statistics is changed
because of auto-analyze happening post-restore, these tests will fail.

I believe, in order to import statistics through IMPORT FOREIGN SCHEMA,
postgresImportForeignSchema() will need to add SELECT commands invoking
pg_set_relation_stats() on each imported table and pg_set_attribute_stats()
on each of its attribute. Am I right? Do we want to make that happen in the
first cut of the feature? How do you expect these functions to be used to
update statistics of foreign tables?

--
Best Wishes,
Ashutosh Bapat

#77Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Corey Huinker (#75)
4 attachment(s)
Re: Statistics Import and Export

On 3/25/24 09:27, Corey Huinker wrote:

On Fri, Mar 22, 2024 at 9:51 PM Corey Huinker <corey.huinker@gmail.com>
wrote:

v12 attached.

v13 attached. All the same features as v12, but with a lot more type
checking, bounds checking, value inspection, etc. Perhaps the most notable
feature is that we're now ensuring that histogram values are in ascending
order. This could come in handy for detecting when we are applying stats to
a column of the wrong type, or the right type but with a different
collation. It's not a guarantee of validity, of course, but it would detect
egregious changes in sort order.

Hi,

I did take a closer look at v13 today. I have a bunch of comments and
some minor whitespace fixes in the attached review patches.

0001
----

1) The docs say this:

<para>
The purpose of this function is to apply statistics values in an
upgrade situation that are "good enough" for system operation until
they are replaced by the next <command>ANALYZE</command>, usually via
<command>autovacuum</command> This function is used by
<command>pg_upgrade</command> and <command>pg_restore</command> to
convey the statistics from the old system version into the new one.
</para>

I find this a bit confusing, considering the pg_dump/pg_restore changes
are only in 0002, not in this patch.

2) Also, I'm not sure about this:

<parameter>relation</parameter>, the parameters in this are all
derived from <structname>pg_stats</structname>, and the values
given are most often extracted from there.

How do we know where do the values come from "most often"? I mean, where
else would it come from?

3) The function pg_set_attribute_stats() is veeeeery long - 1000 lines
or so, that's way too many for me to think about. I agree the flow is
pretty simple, but I still wonder if there's a way to maybe split it
into some smaller "meaningful" steps.

4) It took me *ages* to realize the enums at the beginning of some of
the functions are actually indexes of arguments in PG_FUNCTION_ARGS.
That'd surely deserve a comment explaining this.

5) The comment for param_names in pg_set_attribute_stats says this:

/* names of columns that cannot be null */
const char *param_names[] = { ... }

but isn't that actually incorrect? I think that applies only to a couple
initial arguments, but then other fields (MCV, mcelem stats, ...) can be
NULL, right?

6) There's a couple minor whitespace fixes or comments etc.

0002
----

1) I don't understand why we have exportExtStatsSupported(). Seems
pointless - nothing calls it, even if it did we don't know how to export
the stats.

2) I think this condition in dumpTableSchema() is actually incorrect:

if ((tbinfo->dobj.dump & DUMP_COMPONENT_STATISTICS) &&
(tbinfo->relkind == RELKIND_VIEW))
dumpRelationStats(fout, &tbinfo->dobj, reltypename,

Aren't indexes pretty much exactly the thing for which we don't want to
dump statistics? In fact this skips dumping statistics for table - if
you dump a database with a single table (-Fc), pg_restore -l will tell
you this:

217; 1259 16385 TABLE public t user
3403; 0 16385 TABLE DATA public t user

Which is not surprising, because table is not a view. With an expression
index you get this:

217; 1259 16385 TABLE public t user
3404; 0 16385 TABLE DATA public t user
3258; 1259 16418 INDEX public t_expr_idx user
3411; 0 0 STATS IMPORT public INDEX t_expr_idx

Unfortunately, fixing the condition does not work:

$ pg_dump -Fc test > test.dump
pg_dump: warning: archive items not in correct section order

This happens for a very simple reason - the statistics are marked as
SECTION_POST_DATA, which for the index works, because indexes are in
post-data section. But the table stats are dumped right after data,
still in the "data" section.

IMO that's wrong, the statistics should be delayed to the post-data
section. Which probably means there needs to be a separate dumpable
object for statistics on table/index, with a dependency on the object.

3) I don't like the "STATS IMPORT" description. For extended statistics
we dump the definition as "STATISTICS" so why to shorten it to "STATS"
here? And "IMPORT" seems more like the process of loading data, not the
data itself. So I suggest "STATISTICS DATA".

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

Attachments:

0001-Create-pg_set_relation_stats-pg_set_attribute_stats.patchtext/x-patch; charset=UTF-8; name=0001-Create-pg_set_relation_stats-pg_set_attribute_stats.patchDownload
From 686f748f3f92d3f9dc77b74ff1c35fed758e0dd8 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 21 Mar 2024 18:04:54 -0400
Subject: [PATCH 1/4] Create pg_set_relation_stats, pg_set_attribute_stats.

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

Both functions take an oid to identify the target relation that will
receive the statistics. There is nothing requiring that relation to be
the same one as the one exported, though the statistics have to make
sense in the context of the new relation.

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

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

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

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

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

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 8ecc02f2b90..72711d86d37 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29090,6 +29090,116 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
     in identifying the specific disk files associated with database objects.
    </para>
 
+   <table id="functions-admin-statsimport">
+    <title>Database Object Statistics Import Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_relation_stats</primary>
+        </indexterm>
+        <function>pg_set_relation_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>relpages</parameter> <type>integer</type>,
+         <parameter>reltuples</parameter> <type>real</type>,
+         <parameter>relallvisible</parameter> <type>integer</type> )
+        <returnvalue>void</returnvalue>
+       </para>
+       <para>
+        Updates the <structname>pg_class</structname> row for the specified
+        <parameter>relation</parameter>, setting the values for the columns
+        <structfield>reltuples</structfield>,
+        <structfield>relpages</structfield>, and
+        <structfield>relallvisible</structfield>.
+        To avoid table bloat in <structname>pg_class</structname>, this change
+        is made with an in-place update, and therefore cannot be rolled back through
+        normal transaction processing.
+       </para>
+       <para>
+        The purpose of this function is to apply statistics values in an
+        upgrade situation that are "good enough" for system operation until
+        they are replaced by the next <command>ANALYZE</command>, usually via
+        <command>autovacuum</command> This function is used by
+        <command>pg_upgrade</command> and <command>pg_restore</command> to
+        convey the statistics from the old system version into the new one.
+       </para>
+       <para>
+        The caller must either be the owner of the relation, or have superuser
+        privileges.
+       </para>
+       </entry>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_attribute_stats</primary>
+        </indexterm>
+        <function>pg_set_attribute_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>
+         , <parameter>attname</parameter> <type>name</type>
+         , <parameter>inherited</parameter> <type>boolean</type>
+         , <parameter>null_frac</parameter> <type>real</type>
+         , <parameter>avg_width</parameter> <type>integer</type>
+         , <parameter>n_distinct</parameter> <type>real</type>
+         [, <parameter>most_common_vals</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_freqs</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>histogram_bounds</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>correlation</parameter> <type>real</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_elems</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_elem_freqs</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>elem_count_histogram</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_length_histogram</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_empty_frac</parameter> <type>real</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_bounds_histogram</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ] )
+        <returnvalue>void</returnvalue>
+       </para>
+       <para>
+        Replaces the <structname>pg_statistic</structname> row for the
+        <structname>pg_attribute</structname> row specified by
+        <parameter>relation</parameter>, <parameter>attname</parameter>
+        and <parameter>inherited</parameter>.  Aside from
+        <parameter>relation</parameter>, the parameters in this are all
+        derived from <structname>pg_stats</structname>, and the values
+        given are most often extracted from there.
+       </para>
+       <para>
+        The purpose of this function is to apply statistics values in an
+        upgrade situation that are "good enough" for system operation until
+        they are replaced by the next <command>ANALYZE</command>, usually via
+        <command>autovacuum</command> This function is used by
+        <command>pg_upgrade</command> and <command>pg_restore</command> to
+        convey the statistics from the old system version into the new one.
+       </para>
+       <para>
+        The caller must either be the owner of the relation, or have superuser
+        privileges.
+       </para>
+       </entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
    <table id="functions-admin-dblocation">
     <title>Database Object Location Functions</title>
     <tgroup cols="1">
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index fe2bb50f46d..22be7e66535 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -636,6 +636,24 @@ LANGUAGE INTERNAL
 CALLED ON NULL INPUT VOLATILE PARALLEL SAFE
 AS 'pg_stat_reset_slru';
 
+CREATE OR REPLACE FUNCTION
+  pg_set_attribute_stats(relation oid, attname name, inherited bool,
+                         null_frac real, avg_width integer, n_distinct real,
+                         most_common_vals text DEFAULT NULL,
+                         most_common_freqs real[] DEFAULT NULL,
+                         histogram_bounds text DEFAULT NULL,
+                         correlation real DEFAULT NULL,
+                         most_common_elems text DEFAULT NULL,
+                         most_common_elem_freqs real[] DEFAULT NULL,
+                         elem_count_histogram real[] DEFAULT NULL,
+                         range_length_histogram text DEFAULT NULL,
+                         range_empty_frac real DEFAULT NULL,
+                         range_bounds_histogram text DEFAULT NULL)
+RETURNS void
+LANGUAGE INTERNAL
+CALLED ON NULL INPUT VOLATILE
+AS 'pg_set_attribute_stats';
+
 --
 -- The default permissions for functions mean that anyone can execute them.
 -- A number of functions shouldn't be executable by just anyone, but rather
diff --git a/src/backend/statistics/Makefile b/src/backend/statistics/Makefile
index 89cf8c27973..e4f8ab7c4f5 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 73b29a3d50a..331e82c776b 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 00000000000..3987425ea4d
--- /dev/null
+++ b/src/backend/statistics/statistics.c
@@ -0,0 +1,1108 @@
+/*-------------------------------------------------------------------------
+ * statistics.c
+ *
+ *	  POSTGRES statistics import
+ *
+ * Code supporting the direct importation of relation statistics, similar to
+ * what is done by the ANALYZE command.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *       src/backend/statistics/statistics.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "catalog/pg_database.h"
+#include "catalog/pg_operator.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_type.h"
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "nodes/nodeFuncs.h"
+#include "statistics/statistics.h"
+#include "utils/acl.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/float.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/rangetypes.h"
+#include "utils/syscache.h"
+#include "utils/typcache.h"
+
+/*
+ * A role has privileges to vacuum or analyze the relation if any of the
+ * following are true:
+ *   - the role owns the current database and the relation is not shared
+ *   - the role has the MAINTAIN privilege on the relation
+ *
+ */
+static bool
+canModifyRelation(Oid relid, Form_pg_class reltuple)
+{
+	return ((object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId())
+			 && !reltuple->relisshared) ||
+			pg_class_aclcheck(relid, GetUserId(), ACL_MAINTAIN) == ACLCHECK_OK);
+}
+
+/*
+ * Set statistics for a given pg_class entry.
+ *
+ * This does an in-place (i.e. non-transactional) update of pg_class, just as
+ * is done in ANALYZE.
+ *
+ */
+Datum
+pg_set_relation_stats(PG_FUNCTION_ARGS)
+{
+	enum
+	{
+		P_RELATION = 0,			/* oid */
+		P_RELPAGES,				/* int */
+		P_RELTUPLES,			/* float4 */
+		P_RELALLVISIBLE,		/* int */
+		P_NUM_PARAMS
+	};
+
+	const char *param_names[] = {
+		"relation",
+		"relpages",
+		"reltuples",
+		"relallvisible"
+	};
+
+	Oid			relid;
+	Relation	rel;
+	HeapTuple	ctup;
+	Form_pg_class pgcform;
+	float4		reltuples;
+	int			relpages;
+	int			relallvisible;
+
+	/* Any NULL parameter is an error */
+	for (int i = P_RELATION; i < P_NUM_PARAMS; i++)
+		if (PG_ARGISNULL(i))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be NULL", param_names[i])));
+
+	relid = PG_GETARG_OID(P_RELATION);
+
+	/*
+	 * Open the relation, getting ShareUpdateExclusiveLock to ensure that no
+	 * other stat-setting operation can run on it concurrently.
+	 */
+	rel = table_open(RelationRelationId, ShareUpdateExclusiveLock);
+
+	ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(ctup))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_IN_USE),
+				 errmsg("pg_class entry for relid %u not found", relid)));
+
+	pgcform = (Form_pg_class) GETSTRUCT(ctup);
+
+	if (!canModifyRelation(relid, pgcform))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						RelationGetRelationName(rel))));
+
+
+	relpages = PG_GETARG_INT32(P_RELPAGES);
+	reltuples = PG_GETARG_FLOAT4(P_RELTUPLES);
+	relallvisible = PG_GETARG_INT32(P_RELALLVISIBLE);
+
+	/* Only update pg_class if there is a meaningful change */
+	if ((pgcform->reltuples != reltuples)
+		|| (pgcform->relpages != relpages)
+		|| (pgcform->relallvisible != relallvisible))
+	{
+		pgcform->relpages = PG_GETARG_INT32(P_RELPAGES);
+		pgcform->reltuples = PG_GETARG_FLOAT4(P_RELTUPLES);
+		pgcform->relallvisible = PG_GETARG_INT32(P_RELALLVISIBLE);
+
+		heap_inplace_update(rel, ctup);
+	}
+
+	table_close(rel, NoLock);
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Perform the cast of text to some array type
+ */
+static Datum
+cast_stavalue(FmgrInfo *finfo, Datum d, Oid typid, int32 typmod)
+{
+	char	   *s = TextDatumGetCString(d);
+	Datum		out = FunctionCall3(finfo, CStringGetDatum(s),
+									ObjectIdGetDatum(typid),
+									Int32GetDatum(typmod));
+
+	pfree(s);
+
+	return out;
+}
+
+/*
+ * Convenience routine to handle a common pattern where two function
+ * parameters must either both be NULL or both NOT NULL.
+ */
+static bool
+has_arg_pair(FunctionCallInfo fcinfo, const char **pnames, int p1, int p2)
+{
+	/* if on param is NULL and the other NOT NULL, report an error */
+	if (PG_ARGISNULL(p1) != PG_ARGISNULL(p2))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL when %s is NOT NULL",
+						pnames[(PG_ARGISNULL(p1)) ? p1 : p2],
+						pnames[(PG_ARGISNULL(p1)) ? p2 : p1])));
+
+	return (!PG_ARGISNULL(p1));
+}
+
+/*
+ * Test if the type is a scalar for MCELM purposes
+ */
+static bool
+type_is_scalar(Oid typid)
+{
+	HeapTuple	tp;
+	bool		result = false;
+
+	tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+	if (HeapTupleIsValid(tp))
+	{
+		Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+
+		result = (!OidIsValid(typtup->typanalyze));
+		ReleaseSysCache(tp);
+	}
+	return result;
+}
+
+static int
+value_array_len(ExpandedArrayHeader *arr, const char *name)
+{
+	if (arr->ndims != 1)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be a multidimensional array", name)));
+
+	return arr->dims[0];
+}
+
+/*
+ * Convenience routine to encapsulate all of the steps needed for any
+ * value array.
+ */
+static int
+value_not_null_array_len(ExpandedArrayHeader *arr, const char *name)
+{
+	const int	nelems = value_array_len(arr, name);
+
+	if (nelems > 0)
+	{
+		deconstruct_expanded_array(arr);
+
+		/* if there's a nulls array, all values must be false */
+		if (arr->dnulls != NULL)
+			for (int i = 0; i < nelems; i++)
+				if (arr->dnulls[i])
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("%s array cannot contain NULL values", name)));
+	}
+
+	return nelems;
+}
+
+/*
+ * Import statistics for a given relation attribute.
+ *
+ * This will insert/replace a row in pg_statistic for the given relation and
+ * attribute name.
+ *
+ * The function takes input parameters that correspond to columns in the view
+ * pg_stats.
+ *
+ * Of those, the columns attname, inherited, null_frac, avg_width, and
+ * n_distinct all correspond to NOT NULL columns in pg_statistic. These
+ * parameters have no default value and passing NULL to them will result
+ * in an error.
+ *
+ * If there is no attribute with a matching attname in the relation, the
+ * function will raise an error. Likewise for setting inherited statistics
+ * on a table that is not partitioned.
+ *
+ * The remaining parameters all belong to a specific stakind. Some stakinds
+ * have multiple parameters, and in those cases both parameters must be
+ * NOT NULL or both NULL, otherwise an error will be raised.
+ *
+ * Omitting a parameter or explicitly passing NULL means that that particular
+ * stakind is not associated with the attribute.
+ *
+ * Parameters that are NOT NULL will be inspected for consistency checks,
+ * any of which can raise an error.
+ *
+ * Parameters corresponding to ANYARRAY columns are instead passed in as text
+ * values, which is a valid input string for an array of the type or basetype
+ * of the attribute. Any error generated by the array_in() function will in
+ * turn fail the function.
+ */
+Datum
+pg_set_attribute_stats(PG_FUNCTION_ARGS)
+{
+	enum
+	{
+		P_RELATION = 0,			/* oid */
+		P_ATTNAME,				/* name */
+		P_INHERITED,			/* bool */
+		P_NULL_FRAC,			/* float4 */
+		P_AVG_WIDTH,			/* int32 */
+		P_N_DISTINCT,			/* float4 */
+		P_MC_VALS,				/* text, null */
+		P_MC_FREQS,				/* float4[], null */
+		P_HIST_BOUNDS,			/* text, null */
+		P_CORRELATION,			/* float4, null */
+		P_MC_ELEMS,				/* text, null */
+		P_MC_ELEM_FREQS,		/* float4[], null */
+		P_ELEM_COUNT_HIST,		/* float4[], null */
+		P_RANGE_LENGTH_HIST,	/* text, null */
+		P_RANGE_EMPTY_FRAC,		/* float4, null */
+		P_RANGE_BOUNDS_HIST,	/* text, null */
+		P_NUM_PARAMS
+	};
+
+	/* names of columns that cannot be null */
+	const char *param_names[] = {
+		"relation",
+		"attname",
+		"inherited",
+		"null_frac",
+		"avg_width",
+		"n_distinct",
+		"most_common_vals",
+		"most_common_freqs",
+		"histogram_bounds",
+		"correlation",
+		"most_common_elems",
+		"most_common_elem_freqs",
+		"elem_count_histogram",
+		"range_length_histogram",
+		"range_empty_frac",
+		"range_bounds_histogram"
+	};
+
+	Oid			relid;
+	Name		attname;
+	bool		inherited;
+	Relation	rel;
+	HeapTuple	ctup;
+	HeapTuple	atup;
+	Form_pg_class pgcform;
+
+	TypeCacheEntry *typcache;
+	const int	operator_flags = TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR;
+
+	Oid			typid;
+	int32		typmod;
+	Oid			typcoll;
+	Oid			eqopr;
+	Oid			ltopr;
+	Oid			basetypid;
+	Oid			baseeqopr;
+	Oid			baseltopr;
+
+	const float4 frac_min = 0.0;
+	const float4 frac_max = 1.0;
+	float4		null_frac;
+	const int	avg_width_min = 0;
+	int			avg_width;
+	const float4 n_distinct_min = -1.0;
+	float4		n_distinct;
+
+	Datum		values[Natts_pg_statistic] = {0};
+	bool		nulls[Natts_pg_statistic] = {false};
+
+	Relation	sd;
+	HeapTuple	oldtup;
+	CatalogIndexState indstate;
+	HeapTuple	stup;
+	Form_pg_attribute attr;
+
+	FmgrInfo	finfo;
+
+	bool		has_mcv;
+	bool		has_mc_elems;
+	bool		has_rl_hist;
+	int			stakind_count;
+
+	int			k = 0;
+
+	/*
+	 * A null in a required parameter is an error.
+	 */
+	for (int i = P_RELATION; i <= P_N_DISTINCT; i++)
+		if (PG_ARGISNULL(i))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be NULL", param_names[i])));
+
+	/*
+	 * Check all parameter pairs up front.
+	 */
+	has_mcv = has_arg_pair(fcinfo, param_names,
+						   P_MC_VALS, P_MC_FREQS);
+	has_mc_elems = has_arg_pair(fcinfo, param_names,
+								P_MC_ELEMS, P_MC_ELEM_FREQS);
+	has_rl_hist = has_arg_pair(fcinfo, param_names,
+							   P_RANGE_LENGTH_HIST, P_RANGE_EMPTY_FRAC);
+
+	/*
+	 * If a caller specifies more stakind-stats than we have slots to store
+	 * them, raise an error.
+	 */
+	stakind_count = (int) has_mcv + (int) has_mc_elems + (int) has_rl_hist +
+		(int) !PG_ARGISNULL(P_HIST_BOUNDS) +
+		(int) !PG_ARGISNULL(P_CORRELATION) +
+		(int) !PG_ARGISNULL(P_ELEM_COUNT_HIST) +
+		(int) !PG_ARGISNULL(P_RANGE_BOUNDS_HIST);
+
+	if (stakind_count > STATISTIC_NUM_SLOTS)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("imported statistics must have a maximum of %d slots "
+						"but %d given",
+						STATISTIC_NUM_SLOTS, stakind_count)));
+
+	relid = PG_GETARG_OID(P_RELATION);
+
+	rel = relation_open(relid, ShareUpdateExclusiveLock);
+
+	/* Test existence of Relation */
+	ctup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+
+	if (!HeapTupleIsValid(ctup))
+		elog(ERROR, "cache lookup failed for relation %u", relid);
+
+	pgcform = (Form_pg_class) GETSTRUCT(ctup);
+
+	if (!canModifyRelation(relid, pgcform))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						RelationGetRelationName(rel))));
+
+	ReleaseSysCache(ctup);
+
+	/*
+	 * Test existence of attribute
+	 */
+	attname = PG_GETARG_NAME(P_ATTNAME);
+	atup = SearchSysCache2(ATTNAME,
+						   ObjectIdGetDatum(relid),
+						   NameGetDatum(attname));
+
+	/* Attribute not found nowhere to import the stats to */
+	if (!HeapTupleIsValid(atup))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s has no attname %s",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+
+	attr = (Form_pg_attribute) GETSTRUCT(atup);
+	if (attr->attisdropped)
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s attname %s is dropped",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+
+	/* Test inherited */
+	inherited = PG_GETARG_BOOL(P_INHERITED);
+	if (inherited &&
+		(rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE) &&
+		(rel->rd_rel->relkind != RELKIND_PARTITIONED_INDEX))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s is not partitioned, cannot accept inherited stats",
+						RelationGetRelationName(rel))));
+
+	/*
+	 * Fetch datatype information, this is needed to derive the proper staopN
+	 * and stacollN values.
+	 *
+	 * If this relation is an index and that index has expressions in it, and
+	 * the attnum specified is known to be an expression, then we must walk
+	 * the list attributes up to the specified attnum to get the right
+	 * expression.
+	 */
+	if ((rel->rd_rel->relkind == RELKIND_INDEX
+		 || (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+		&& (rel->rd_indexprs != NIL)
+		&& (rel->rd_index->indkey.values[attr->attnum - 1] == 0))
+	{
+		ListCell   *indexpr_item = list_head(rel->rd_indexprs);
+		Node	   *expr;
+
+		for (int i = 0; i < attr->attnum - 1; i++)
+			if (rel->rd_index->indkey.values[i] == 0)
+				indexpr_item = lnext(rel->rd_indexprs, indexpr_item);
+
+		if (indexpr_item == NULL)	/* shouldn't happen */
+			elog(ERROR, "too few entries in indexprs list");
+
+		expr = (Node *) lfirst(indexpr_item);
+
+		typid = exprType(expr);
+		typmod = exprTypmod(expr);
+
+		/*
+		 * If a collation has been specified for the index column, use that in
+		 * preference to anything else; but if not, fall back to whatever we
+		 * can get from the expression.
+		 */
+		if (OidIsValid(attr->attcollation))
+			typcoll = attr->attcollation;
+		else
+			typcoll = exprCollation(expr);
+	}
+	else
+	{
+		typid = attr->atttypid;
+		typmod = attr->atttypmod;
+		typcoll = attr->attcollation;
+	}
+
+	/* if it's a multirange, step down to the range type */
+	if (type_is_multirange(typid))
+		typid = get_multirange_range(typid);
+
+	typcache = lookup_type_cache(typid, operator_flags);
+	ltopr = typcache->lt_opr;
+	eqopr = typcache->eq_opr;
+
+	/*
+	 * if it's a range type, swap the subtype for the base type, otherwise get
+	 * the base element type
+	 */
+	if (type_is_range(typid))
+		basetypid = get_range_subtype(typid);
+	else
+		basetypid = get_base_element_type(typid);
+
+	if (basetypid == InvalidOid)
+	{
+		/* type is its own base type */
+		basetypid = typid;
+		baseltopr = ltopr;
+		baseeqopr = eqopr;
+	}
+	else
+	{
+		TypeCacheEntry *bentry = lookup_type_cache(basetypid, operator_flags);
+
+		baseltopr = bentry->lt_opr;
+		baseeqopr = bentry->eq_opr;
+	}
+
+	/* P_HIST_BOUNDS and P_CORRELATION must have a < operator */
+	if (baseltopr == InvalidOid)
+		for (int i = P_HIST_BOUNDS; i <= P_CORRELATION; i++)
+			if (!PG_ARGISNULL(i))
+				ereport(ERROR,
+						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+						 errmsg("Relation %s attname %s cannot "
+								"have stats of type %s",
+								RelationGetRelationName(rel),
+								NameStr(*attname),
+								param_names[i])));
+
+	/* Scalar types can't have P_MC_ELEMS, P_MC_ELEM_FREQS, P_ELEM_COUNT_HIST */
+	/* TODO any other types we can exclude? */
+	if (type_is_scalar(typid))
+		for (int i = P_MC_ELEMS; i <= P_ELEM_COUNT_HIST; i++)
+			if (!PG_ARGISNULL(i))
+				ereport(ERROR,
+						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+						 errmsg("Relation %s attname %s is a scalar type, "
+								"cannot have stats of type %s",
+								RelationGetRelationName(rel),
+								NameStr(*attname),
+								param_names[i])));
+
+	/* Only range types can have P_RANGE_x */
+	if ((!type_is_range(typid)) && (!type_is_multirange(typid)))
+		for (int i = P_RANGE_LENGTH_HIST; i <= P_RANGE_BOUNDS_HIST; i++)
+			if (!PG_ARGISNULL(i))
+				ereport(ERROR,
+						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+						 errmsg("Relation %s attname %s is not a range type, "
+								"cannot have stats of type %s",
+								RelationGetRelationName(rel),
+								NameStr(*attname),
+								param_names[i])));
+
+	/*
+	 * Statistical parameters that must pass data validity tests
+	 */
+	null_frac = PG_GETARG_FLOAT4(P_NULL_FRAC);
+	if ((null_frac < frac_min) || (null_frac > frac_max))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s %f is out of range %.1f to %.1f",
+						param_names[P_NULL_FRAC], null_frac,
+						frac_min, frac_max)));
+
+	avg_width = PG_GETARG_INT32(P_AVG_WIDTH);
+	if (avg_width < avg_width_min)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s %d must be >= %d",
+						param_names[P_AVG_WIDTH], avg_width, avg_width_min)));
+
+	n_distinct = PG_GETARG_FLOAT4(P_N_DISTINCT);
+	if (n_distinct < n_distinct_min)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s %f must be >= %.1f",
+						param_names[P_N_DISTINCT], n_distinct,
+						n_distinct_min)));
+
+	values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(relid);
+	values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(attr->attnum);
+	values[Anum_pg_statistic_stainherit - 1] = PG_GETARG_DATUM(P_INHERITED);
+	values[Anum_pg_statistic_stanullfrac - 1] = PG_GETARG_DATUM(P_NULL_FRAC);
+	values[Anum_pg_statistic_stawidth - 1] = PG_GETARG_DATUM(P_AVG_WIDTH);
+	values[Anum_pg_statistic_stadistinct - 1] = PG_GETARG_DATUM(P_N_DISTINCT);
+
+	fmgr_info(F_ARRAY_IN, &finfo);
+
+	/* MC_VALS && MC_FREQS => STATISTIC_KIND_MCV */
+	if (has_mcv)
+	{
+		const char *freqsname = param_names[P_MC_FREQS];
+		const char *valsname = param_names[P_MC_VALS];
+		Datum		freqs = PG_GETARG_DATUM(P_MC_FREQS);
+		Datum		vals = cast_stavalue(&finfo, PG_GETARG_DATUM(P_MC_VALS),
+										 basetypid, typmod);
+
+		ExpandedArrayHeader *freqsarr = DatumGetExpandedArray(freqs);
+		ExpandedArrayHeader *valsarr = DatumGetExpandedArray(vals);
+
+		int			nvals = value_array_len(valsarr, valsname);
+		int			nfreqs = value_not_null_array_len(freqsarr, freqsname);
+
+		if (nfreqs != nvals)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s has %d elements, but %s has %d elements, "
+							"but they must be equal",
+							freqsname, nfreqs,
+							valsname, nvals)));
+
+		/*
+		 * check that freqs sum to <= 1.0 or some number slightly higer to
+		 * allow for compounded rounding errors.
+		 */
+
+
+		if (nfreqs >= 1)
+		{
+			const float4 freqsummax = 1.1;
+
+			float4		prev = DatumGetFloat4(freqsarr->dvalues[0]);
+			float4		freqsum = prev;
+
+			for (int i = 1; i < nfreqs; i++)
+			{
+				float4		f = DatumGetFloat4(freqsarr->dvalues[i]);
+
+				if (f > prev)
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("%s array values must be in descending "
+									"order, but %f > %f",
+									freqsname, f, prev)));
+
+				freqsum += f;
+				prev = f;
+			}
+
+			if (freqsum > freqsummax)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("The sum of elements in %s must not exceed "
+								"%.2f but is %f",
+								freqsname, freqsummax, freqsum)));
+		}
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_MCV);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(eqopr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] = freqs;
+		values[Anum_pg_statistic_stavalues1 - 1 + k] = vals;
+
+		k++;
+	}
+
+
+	/* HIST_BOUNDS => STATISTIC_KIND_HISTOGRAM */
+	if (!PG_ARGISNULL(P_HIST_BOUNDS))
+	{
+		const char *statname = param_names[P_HIST_BOUNDS];
+		Datum		strvalue = PG_GETARG_DATUM(P_HIST_BOUNDS);
+		Datum		stavalues = cast_stavalue(&finfo, strvalue, basetypid, typmod);
+
+		ExpandedArrayHeader *arr = DatumGetExpandedArray(stavalues);
+		SortSupportData ssupd;
+
+		int			nelems = value_not_null_array_len(arr, statname);
+
+
+
+		memset(&ssupd, 0, sizeof(ssupd));
+		ssupd.ssup_cxt = CurrentMemoryContext;
+		ssupd.ssup_collation = typcoll;
+		ssupd.ssup_nulls_first = false;
+		ssupd.abbreviate = false;
+
+		PrepareSortSupportFromOrderingOp(baseltopr, &ssupd);
+
+		/*
+		 * This is a histogram, which means that the values must be in
+		 * monotonically non-decreasing order. If we every find a case where
+		 * [n] > [n+1], raise an error.
+		 */
+		for (int i = 1; i < nelems; i++)
+		{
+			Datum		a = arr->dvalues[i - 1];
+			Datum		b = arr->dvalues[i];
+
+			if (ssupd.comparator(a, b, &ssupd) > 0)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s values must be in ascending order %s",
+								statname, TextDatumGetCString(strvalue))));
+
+		}
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_HISTOGRAM);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(ltopr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+		k++;
+	}
+
+	/* CORRELATION => STATISTIC_KIND_CORRELATION */
+	if (!PG_ARGISNULL(P_CORRELATION))
+	{
+		const char *statname = param_names[P_CORRELATION];
+		Datum		elem = PG_GETARG_DATUM(P_CORRELATION);
+		Datum		elems[] = {elem};
+		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+
+		const float4 corr_min = -1.0;
+		const float4 corr_max = 1.0;
+		float4		corr = PG_GETARG_FLOAT4(P_CORRELATION);
+
+		if ((corr < corr_min) || (corr > corr_max))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s %f is out of range %.1f to %.1f",
+							statname, corr, corr_min, corr_max)));
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_CORRELATION);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(ltopr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] = PointerGetDatum(arry);
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+		k++;
+	}
+
+	/* MC_ELEMS && MC_ELEM_FREQS => STATISTIC_KIND_MCELEM */
+	if (has_mc_elems)
+	{
+		const char *elemsname = param_names[P_MC_ELEMS];
+		const char *freqsname = param_names[P_MC_ELEM_FREQS];
+		Datum		freqs = PG_GETARG_DATUM(P_MC_ELEM_FREQS);
+		Datum		vals = cast_stavalue(&finfo, PG_GETARG_DATUM(P_MC_ELEMS),
+										 basetypid, typmod);
+
+		ExpandedArrayHeader *freqsarr = DatumGetExpandedArray(freqs);
+		ExpandedArrayHeader *valsarr = DatumGetExpandedArray(vals);
+
+		int			nfreqs = value_not_null_array_len(freqsarr, freqsname);
+		int			nvals = value_not_null_array_len(valsarr, elemsname);
+
+		/*
+		 * The mcelem freqs array has either 2 or 3 additional values: the min
+		 * frequency, the max frequency, the optional null frequency.
+		 */
+		int			nfreqsmin = nvals + 2;
+		int			nfreqsmax = nvals + 3;
+
+		float4		freqlowbound;
+		float4		freqhighbound;
+
+		if (nfreqs > 0)
+		{
+			if ((nfreqs < nfreqsmin) || (nfreqs > nfreqsmax))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s has %d elements, but must have between "
+								"%d and %d because %s has %d elements",
+								freqsname, nfreqs, nfreqsmin, nfreqsmax,
+								elemsname, nvals)));
+
+			/*
+			 * the freqlowbound and freqhighbound must themselves be valid
+			 * percentages
+			 */
+
+			/* first freq element past the length of the values is the min */
+			freqlowbound = DatumGetFloat4(freqsarr->dvalues[nvals]);
+			if ((freqlowbound < frac_min) || (freqlowbound > frac_max))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s %s frequency %f is out of "
+								"range %.1f to %.1f",
+								freqsname, "minimum", freqlowbound,
+								frac_min, frac_max)));
+
+			/* second freq element past the length of the values is the max */
+			freqhighbound = DatumGetFloat4(freqsarr->dvalues[nvals + 1]);
+			if ((freqhighbound < frac_min) || (freqhighbound > frac_max))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s %s frequency %f is out of "
+								"range %.1f to %.1f",
+								freqsname, "maximum", freqhighbound,
+								frac_min, frac_max)));
+
+			/* low bound must be < high bound */
+			if (freqlowbound > freqhighbound)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s frequency low bound %f cannot be greater "
+								"than high bound %f",
+								freqsname, freqlowbound, freqhighbound)));
+
+			/*
+			 * third freq element past the length of the values is the null
+			 * frac
+			 */
+			if (nfreqs == nvals + 3)
+			{
+				float4		freqnullpct;
+
+				freqnullpct = DatumGetFloat4(freqsarr->dvalues[nvals + 2]);
+
+				if ((freqnullpct < frac_min) || (freqnullpct > frac_max))
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("%s %s frequency %f is out of "
+									"range %.1f to %.1f",
+									freqsname, "null", freqnullpct,
+									frac_min, frac_max)));
+			}
+
+			/*
+			 * All the freqs that match up to a val must bet between low/high
+			 * bounds (which is never less strict than frac_min/frac_max) and
+			 * must be in monotonically non-increasing order.
+			 *
+			 * Also, these frequencies do not sum to a number <= 1.0 as is the
+			 * case with MC_FREQS.
+			 */
+			if (nvals > 1)
+			{
+				float4		prev = DatumGetFloat4(freqsarr->dvalues[0]);
+
+				for (int i = 1; i < nvals; i++)
+				{
+					float4		f = DatumGetFloat4(freqsarr->dvalues[i]);
+
+					if ((f < freqlowbound) || (f > freqhighbound))
+						ereport(ERROR,
+								(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+								 errmsg("%s frequency %f is out of range "
+										"%f to %f",
+										freqsname, f,
+										freqlowbound, freqhighbound)));
+					if (f > prev)
+						ereport(ERROR,
+								(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+								 errmsg("%s array values must be in "
+										"descending order, but %f > %f",
+										freqsname, f, prev)));
+
+					prev = f;
+				}
+			}
+		}
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_MCELEM);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(baseeqopr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] =
+			PG_GETARG_DATUM(P_MC_ELEM_FREQS);
+		values[Anum_pg_statistic_stavalues1 - 1 + k] =
+			cast_stavalue(&finfo, PG_GETARG_DATUM(P_MC_ELEMS),
+						  basetypid, typmod);
+
+		k++;
+	}
+
+	/* ELEM_COUNT_HIST => STATISTIC_KIND_DECHIST */
+	if (!PG_ARGISNULL(P_ELEM_COUNT_HIST))
+	{
+		const char *statname = param_names[P_ELEM_COUNT_HIST];
+		Datum		stanumbers = PG_GETARG_DATUM(P_ELEM_COUNT_HIST);
+
+		ExpandedArrayHeader *arr = DatumGetExpandedArray(stanumbers);
+
+		const float4 last_min = 0.0;
+
+		int			nelems = value_not_null_array_len(arr, statname);
+		float4		last;
+
+		/* Last element must be >= 0 */
+		last = DatumGetFloat4(arr->dvalues[nelems - 1]);
+		if (last < last_min)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s has last element %f < %.1f",
+							statname, last, last_min)));
+
+		/* all other elements must be monotonically nondecreasing */
+		if (nelems > 1)
+		{
+			float4		prev = DatumGetFloat4(arr->dvalues[0]);
+
+			for (int i = 1; i < nelems - 1; i++)
+			{
+				float4		f = DatumGetFloat4(arr->dvalues[i]);
+
+				if (f < prev)
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("%s array values must be in ascending "
+									"order, but %f > %f",
+									statname, prev, f)));
+
+				prev = f;
+			}
+		}
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_DECHIST);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(baseeqopr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+		k++;
+	}
+
+	/*
+	 * RANGE_BOUNDS_HIST => STATISTIC_KIND_BOUNDS_HISTOGRAM
+	 *
+	 * This stakind appears before STATISTIC_KIND_BOUNDS_HISTOGRAM even though
+	 * it is numerically greater, and all other stakinds appear in numerical
+	 * order. We duplicate this quirk to make before/after tests of
+	 * pg_statistic records easier.
+	 *
+	 */
+	if (!PG_ARGISNULL(P_RANGE_BOUNDS_HIST))
+	{
+		const char *statname = param_names[P_RANGE_BOUNDS_HIST];
+		Datum		strvalue = PG_GETARG_DATUM(P_RANGE_BOUNDS_HIST);
+		Datum		stavalues = cast_stavalue(&finfo, strvalue, typid, typmod);
+
+		ExpandedArrayHeader *arr = DatumGetExpandedArray(stavalues);
+
+		int			nelems = value_not_null_array_len(arr, statname);
+
+		/*
+		 * The values in this array are range types, but in fact it's using
+		 * range types to have two parallel arrays with inclusive/exclusive
+		 * bounds, and those two arrays must each be monotonically
+		 * nondecreasing. So basically we want to test that: lower_bound(N) <=
+		 * lower_bound(N+1) and upper_bound(N) <= upper_bound(N+1)
+		 */
+		if (nelems > 1)
+		{
+			RangeType  *prevrange = DatumGetRangeTypeP(arr->dvalues[0]);
+			RangeBound	prevlower;
+			RangeBound	prevupper;
+			bool		empty;
+
+			range_deserialize(typcache, prevrange, &prevlower,
+							  &prevupper, &empty);
+
+
+			for (int i = 1; i < nelems; i++)
+			{
+				RangeType  *range = DatumGetRangeTypeP(arr->dvalues[i]);
+				RangeBound	lower;
+				RangeBound	upper;
+
+				range_deserialize(typcache, range, &lower, &upper, &empty);
+
+				if (range_cmp_bounds(typcache, &prevlower, &lower) == 1)
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("%s array %s bounds must be in ascending order",
+									statname, "lower")));
+
+				if (range_cmp_bounds(typcache, &prevupper, &upper) == 1)
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("%s array %s bounds must be in ascending order",
+									statname, "upper")));
+
+				memcpy(&lower, &prevlower, sizeof(RangeBound));
+				memcpy(&upper, &prevupper, sizeof(RangeBound));
+			}
+		}
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_BOUNDS_HISTOGRAM);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+		k++;
+	}
+
+	/*
+	 * P_RANGE_LENGTH_HIST && P_RANGE_EMPTY_FRAC =>
+	 * STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM
+	 */
+	if (has_rl_hist)
+	{
+		const char *histname = param_names[P_RANGE_LENGTH_HIST];
+		const char *fracname = param_names[P_RANGE_EMPTY_FRAC];
+		Datum		elem = PG_GETARG_DATUM(P_RANGE_EMPTY_FRAC);
+		Datum		rlhist = PG_GETARG_DATUM(P_RANGE_LENGTH_HIST);
+		Datum		elems[] = {elem};
+		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+		float4		frac = DatumGetFloat4(elem);
+		Datum		stavalue;
+
+		ExpandedArrayHeader *arr;
+		int			nelems;
+
+		if ((frac < frac_min) || (frac > frac_max))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s %f is out of range %.1f to %.1f",
+							fracname, frac, frac_min, frac_max)));
+
+		/*
+		 * RANGE_LENGTH_HIST is stored in an anyarray, but it is known to be
+		 * of type float8[]. It is also a histogram, so it must be in
+		 * monotonically nondecreasing order.
+		 */
+		stavalue = cast_stavalue(&finfo, rlhist, FLOAT8OID, typmod);
+
+		arr = DatumGetExpandedArray(stavalue);
+		nelems = value_not_null_array_len(arr, histname);
+
+		if (nelems > 1)
+		{
+			float8		prev = DatumGetFloat8(arr->dvalues[0]);
+
+			for (int i = 1; i < nelems; i++)
+			{
+				float8		f = DatumGetFloat8(arr->dvalues[i]);
+
+				if (f < prev)
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("%s array values must be in ascending "
+									"order, but %f > %f",
+									histname, prev, f)));
+
+				prev = f;
+			}
+		}
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(Float8LessOperator);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] = PointerGetDatum(arry);
+		values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalue;
+		k++;
+	}
+
+	/* fill in all remaining slots */
+	for (; k < STATISTIC_NUM_SLOTS; k++)
+	{
+		values[Anum_pg_statistic_stakind1 - 1 + k] = Int16GetDatum(0);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+	}
+
+	/* Is there already a pg_statistic tuple for this attribute? */
+	oldtup = SearchSysCache3(STATRELATTINH,
+							 ObjectIdGetDatum(relid),
+							 Int16GetDatum(attr->attnum),
+							 PG_GETARG_DATUM(P_INHERITED));
+
+	sd = table_open(StatisticRelationId, RowExclusiveLock);
+	indstate = CatalogOpenIndexes(sd);
+
+	if (HeapTupleIsValid(oldtup))
+	{
+		/* Yes, replace it */
+		bool		replaces[Natts_pg_statistic];
+
+		for (int i = 0; i < Natts_pg_statistic; i++)
+			replaces[i] = true;
+
+		stup = heap_modify_tuple(oldtup, RelationGetDescr(sd),
+								 values, nulls, replaces);
+		ReleaseSysCache(oldtup);
+		CatalogTupleUpdateWithInfo(sd, &stup->t_self, stup, indstate);
+	}
+	else
+	{
+		/* No, insert new tuple */
+		stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
+		CatalogTupleInsertWithInfo(sd, stup, indstate);
+	}
+
+	heap_freetuple(stup);
+	CatalogCloseIndexes(indstate);
+	table_close(sd, NoLock);
+	relation_close(rel, NoLock);
+	ReleaseSysCache(atup);
+	PG_RETURN_VOID();
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0d26e5b422a..fdb138357e3 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12180,4 +12180,19 @@
   proargtypes => 'int2',
   prosrc => 'gist_stratnum_identity' },
 
+# Statistics Import
+{ oid => '8048',
+  descr => 'set statistics on relation',
+  proname => 'pg_set_relation_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'void',
+  proargtypes => 'oid int4 float4 int4',
+  proargnames => '{relation,relpages,reltuples,relallvisible}',
+  prosrc => 'pg_set_relation_stats' },
+{ oid => '8049',
+  descr => 'set statistics on attribute',
+  proname => 'pg_set_attribute_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'void',
+  proargtypes => 'oid name bool float4 int4 float4 text _float4 text float4 text _float4 _float4 text float4 text',
+  proargnames => '{relation,attname,inherited,null_frac,avg_width,n_distinct,most_common_vals,most_common_freqs,histogram_bounds,correlation,most_common_elems,most_common_elem_freqs,elem_count_histogram,range_length_histogram,range_empty_frac,range_bounds_histogram}',
+  prosrc => 'pg_set_attribute_stats' },
 ]
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 7f2bf18716d..1dddf96576e 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -127,4 +127,6 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												int nclauses);
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
+extern Datum pg_set_relation_stats(PG_FUNCTION_ARGS);
+extern Datum pg_set_attribute_stats(PG_FUNCTION_ARGS);
 #endif							/* STATISTICS_H */
diff --git a/src/test/regress/expected/stats_export_import.out b/src/test/regress/expected/stats_export_import.out
new file mode 100644
index 00000000000..32b9e7ca113
--- /dev/null
+++ b/src/test/regress/expected/stats_export_import.out
@@ -0,0 +1,939 @@
+CREATE SCHEMA stats_export_import;
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+        0 |        -1 |             0
+(1 row)
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass, 999, 3.6::real, 15000);
+ pg_set_relation_stats 
+-----------------------
+ 
+(1 row)
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+      999 |       3.6 |         15000
+(1 row)
+
+-- error: relpages null
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass, NULL, 3.6::real, 15000);
+ERROR:  relpages cannot be NULL
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+      999 |       3.6 |         15000
+(1 row)
+
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => '0'::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  could not open relation with OID 0
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => NULL::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  relation cannot be NULL
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => NULL::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  attname cannot be NULL
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => NULL::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  inherited cannot be NULL
+-- error: inherited true on nonpartitioned table
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => true::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  Relation test is not partitioned, cannot accept inherited stats
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => NULL::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  null_frac cannot be NULL
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => NULL::integer,
+    n_distinct => 0.3::real);
+ERROR:  avg_width cannot be NULL
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => NULL::real);
+ERROR:  n_distinct cannot be NULL
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.1 |         2 |        0.3 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- error: null_frac < 0
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => -0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  null_frac -0.100000 is out of range 0.0 to 1.0
+-- error: null_frac > 1
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 1.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  null_frac 1.100000 is out of range 0.0 to 1.0
+-- error: avg_width < 0
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => -1::integer,
+    n_distinct => 0.3::real);
+ERROR:  avg_width -1 must be >= 0
+-- error: n_distinct < -1
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -1.1::real);
+ERROR:  n_distinct -1.100000 must be >= -1.0
+-- error: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => NULL::text,
+    most_common_freqs => '{0.1,0.2,0.3}'::real[]
+    );
+ERROR:  most_common_vals cannot be NULL when most_common_freqs is NOT NULL
+-- error: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{1,2,3}'::text,
+    most_common_freqs => NULL::real[]
+    );
+ERROR:  most_common_freqs cannot be NULL when most_common_vals is NOT NULL
+-- error: mcv / mcf length mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.2,0.1}'::real[]
+    );
+ERROR:  most_common_freqs has 2 elements, but most_common_vals has 3 elements, but they must be equal
+-- error: mcf sum bad
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.6,0.5,0.3}'::real[]
+    );
+ERROR:  The sum of elements in most_common_freqs must not exceed 1.10 but is 1.400000
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- error: histogram elements null value
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,NULL,3,4}'::text
+    );
+ERROR:  histogram_bounds array cannot contain NULL values
+-- error: histogram elements must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,20,3,4}'::text
+    );
+ERROR:  histogram_bounds values must be in ascending order {1,20,3,4}
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,2,3,4}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- error: correlation low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => -1.1::real);
+ERROR:  correlation -1.100000 is out of range -1.0 to 1.0
+-- error: correlation high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 1.1::real);
+ERROR:  correlation 1.100000 is out of range -1.0 to 1.0
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 0.5::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |         0.5 |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- error: scalars can't have mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{1,3}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+ERROR:  Relation test attname id is a scalar type, cannot have stats of type most_common_elems
+-- error: mcelem / mcelem null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,two}'::text,
+    most_common_elem_freqs => NULL::real[]
+    );
+ERROR:  most_common_elem_freqs cannot be NULL when most_common_elems is NOT NULL
+-- error: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => NULL::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
+    );
+ERROR:  most_common_elems cannot be NULL when most_common_elem_freqs is NOT NULL
+-- error: mcelem / mcelem length mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.2}'::real[]
+    );
+ERROR:  most_common_elem_freqs has 2 elements, but must have between 4 and 5 because most_common_elems has 2 elements
+-- error: mcelem freq element out of bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.1,0.2,0.3,0.0}'::real[]
+    );
+ERROR:  most_common_elem_freqs frequency 0.100000 is out of range 0.200000 to 0.300000
+-- error: mcelem freq low-high mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.4,0.3,0.0}'::real[]
+    );
+ERROR:  most_common_elem_freqs frequency low bound 0.400000 cannot be greater than high bound 0.300000
+-- error: mcelem freq null pct invalid
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,-0.0001}'::real[]
+    );
+ERROR:  most_common_elem_freqs null frequency -0.000100 is out of range 0.0 to 1.0
+-- error: mcelem freq bad low bound low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.2,-0.15,0.3,0.1}'::real[]
+    );
+ERROR:  most_common_elem_freqs minimum frequency -0.150000 is out of range 0.0 to 1.0
+-- error: mcelem freq bad low bound high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.2,1.5,0.3,0.1}'::real[]
+    );
+ERROR:  most_common_elem_freqs minimum frequency 1.500000 is out of range 0.0 to 1.0
+-- error: mcelem freq bad high bound low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.4,-0.3,0.1}'::real[]
+    );
+ERROR:  most_common_elem_freqs maximum frequency -0.300000 is out of range 0.0 to 1.0
+-- error: mcelem freq bad high bound high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.4,3.0,0.1}'::real[]
+    );
+ERROR:  most_common_elem_freqs maximum frequency 3.000000 is out of range 0.0 to 1.0
+-- error mcelem freq must be non-increasing
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.4,0.2,0.3,0.0}'::real[]
+    );
+ERROR:  most_common_elem_freqs frequency 0.400000 is out of range 0.200000 to 0.300000
+-- ok: mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             | {three,one}       | {0.3,0.2,0.2,0.3,0}    |                      |                        |                  | 
+(1 row)
+
+-- error: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ERROR:  Relation test attname id is a scalar type, cannot have stats of type elem_count_histogram
+-- error: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ERROR:  elem_count_histogram array cannot contain NULL values
+-- error: elem_count_histogram must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ERROR:  elem_count_histogram array values must be in ascending order, but 1.000000 > 0.000000
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+(1 row)
+
+-- error: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ERROR:  Relation test attname id is not a range type, cannot have stats of type range_length_histogram
+-- error: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ERROR:  range_empty_frac cannot be NULL when range_length_histogram is NOT NULL
+-- error: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real
+    );
+ERROR:  range_length_histogram cannot be NULL when range_empty_frac is NOT NULL
+-- error: range_empty_frac low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => -0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ERROR:  range_empty_frac -0.500000 is out of range 0.0 to 1.0
+-- error: range_empty_frac high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 1.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ERROR:  range_empty_frac 1.500000 is out of range 0.0 to 1.0
+-- error: range_length_histogram not ascending
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,Infinity,499}'::text
+    );
+ERROR:  range_length_histogram array values must be in ascending order, but Infinity > 499.000000
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
+(1 row)
+
+-- error: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ERROR:  Relation test attname id is not a range type, cannot have stats of type range_bounds_histogram
+-- error: range_bound_hist low bounds must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[-2,4)","[1,4)","[1,100)"}'::text
+    );
+ERROR:  range_bounds_histogram array lower bounds must be in ascending order
+-- error: range_bound_hist high bounds must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,11)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ERROR:  range_bounds_histogram array upper bounds must be in ascending order
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
+ stats_export_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      |                        |                  | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+(1 row)
+
+-- error: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[],
+    histogram_bounds => '{1,2,3,4}'::text,
+    correlation => 1.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    range_empty_frac => -0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ERROR:  imported statistics must have a maximum of 5 slots but 6 given
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+   relname    | num_stats 
+--------------+-----------
+ is_odd       |         1
+ is_odd_clone |         1
+ test         |         5
+ test_clone   |         5
+(4 rows)
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 5ac6e871f54..a7a4dfd411c 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,8 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc stats_export_import
+
 
 # ----------
 # Load huge amounts of data
diff --git a/src/test/regress/sql/stats_export_import.sql b/src/test/regress/sql/stats_export_import.sql
new file mode 100644
index 00000000000..b3f17373d3e
--- /dev/null
+++ b/src/test/regress/sql/stats_export_import.sql
@@ -0,0 +1,834 @@
+CREATE SCHEMA stats_export_import;
+
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass, 999, 3.6::real, 15000);
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+-- error: relpages null
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass, NULL, 3.6::real, 15000);
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => '0'::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => NULL::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => NULL::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => NULL::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: inherited true on nonpartitioned table
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => true::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => NULL::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => NULL::integer,
+    n_distinct => 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => NULL::real);
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- error: null_frac < 0
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => -0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: null_frac > 1
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 1.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: avg_width < 0
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => -1::integer,
+    n_distinct => 0.3::real);
+
+-- error: n_distinct < -1
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -1.1::real);
+
+-- error: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => NULL::text,
+    most_common_freqs => '{0.1,0.2,0.3}'::real[]
+    );
+
+-- error: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{1,2,3}'::text,
+    most_common_freqs => NULL::real[]
+    );
+
+-- error: mcv / mcf length mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.2,0.1}'::real[]
+    );
+
+-- error: mcf sum bad
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.6,0.5,0.3}'::real[]
+    );
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- error: histogram elements null value
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,NULL,3,4}'::text
+    );
+-- error: histogram elements must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,20,3,4}'::text
+    );
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,2,3,4}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- error: correlation low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => -1.1::real);
+
+-- error: correlation high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 1.1::real);
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 0.5::real);
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- error: scalars can't have mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{1,3}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+-- error: mcelem / mcelem null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,two}'::text,
+    most_common_elem_freqs => NULL::real[]
+    );
+
+-- error: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => NULL::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
+    );
+
+-- error: mcelem / mcelem length mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.2}'::real[]
+    );
+
+-- error: mcelem freq element out of bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.1,0.2,0.3,0.0}'::real[]
+    );
+
+-- error: mcelem freq low-high mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.4,0.3,0.0}'::real[]
+    );
+
+-- error: mcelem freq null pct invalid
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,-0.0001}'::real[]
+    );
+
+-- error: mcelem freq bad low bound low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.2,-0.15,0.3,0.1}'::real[]
+    );
+
+-- error: mcelem freq bad low bound high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.2,1.5,0.3,0.1}'::real[]
+    );
+
+-- error: mcelem freq bad high bound low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.4,-0.3,0.1}'::real[]
+    );
+
+-- error: mcelem freq bad high bound high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.4,3.0,0.1}'::real[]
+    );
+
+-- error mcelem freq must be non-increasing
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.4,0.2,0.3,0.0}'::real[]
+    );
+
+-- ok: mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- error: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- error: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- error: elem_count_histogram must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- error: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+-- error: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+-- error: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real
+    );
+-- error: range_empty_frac low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => -0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+-- error: range_empty_frac high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 1.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+-- error: range_length_histogram not ascending
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,Infinity,499}'::text
+    );
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- error: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+-- error: range_bound_hist low bounds must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[-2,4)","[1,4)","[1,100)"}'::text
+    );
+-- error: range_bound_hist high bounds must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,11)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- error: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[],
+    histogram_bounds => '{1,2,3,4}'::text,
+    correlation => 1.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    range_empty_frac => -0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+
+SELECT
+    format('SELECT pg_catalog.pg_set_attribute_stats( '
+            || 'relation => %L::regclass::oid, attname => %L::name, '
+            || 'inherited => %L::boolean, null_frac => %L::real, '
+            || 'avg_width => %L::integer, n_distinct => %L::real, '
+            || 'most_common_vals => %L::text, '
+            || 'most_common_freqs => %L::real[], '
+            || 'histogram_bounds => %L::text, '
+            || 'correlation => %L::real, '
+            || 'most_common_elems => %L::text, '
+            || 'most_common_elem_freqs => %L::real[], '
+            || 'elem_count_histogram => %L::real[], '
+            || 'range_length_histogram => %L::text, '
+            || 'range_empty_frac => %L::real, '
+            || 'range_bounds_histogram => %L::text) ',
+        'stats_export_import.' || s.tablename || '_clone', s.attname,
+        s.inherited, s.null_frac,
+        s.avg_width, s.n_distinct,
+        s.most_common_vals, s.most_common_freqs, s.histogram_bounds,
+        s.correlation, s.most_common_elems, s.most_common_elem_freqs,
+        s.elem_count_histogram, s.range_length_histogram,
+        s.range_empty_frac, s.range_bounds_histogram)
+FROM pg_catalog.pg_stats AS s
+WHERE s.schemaname = 'stats_export_import'
+AND s.tablename IN ('test', 'is_odd')
+\gexec
+
+-- restore ECHO to original value
+\set ECHO :orig_ECHO
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
-- 
2.44.0

0002-review.patchtext/x-patch; charset=UTF-8; name=0002-review.patchDownload
From adf40a833c14f3f80c7b3f40b5450e494cfb1561 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@2ndquadrant.com>
Date: Mon, 25 Mar 2024 17:18:34 +0100
Subject: [PATCH 2/4] review

---
 src/backend/statistics/statistics.c               | 11 +++++++++++
 src/test/regress/expected/stats_export_import.out |  4 ++--
 src/test/regress/sql/stats_export_import.sql      |  4 ++--
 3 files changed, 15 insertions(+), 4 deletions(-)

diff --git a/src/backend/statistics/statistics.c b/src/backend/statistics/statistics.c
index 3987425ea4d..d1cd8342047 100644
--- a/src/backend/statistics/statistics.c
+++ b/src/backend/statistics/statistics.c
@@ -259,10 +259,16 @@ value_not_null_array_len(ExpandedArrayHeader *arr, const char *name)
  * values, which is a valid input string for an array of the type or basetype
  * of the attribute. Any error generated by the array_in() function will in
  * turn fail the function.
+ *
+ * XXX this is a very looooong function, maybe split that into smaller parts
+ * that do different parts (e.g. checks vs. actual update).
  */
 Datum
 pg_set_attribute_stats(PG_FUNCTION_ARGS)
 {
+	/* XXX this would really deserve some comment that the order matters,
+	 * and so on. It took me ages to realize it's also the index of the
+	 * attribute in function arguments. */
 	enum
 	{
 		P_RELATION = 0,			/* oid */
@@ -285,6 +291,7 @@ pg_set_attribute_stats(PG_FUNCTION_ARGS)
 	};
 
 	/* names of columns that cannot be null */
+	/* XXX surely many of those can be NULL? say, MCV or elem fields */
 	const char *param_names[] = {
 		"relation",
 		"attname",
@@ -352,6 +359,8 @@ pg_set_attribute_stats(PG_FUNCTION_ARGS)
 
 	/*
 	 * A null in a required parameter is an error.
+	 *
+	 * XXX How do we know P_N_DISTINCT is the last non-null argument?
 	 */
 	for (int i = P_RELATION; i <= P_N_DISTINCT; i++)
 		if (PG_ARGISNULL(i))
@@ -372,6 +381,8 @@ pg_set_attribute_stats(PG_FUNCTION_ARGS)
 	/*
 	 * If a caller specifies more stakind-stats than we have slots to store
 	 * them, raise an error.
+	 *
+	 * XXX is it a good idea to treat bool as 0/1 int?
 	 */
 	stakind_count = (int) has_mcv + (int) has_mc_elems + (int) has_rl_hist +
 		(int) !PG_ARGISNULL(P_HIST_BOUNDS) +
diff --git a/src/test/regress/expected/stats_export_import.out b/src/test/regress/expected/stats_export_import.out
index 32b9e7ca113..c4072fa05f5 100644
--- a/src/test/regress/expected/stats_export_import.out
+++ b/src/test/regress/expected/stats_export_import.out
@@ -339,7 +339,7 @@ AND attname = 'id';
  stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |         0.5 |                   |                        |                      |                        |                  | 
 (1 row)
 
--- error: scalars can't have mcelem 
+-- error: scalars can't have mcelem
 SELECT pg_catalog.pg_set_attribute_stats(
     relation => 'stats_export_import.test'::regclass,
     attname => 'id'::name,
@@ -483,7 +483,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
     most_common_elem_freqs => '{0.3,0.4,0.2,0.3,0.0}'::real[]
     );
 ERROR:  most_common_elem_freqs frequency 0.400000 is out of range 0.200000 to 0.300000
--- ok: mcelem 
+-- ok: mcelem
 SELECT pg_catalog.pg_set_attribute_stats(
     relation => 'stats_export_import.test'::regclass,
     attname => 'tags'::name,
diff --git a/src/test/regress/sql/stats_export_import.sql b/src/test/regress/sql/stats_export_import.sql
index b3f17373d3e..ef4e74900b3 100644
--- a/src/test/regress/sql/stats_export_import.sql
+++ b/src/test/regress/sql/stats_export_import.sql
@@ -293,7 +293,7 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- error: scalars can't have mcelem 
+-- error: scalars can't have mcelem
 SELECT pg_catalog.pg_set_attribute_stats(
     relation => 'stats_export_import.test'::regclass,
     attname => 'id'::name,
@@ -437,7 +437,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
     most_common_elem_freqs => '{0.3,0.4,0.2,0.3,0.0}'::real[]
     );
 
--- ok: mcelem 
+-- ok: mcelem
 SELECT pg_catalog.pg_set_attribute_stats(
     relation => 'stats_export_import.test'::regclass,
     attname => 'tags'::name,
-- 
2.44.0

0003-Enable-dumping-of-table-index-stats-in-pg_dump.patchtext/x-patch; charset=UTF-8; name=0003-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From 394e28f8fe7f83e2c0ac6922b67f9df57bd7eb08 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH 3/4] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will also generate a statement
that calls all of the pg_set_relation_stats() and
pg_set_attribute_stats() calls necessary to restore the statistics of
the current system onto the destination system.

As is the pattern with pg_dump options, this can be disabled with
--no-statistics.
---
 src/bin/pg_dump/pg_backup.h          |   2 +
 src/bin/pg_dump/pg_backup_archiver.c |   5 +
 src/bin/pg_dump/pg_dump.c            | 100 ++++++++++++-
 src/bin/pg_dump/pg_dump.h            |   1 +
 src/bin/pg_dump/pg_dumpall.c         |   5 +
 src/bin/pg_dump/pg_restore.c         |   3 +
 src/fe_utils/Makefile                |   1 +
 src/fe_utils/meson.build             |   1 +
 src/fe_utils/stats_export.c          | 201 +++++++++++++++++++++++++++
 src/include/fe_utils/stats_export.h  |  36 +++++
 10 files changed, 353 insertions(+), 2 deletions(-)
 create mode 100644 src/fe_utils/stats_export.c
 create mode 100644 src/include/fe_utils/stats_export.h

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 9ef2f2017ef..1db5cf52eb8 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -112,6 +112,7 @@ typedef struct _restoreOptions
 	int			no_publications;	/* Skip publication entries */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
@@ -179,6 +180,7 @@ typedef struct _dumpOptions
 	int			no_security_labels;
 	int			no_publications;
 	int			no_subscriptions;
+	int			no_statistics;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index d97ebaff5b8..d5f61399d91 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2833,6 +2833,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's a stats dump, maybe ignore it */
+	if (ropt->no_statistics && strcmp(te->desc, "STATISTICS") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2862,6 +2866,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index b1c4c3ec7f0..621bfa12337 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -59,6 +59,7 @@
 #include "compress_io.h"
 #include "dumputils.h"
 #include "fe_utils/option_utils.h"
+#include "fe_utils/stats_export.h"
 #include "fe_utils/string_utils.h"
 #include "filter.h"
 #include "getopt_long.h"
@@ -428,6 +429,7 @@ main(int argc, char **argv)
 		{"no-comments", no_argument, &dopt.no_comments, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -1144,6 +1146,7 @@ help(const char *progname)
 	printf(_("  --no-comments                do not dump comments\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -7001,6 +7004,7 @@ getTables(Archive *fout, int *numTables)
 
 		/* Tables have data */
 		tblinfo[i].dobj.components |= DUMP_COMPONENT_DATA;
+		tblinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
 
 		/* Mark whether table has an ACL */
 		if (!PQgetisnull(res, i, i_relacl))
@@ -7498,6 +7502,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -10247,6 +10252,82 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const DumpableObject *dobj,
+				  const char *reltypename, DumpId dumpid)
+{
+	const char *stmtname = "relstats";
+	static bool prepared = false;
+	const char *values[2];
+	PGconn     *conn;
+	PGresult   *res;
+	PQExpBuffer query;
+	PQExpBuffer tag;
+
+	/* do nothing, if --no-statistics is supplied */
+	if (fout->dopt->no_statistics)
+		return;
+
+	conn = GetConnection(fout);
+
+	if (!prepared)
+	{
+		int		ver = PQserverVersion(conn);
+		char   *sql = exportRelationStatsSQL(ver);
+
+		if (sql == NULL)
+			pg_fatal("could not prepare stats export query for server version %d",
+					 ver);
+
+		res = PQprepare(conn, stmtname, sql, 2, NULL);
+		if (res == NULL || PQresultStatus(res) != PGRES_COMMAND_OK)
+			pg_fatal("prepared statement failed: %s",
+					 PQerrorMessage(conn));
+
+		free(sql);
+		prepared = true;
+	}
+
+	values[0] = fmtQualifiedId(dobj->namespace->dobj.name, dobj->name);
+	values[1] = NULL;
+	res = PQexecPrepared(conn, stmtname, 2, values, NULL, NULL, 0);
+
+
+	/* Result set must be 1x1 */
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		pg_fatal("error in statistics extraction: %s", PQerrorMessage(conn));
+
+	if (PQntuples(res) != 1)
+		pg_fatal("statistics extraction expected one row, but got %d rows", 
+				 PQntuples(res));
+
+	query = createPQExpBuffer();
+	appendPQExpBufferStr(query, strdup(PQgetvalue(res, 0, 0)));
+	appendPQExpBufferStr(query, ";\n");
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", reltypename,
+					  fmtId(dobj->name));
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATS IMPORT",
+							  .section = SECTION_POST_DATA,
+							  .createStmt = query->data,
+							  .deps = &dumpid,
+							  .nDeps = 1));
+
+	PQclear(res);
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -16681,6 +16762,13 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 	if (tbinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
 		dumpTableSecLabel(fout, tbinfo, reltypename);
 
+	/* Statistics are dependent on the definition, not the data */
+	/* Views don't have stats */
+	if ((tbinfo->dobj.dump & DUMP_COMPONENT_STATISTICS) &&
+		(tbinfo->relkind == RELKIND_VIEW))
+		dumpRelationStats(fout, &tbinfo->dobj, reltypename,
+						  tbinfo->dobj.dumpId);
+
 	/* Dump comments on inlined table constraints */
 	for (j = 0; j < tbinfo->ncheck; j++)
 	{
@@ -16882,6 +16970,7 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 	PQExpBuffer delq;
 	char	   *qindxname;
 	char	   *qqindxname;
+	DumpId		dumpid;
 
 	/* Do nothing in data-only dump */
 	if (dopt->dataOnly)
@@ -16994,14 +17083,21 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 		free(indstatvalsarray);
 	}
 
+	/* Comments and stats share same .dep */
+	dumpid = is_constraint ? indxinfo->indexconstraint :
+							 indxinfo->dobj.dumpId;
+
 	/* Dump Index Comments */
 	if (indxinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
 		dumpComment(fout, "INDEX", qindxname,
 					tbinfo->dobj.namespace->dobj.name,
 					tbinfo->rolname,
 					indxinfo->dobj.catId, 0,
-					is_constraint ? indxinfo->indexconstraint :
-					indxinfo->dobj.dumpId);
+					dumpid);
+
+	/* Dump Index Stats */
+	if (indxinfo->dobj.dump & DUMP_COMPONENT_STATISTICS)
+		dumpRelationStats(fout, &indxinfo->dobj, "INDEX", dumpid);
 
 	destroyPQExpBuffer(q);
 	destroyPQExpBuffer(delq);
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 9bc93520b45..d6a071ec28f 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -101,6 +101,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 046c0dc3b36..69652aa2059 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -105,6 +105,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -174,6 +175,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -453,6 +455,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -668,6 +672,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index c3beacdec1d..2d326dec727 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -75,6 +75,7 @@ main(int argc, char **argv)
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
 	static int	no_subscriptions = 0;
+	static int  no_statistics = 0;
 	static int	strict_names = 0;
 
 	struct option cmdopts[] = {
@@ -126,6 +127,7 @@ main(int argc, char **argv)
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
+		{"no-statistics", no_argument, &no_statistics, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -358,6 +360,7 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
diff --git a/src/fe_utils/Makefile b/src/fe_utils/Makefile
index 946c05258f0..c734f9f6d3a 100644
--- a/src/fe_utils/Makefile
+++ b/src/fe_utils/Makefile
@@ -32,6 +32,7 @@ OBJS = \
 	query_utils.o \
 	recovery_gen.o \
 	simple_list.o \
+	stats_export.o \
 	string_utils.o
 
 ifeq ($(PORTNAME), win32)
diff --git a/src/fe_utils/meson.build b/src/fe_utils/meson.build
index 14d0482a2cc..fce503f6410 100644
--- a/src/fe_utils/meson.build
+++ b/src/fe_utils/meson.build
@@ -12,6 +12,7 @@ fe_utils_sources = files(
   'query_utils.c',
   'recovery_gen.c',
   'simple_list.c',
+  'stats_export.c',
   'string_utils.c',
 )
 
diff --git a/src/fe_utils/stats_export.c b/src/fe_utils/stats_export.c
new file mode 100644
index 00000000000..fd09e6ea8af
--- /dev/null
+++ b/src/fe_utils/stats_export.c
@@ -0,0 +1,201 @@
+/*-------------------------------------------------------------------------
+ *
+ * Utility functions for extracting object statistics for frontend code
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/fe_utils/stats_export.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include "fe_utils/stats_export.h"
+/*
+#include "libpq/libpq-fs.h"
+*/
+#include "fe_utils/string_utils.h"
+
+/*
+ * No-frills catalog queries that are named according to the statistics they
+ * fetch (relation, attribute, extended) and the earliest server version for
+ * which they work. These are presented so that if other use cases arise they
+ * can share the same base queries but utilize them in their own way.
+ *
+ * The queries themselves do not filter results, so it is up to the caller
+ * to append a WHERE clause filtering either on either c.oid or a combination
+ * of c.relname and n.nspname.
+ */
+
+const char *export_class_stats_query_v9_2 =
+	"SELECT c.oid, n.nspname, c.relname, c.relpages, c.reltuples, c.relallvisible "
+	"FROM pg_class AS c "
+	"JOIN pg_namespace AS n ON n.oid = c.relnamespace";
+
+const char *export_attribute_stats_query_v17 =
+	"SELECT c.oid, n.nspname, c.relname, a.attnum, a.attname, s.inherited, "
+	"s.null_frac, s.avg_width, s.n_distinct, "
+	"s.most_common_vals::text AS most_common_vals, s.most_common_freqs, "
+	"s.histogram_bounds::text AS histogram_bounds, s.correlation, "
+	"s.most_common_elems::text AS most_common_elems, "
+	"s.most_common_elem_freqs, s.elem_count_histogram, "
+	"s.range_length_histogram::text AS range_length_histogram, "
+	"s.range_empty_frac, "
+	"s.range_bounds_histogram::text AS range_bounds_histogram "
+	"FROM pg_class AS c "
+	"JOIN pg_namespace AS n ON n.oid = c.relnamespace "
+	"JOIN pg_attribute AS a ON a.attrelid = c.oid AND not a.attisdropped "
+	"JOIN pg_stats AS s ON s.schemaname = n.nspname AND s.tablename = c.relname";
+
+const char *export_attribute_stats_query_v9_2 =
+	"SELECT c.oid, n.nspname, c.relname, a.attnum, a.attname, s.inherited, "
+	"s.null_frac, s.avg_width, s.n_distinct, "
+	"s.most_common_vals::text AS most_common_vals, s.most_common_freqs, "
+	"s.histogram_bounds::text AS histogram_bounds, s.correlation, "
+	"s.most_common_elems::text AS most_common_elems, "
+	"s.most_common_elem_freqs, s.elem_count_histogram, "
+	"NULL::text AS range_length_histogram, NULL::real AS range_empty_frac, "
+	"NULL::text AS range_bounds_histogram "
+	"FROM pg_class AS c "
+	"JOIN pg_namespace AS n ON n.oid = c.relnamespace "
+	"JOIN pg_attribute AS a ON a.attrelid = c.oid AND not a.attisdropped "
+	"JOIN pg_stats AS s ON s.schemaname = n.nspname AND s.tablename = c.relname";
+
+/*
+ * Returns true if the server version number supports exporting regular
+ * (e.g. pg_statistic) statistics.
+ */
+bool
+exportStatsSupported(int server_version_num)
+{
+	return (server_version_num >= MIN_SERVER_NUM);
+}
+
+/*
+ * Returns true if the server version number supports exporting extended
+ * (e.g. pg_statistic_ext, pg_statitic_ext_data) statistics.
+ *
+ * Currently, none do.
+ */
+bool
+exportExtStatsSupported(int server_version_num)
+{
+	return false;
+}
+
+/*
+ * Return the query appropriate for extracting relation statistics for the
+ * given server version, if one exists.
+ */
+const char *
+exportClassStatsSQL(int server_version_num)
+{
+	if (server_version_num >= MIN_SERVER_NUM)
+		return export_class_stats_query_v9_2;
+	return NULL;
+}
+
+/*
+ * Return the query appropriate for extracting attribute statistics for the
+ * given server version, if one exists.
+ */
+const char *
+exportAttributeStatsSQL(int server_version_num)
+{
+	if (server_version_num >= 170000)
+		return export_attribute_stats_query_v17;
+	if (server_version_num >= MIN_SERVER_NUM)
+		return export_attribute_stats_query_v9_2;
+	return NULL;
+}
+
+/*
+ * Generate a SQL statement that will itself generate a SQL statement to
+ * import all regular stats from a given relation into another relation.
+ *
+ * The query generated takes two parameters.
+ *
+ * $1 is of type Oid, and represents the oid of the source relation.
+ *
+ * $2 is is a cstring, and represents the qualified name of the destination
+ * relation. If NULL, then the qualified name of the source relation will
+ * be used. In either case, the value is casted via ::regclass.
+ *
+ * The function will return NULL for invalid server version numbers.
+ * Otherwise,
+ *
+ * This function needs to work on databases back to 9.2.
+ * The format() function was introduced in 9.1.
+ * The string_agg() aggregate was introduced in 9.0.
+ *
+ */
+char *exportRelationStatsSQL(int server_version_num)
+{
+	const char *relsql = exportClassStatsSQL(server_version_num);
+	const char *attrsql = exportAttributeStatsSQL(server_version_num);
+	const char *filter = "WHERE c.oid = $1::regclass";
+	char	   *s;
+	PQExpBuffer sql;
+
+	if ((relsql == NULL) || (attrsql == NULL))
+		return NULL;
+
+	/*
+	 * Set up the initial CTEs each with the same oid filter
+	 */
+	sql = createPQExpBuffer();
+	appendPQExpBuffer(sql,
+					  "WITH r AS (%s %s), a AS (%s %s), ",
+					  relsql, filter, attrsql, filter);
+
+	/*
+	 * Generate the pg_set_relation_stats function call for the relation
+	 * and one pg_set_attribute_stats function call for each attribute with
+	 * a pg_statistic entry. Give each row an order value such that the
+	 * set relation stats call will be first, followed by the set attribute
+	 * stats calls in attnum order (even though the attributes are identified
+	 * by attname).
+	 *
+	 * Then aggregate the function calls into a single SELECT statement that
+	 * puts the calls in the order described above.
+	 */
+	appendPQExpBufferStr(sql,
+		"s(ord,sql) AS ( "
+			"SELECT 0, format('pg_catalog.pg_set_relation_stats("
+			"%L::regclass, %L::integer, %L::real, %L::integer)', "
+			"coalesce($2, format('%I.%I', r.nspname, r.relname)), "
+			"r.relpages, r.reltuples, r.relallvisible) "
+			"FROM r "
+			"UNION ALL "
+			"SELECT 1, format('pg_catalog.pg_set_attribute_stats( "
+			"relation => %L::regclass, attname => %L::name, "
+			"inherited => %L::boolean, null_frac => %L::real, "
+			"avg_width => %L::integer, n_distinct => %L::real, "
+			"most_common_vals => %L::text, "
+			"most_common_freqs => %L::real[], "
+			"histogram_bounds => %L::text, "
+			"correlation => %L::real, "
+			"most_common_elems => %L::text, "
+			"most_common_elem_freqs => %L::real[], "
+			"elem_count_histogram => %L::real[], "
+			"range_length_histogram => %L::text, "
+			"range_empty_frac => %L::real, "
+			"range_bounds_histogram => %L::text)', "
+			"coalesce($2, format('%I.%I', a.nspname, a.relname)), "
+			"a.attname, a.inherited, a.null_frac, a.avg_width, "
+			"a.n_distinct, a.most_common_vals, a.most_common_freqs, "
+			"a.histogram_bounds, a.correlation, "
+			"a.most_common_elems, a.most_common_elem_freqs, "
+			"a.elem_count_histogram, a.range_length_histogram, "
+			"a.range_empty_frac, a.range_bounds_histogram ) "
+			"FROM a "
+		") "
+		"SELECT 'SELECT ' || string_agg(s.sql, ', ' ORDER BY s.ord) "
+		"FROM s ");
+
+	s = strdup(sql->data);
+	destroyPQExpBuffer(sql);
+	return s;
+}
diff --git a/src/include/fe_utils/stats_export.h b/src/include/fe_utils/stats_export.h
new file mode 100644
index 00000000000..f0dc7041f79
--- /dev/null
+++ b/src/include/fe_utils/stats_export.h
@@ -0,0 +1,36 @@
+/*-------------------------------------------------------------------------
+ *
+ * stats_export.h
+ *    Queries to export statistics from current and past versions.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1995, Regents of the University of California
+ *
+ * src/include/varatt.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef STATS_EXPORT_H
+#define STATS_EXPORT_H
+
+#include "postgres_fe.h"
+#include "libpq-fe.h"
+
+/*
+ * The minimum supported version number. No attempt is made to get statistics
+ * import to work on versions older than this. This version was initially chosen
+ * because that was the minimum version supported by pg_dump at the time.
+ */
+#define MIN_SERVER_NUM 90200
+
+extern bool exportStatsSupported(int server_version_num);
+extern bool exportExtStatsSupported(int server_version_num);
+
+extern const char *exportClassStatsSQL(int server_verson_num);
+extern const char *exportAttributeStatsSQL(int server_verson_num);
+
+extern char *exportRelationStatsSQL(int server_version_num);
+
+#endif
-- 
2.44.0

0004-review.patchtext/x-patch; charset=UTF-8; name=0004-review.patchDownload
From 567edfd8b77272af9d4320ecf7274cff26343bca Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tv@fuzzy.cz>
Date: Mon, 25 Mar 2024 22:06:49 +0100
Subject: [PATCH 4/4] review

---
 src/bin/pg_dump/pg_dump.c   | 5 ++++-
 src/fe_utils/stats_export.c | 3 +++
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 621bfa12337..6e772264509 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -10303,7 +10303,7 @@ dumpRelationStats(Archive *fout, const DumpableObject *dobj,
 		pg_fatal("error in statistics extraction: %s", PQerrorMessage(conn));
 
 	if (PQntuples(res) != 1)
-		pg_fatal("statistics extraction expected one row, but got %d rows", 
+		pg_fatal("statistics extraction expected one row, but got %d rows",
 				 PQntuples(res));
 
 	query = createPQExpBuffer();
@@ -16764,6 +16764,8 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 
 	/* Statistics are dependent on the definition, not the data */
 	/* Views don't have stats */
+	/* XXX is this check needed? we'd find there's no stats, no? */
+	/* XXX the condition is wrong, though */
 	if ((tbinfo->dobj.dump & DUMP_COMPONENT_STATISTICS) &&
 		(tbinfo->relkind == RELKIND_VIEW))
 		dumpRelationStats(fout, &tbinfo->dobj, reltypename,
@@ -17084,6 +17086,7 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 	}
 
 	/* Comments and stats share same .dep */
+	/* XXX what's ".dep"? */
 	dumpid = is_constraint ? indxinfo->indexconstraint :
 							 indxinfo->dobj.dumpId;
 
diff --git a/src/fe_utils/stats_export.c b/src/fe_utils/stats_export.c
index fd09e6ea8af..bcaaf200c24 100644
--- a/src/fe_utils/stats_export.c
+++ b/src/fe_utils/stats_export.c
@@ -78,6 +78,9 @@ exportStatsSupported(int server_version_num)
  * (e.g. pg_statistic_ext, pg_statitic_ext_data) statistics.
  *
  * Currently, none do.
+ *
+ * XXX Why do we even have this? Nothing is using it, even if it called
+ * this function we wouldn't know how to export stats.
  */
 bool
 exportExtStatsSupported(int server_version_num)
-- 
2.44.0

#78Corey Huinker
corey.huinker@gmail.com
In reply to: Ashutosh Bapat (#76)
Re: Statistics Import and Export

+\gexec

Why do we need to construct the command and execute? Can we instead
execute the function directly? That would also avoid ECHO magic.

We don't strictly need it, but I've found the set-difference operation to
be incredibly useful in diagnosing problems. Additionally, the values are
subject to change due to changes in test data, no guarantee that the output
of ANALYZE is deterministic, etc. But most of all, because the test cares
about the correct copying of values, not the values themselves.

+   <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>

COMMENT: The functions throw many validation errors. Do we want to list
the acceptable/unacceptable input values in the documentation corresponding
to those? I don't expect one line per argument validation. Something like
"these, these and these arguments can not be NULL" or "both arguments in
each of the pairs x and y, a and b, and c and d should be non-NULL or NULL
respectively".

Yes. It should.

Statistics are about data. Whenever pg_dump dumps some filtered data, the
statistics collected for the whole table are uselss. We should avoide
dumping
statistics in such a case. E.g. when only schema is dumped what good is
statistics? Similarly the statistics on a partitioned table may not be
useful
if some its partitions are not dumped. Said that dumping statistics on
foreign
table makes sense since they do not contain data but the statistics still
makes sense.

Good points, but I'm not immediately sure how to enforce those rules.

Key areas where I'm seeking feedback:

- What level of errors in a restore will a user tolerate, and what should
be done to the error messages to indicate that the data itself is fine, but
a manual operation to update stats on that particular table is now
warranted?
- To what degree could pg_restore/pg_upgrade take that recovery action
automatically?
- Should the individual attribute/class set function calls be grouped by
relation, so that they all succeed/fail together, or should they be called
separately, each able to succeed or fail on their own?
- Any other concerns about how to best use these new functions.

Whether or not I pass --no-statistics, there is no difference in the dump
output. Am I missing something?
$ pg_dump -d postgres > /tmp/dump_no_arguments.out
$ pg_dump -d postgres --no-statistics > /tmp/dump_no_statistics.out
$ diff /tmp/dump_no_arguments.out /tmp/dump_no_statistics.out
$

IIUC, pg_dump includes statistics by default. That means all our pg_dump
related tests will have statistics output by default. That's good since the
functionality will always be tested. 1. We need additional tests to ensure
that the statistics is installed after restore. 2. Some of those tests
compare dumps before and after restore. In case the statistics is changed
because of auto-analyze happening post-restore, these tests will fail.

+1

I believe, in order to import statistics through IMPORT FOREIGN SCHEMA,
postgresImportForeignSchema() will need to add SELECT commands invoking
pg_set_relation_stats() on each imported table and pg_set_attribute_stats()
on each of its attribute. Am I right? Do we want to make that happen in the
first cut of the feature? How do you expect these functions to be used to
update statistics of foreign tables?

I don't think there's time to get it into this release. I think we'd want
to extend this functionality to both IMPORT FOREIGN SCHEMA and ANALYZE for
foreign tables, in both cases with a server/table option to do regular
remote sampling. In both cases, they'd do a remote query very similar to
what pg_dump does (hence putting it in fe_utils), with some filters on
which columns/tables it believes it can trust. The remote table might
itself be a view (in which case they query would turn up nothing) or column
data types may change across the wire, and in those cases we'd have to fall
back to sampling.

#79Corey Huinker
corey.huinker@gmail.com
In reply to: Tomas Vondra (#77)
Re: Statistics Import and Export

1) The docs say this:

<para>
The purpose of this function is to apply statistics values in an
upgrade situation that are "good enough" for system operation until
they are replaced by the next <command>ANALYZE</command>, usually via
<command>autovacuum</command> This function is used by
<command>pg_upgrade</command> and <command>pg_restore</command> to
convey the statistics from the old system version into the new one.
</para>

I find this a bit confusing, considering the pg_dump/pg_restore changes
are only in 0002, not in this patch.

True, I'll split the docs.

2) Also, I'm not sure about this:

<parameter>relation</parameter>, the parameters in this are all
derived from <structname>pg_stats</structname>, and the values
given are most often extracted from there.

How do we know where do the values come from "most often"? I mean, where
else would it come from?

The next most likely sources would be 1. stats from another similar table
and 2. the imagination of a user testing hypothetical query plans.

3) The function pg_set_attribute_stats() is veeeeery long - 1000 lines
or so, that's way too many for me to think about. I agree the flow is
pretty simple, but I still wonder if there's a way to maybe split it
into some smaller "meaningful" steps.

I wrestle with that myself. I think there's some pieces that can be
factored out.

4) It took me *ages* to realize the enums at the beginning of some of
the functions are actually indexes of arguments in PG_FUNCTION_ARGS.
That'd surely deserve a comment explaining this.

My apologies, it definitely deserves a comment.

5) The comment for param_names in pg_set_attribute_stats says this:

/* names of columns that cannot be null */
const char *param_names[] = { ... }

but isn't that actually incorrect? I think that applies only to a couple
initial arguments, but then other fields (MCV, mcelem stats, ...) can be
NULL, right?

Yes, that is vestigial, I'll remove it.

6) There's a couple minor whitespace fixes or comments etc.

0002
----

1) I don't understand why we have exportExtStatsSupported(). Seems
pointless - nothing calls it, even if it did we don't know how to export
the stats.

It's not strictly necessary.

2) I think this condition in dumpTableSchema() is actually incorrect:

IMO that's wrong, the statistics should be delayed to the post-data
section. Which probably means there needs to be a separate dumpable
object for statistics on table/index, with a dependency on the object.

Good points.

3) I don't like the "STATS IMPORT" description. For extended statistics
we dump the definition as "STATISTICS" so why to shorten it to "STATS"
here? And "IMPORT" seems more like the process of loading data, not the
data itself. So I suggest "STATISTICS DATA".

+1

#80Jeff Davis
pgsql@j-davis.com
In reply to: Tom Lane (#15)
Re: Statistics Import and Export

Hi Tom,

Comparing the current patch set to your advice below:

On Tue, 2023-12-26 at 14:19 -0500, Tom Lane wrote:

I had things set up with simple functions, which
pg_dump would invoke by writing more or less

        SELECT pg_catalog.load_statistics(....);

This has a number of advantages, not least of which is that an
extension
could plausibly add compatible functions to older versions.

Check.

  The trick,
as you say, is to figure out what the argument lists ought to be.
Unfortunately I recall few details of what I wrote for Salesforce,
but I think I had it broken down in a way where there was a separate
function call occurring for each pg_statistic "slot", thus roughly

load_statistics(table regclass, attname text, stakind int, stavalue
...);

The problem with basing the function on pg_statistic directly is that
it can only be exported by the superuser.

The current patches instead base it on the pg_stats view, which already
does the privilege checking. Technically, information about which
stakinds go in which slots is lost, but I don't think that's a problem
as long as the stats make it in, right? It's also more user-friendly to
have nice names for the function arguments. The only downside I see is
that it's slightly asymmetric: exporting from pg_stats and importing
into pg_statistic.

I do have some concerns about letting non-superusers import their own
statistics: how robust is the rest of the code to handle malformed
stats once they make it into pg_statistic? Corey has addressed that
with basic input validation, so I think it's fine, but perhaps I'm
missing something.

As mentioned already, we'd also need some sort of
version identifier, and we'd expect the load_statistics() functions
to be able to transform the data if the old version used a different
representation.

You mean a version argument to the function, which would appear in the
exported stats data? That's not in the current patch set.

It's relying on the new version of pg_dump understanding the old
statistics data, and dumping it out in a form that the new server will
understand.

  I agree with the idea that an explicit representation
of the source table attribute's type would be wise, too.

That's not in the current patch set, either.

Regards,
Jeff Davis

#81Jeff Davis
pgsql@j-davis.com
In reply to: Tomas Vondra (#77)
1 attachment(s)
Re: Statistics Import and Export

On Tue, 2024-03-26 at 00:16 +0100, Tomas Vondra wrote:

I did take a closer look at v13 today. I have a bunch of comments and
some minor whitespace fixes in the attached review patches.

I also attached a patch implementing a different approach to the
pg_dump support. Instead of trying to create a query that uses SQL
"format()" to create more SQL, I did all the formatting in C. It turned
out to be about 30% fewer lines, and I find it more understandable and
consistent with the way other stuff in pg_dump happens.

The attached patch is pretty rough -- not many comments, and perhaps
some things should be moved around. I only tested very basic
dump/reload in SQL format.

Regards,
Jeff Davis

Attachments:

vjeff-0001-Enable-dumping-of-table-index-stats-in-pg_dump.patchtext/x-patch; charset=UTF-8; name=vjeff-0001-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From 7ca575e5a02bf380af92b6144622468a501f7636 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH vjeff] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will also generate a statement
that calls all of the pg_set_relation_stats() and
pg_set_attribute_stats() calls necessary to restore the statistics of
the current system onto the destination system.

As is the pattern with pg_dump options, this can be disabled with
--no-statistics.
---
 src/bin/pg_dump/pg_backup.h          |   2 +
 src/bin/pg_dump/pg_backup_archiver.c |   5 +
 src/bin/pg_dump/pg_dump.c            | 229 ++++++++++++++++++++++++++-
 src/bin/pg_dump/pg_dump.h            |   1 +
 src/bin/pg_dump/pg_dumpall.c         |   5 +
 src/bin/pg_dump/pg_restore.c         |   3 +
 6 files changed, 243 insertions(+), 2 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 9ef2f2017e..1db5cf52eb 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -112,6 +112,7 @@ typedef struct _restoreOptions
 	int			no_publications;	/* Skip publication entries */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
@@ -179,6 +180,7 @@ typedef struct _dumpOptions
 	int			no_security_labels;
 	int			no_publications;
 	int			no_subscriptions;
+	int			no_statistics;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index d97ebaff5b..d5f61399d9 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2833,6 +2833,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's a stats dump, maybe ignore it */
+	if (ropt->no_statistics && strcmp(te->desc, "STATISTICS") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2862,6 +2866,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index b1c4c3ec7f..d483122998 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -428,6 +428,7 @@ main(int argc, char **argv)
 		{"no-comments", no_argument, &dopt.no_comments, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -1144,6 +1145,7 @@ help(const char *progname)
 	printf(_("  --no-comments                do not dump comments\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -7001,6 +7003,7 @@ getTables(Archive *fout, int *numTables)
 
 		/* Tables have data */
 		tblinfo[i].dobj.components |= DUMP_COMPONENT_DATA;
+		tblinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
 
 		/* Mark whether table has an ACL */
 		if (!PQgetisnull(res, i, i_relacl))
@@ -7498,6 +7501,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -10247,6 +10251,212 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+static const char *rel_stats_arginfo[][2] = {
+	{"relation", "regclass"},
+	{"relpages", "integer"},
+	{"reltuples", "real"},
+	{"relallvisible", "integer"},
+};
+
+static const char *att_stats_arginfo[][2] = {
+	{"relation", "regclass"},
+	{"attname", "name"},
+	{"inherited", "boolean"},
+	{"null_frac", "float4"},
+	{"avg_width", "integer"},
+	{"n_distinct", "float4"},
+	{"most_common_vals", "text"},
+	{"most_common_freqs", "float4[]"},
+	{"histogram_bounds", "text"},
+	{"correlation", "float4"},
+	{"most_common_elems", "text"},
+	{"most_common_elem_freqs", "float4[]"},
+	{"elem_count_histogram", "float4[]"},
+	{"range_length_histogram", "text"},
+	{"range_empty_frac", "float4"},
+	{"range_bounds_histogram", "text"},
+};
+
+
+static void
+getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *tablename)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT oid::regclass as relation, relpages, "
+						 "reltuples, relallvisible "
+						 "FROM pg_class "
+						 "WHERE relnamespace::regnamespace::name = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND relname = ");
+	appendStringLiteralAH(query, tablename, fout);
+}
+
+static void
+getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *tablename)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "s.attname,"
+						 "s.inherited,"
+						 "s.null_frac,"
+						 "s.avg_width,"
+						 "s.n_distinct,"
+						 "s.most_common_vals,"
+						 "s.most_common_freqs,"
+						 "s.histogram_bounds,"
+						 "s.correlation,"
+						 "s.most_common_elems,"
+						 "s.most_common_elem_freqs,"
+						 "s.elem_count_histogram,");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 "s.range_length_histogram,"
+							 "s.range_empty_frac,"
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(query,
+							 "NULL AS range_length_histogram,"
+							 "NULL AS range_empty_frac,"
+							 "NULL AS range_bounds_histogram ");
+
+	appendPQExpBufferStr(query,
+						 "FROM pg_stats s, pg_class c "
+						 "WHERE c.relnamespace::regnamespace::name = s.schemaname "
+						 "AND c.relname = s.tablename "
+						 "AND s.schemaname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND s.tablename = ");
+	appendStringLiteralAH(query, tablename, fout);
+}
+
+static void
+appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
+					const char *argval, const char *argtype)
+{
+	appendPQExpBuffer(out, "\t%s => ", argname);
+	appendStringLiteralAH(out, argval, fout);
+	appendPQExpBuffer(out, "::%s", argtype);
+}
+
+static void
+appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	const char *sep = "";
+
+	if (PQntuples(res) == 0)
+		return;
+
+	if (PQntuples(res) > 1)
+		pg_fatal("relation stats export returned %d rows, expected 1",
+				 PQntuples(res));
+
+	appendPQExpBufferStr(out, "SELECT pg_catalog.pg_set_relation_stats(\n");
+	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
+	{
+		const char	*argname = rel_stats_arginfo[argno][0];
+		const char	*argtype = rel_stats_arginfo[argno][1];
+		int			 fieldno = PQfnumber(res, argname);
+
+		if (fieldno < 0)
+			pg_fatal("relation stats export query missing field '%s'",
+					 argname);
+
+		if (PQgetisnull(res, 0, fieldno))
+			continue;
+
+		appendPQExpBufferStr(out, sep);
+		appendNamedArgument(out, fout, argname,
+							PQgetvalue(res, 0, fieldno), argtype);
+		sep = ",\n";
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+static void
+appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	{
+		const char *sep = "";
+		appendPQExpBufferStr(out, "SELECT pg_catalog.pg_set_attribute_stats(\n");
+		for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
+		{
+			const char	*argname = att_stats_arginfo[argno][0];
+			const char	*argtype = att_stats_arginfo[argno][1];
+			int			 fieldno = PQfnumber(res, argname);
+
+			if (fieldno < 0)
+				pg_fatal("attribute stats export query missing field '%s'",
+						 argname);
+
+			if (PQgetisnull(res, rownum, fieldno))
+				continue;
+
+			appendPQExpBufferStr(out, sep);
+			appendNamedArgument(out, fout, argname,
+								PQgetvalue(res, rownum, fieldno), argtype);
+			sep = ",\n";
+		}
+		appendPQExpBufferStr(out, "\n);\n");
+	}
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const DumpableObject *dobj,
+				  const char *reltypename, DumpId dumpid)
+{
+	PGresult   *res;
+	PQExpBuffer query;
+	PQExpBuffer out;
+	PQExpBuffer tag;
+
+	/* do nothing, if --no-statistics is supplied */
+	if (fout->dopt->no_statistics)
+		return;
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", reltypename,
+					  fmtId(dobj->name));
+
+	query = createPQExpBuffer();
+	out = createPQExpBuffer();
+
+	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendRelStatsImport(out, fout, res);
+	PQclear(res);
+
+	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendAttStatsImport(out, fout, res);
+	PQclear(res);
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATISTICS DATA",
+							  .section = SECTION_NONE,
+							  .createStmt = out->data,
+							  .deps = &dumpid,
+							  .nDeps = 1));
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(tag);
+	destroyPQExpBuffer(out);
+}
+
 /*
  * dumpTableComment --
  *
@@ -16681,6 +16891,13 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 	if (tbinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
 		dumpTableSecLabel(fout, tbinfo, reltypename);
 
+	/* Statistics are dependent on the definition, not the data */
+	/* Views don't have stats */
+	if ((tbinfo->dobj.dump & DUMP_COMPONENT_STATISTICS) &&
+		(tbinfo->relkind != RELKIND_VIEW))
+		dumpRelationStats(fout, &tbinfo->dobj, reltypename,
+						  tbinfo->dobj.dumpId);
+
 	/* Dump comments on inlined table constraints */
 	for (j = 0; j < tbinfo->ncheck; j++)
 	{
@@ -16882,6 +17099,7 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 	PQExpBuffer delq;
 	char	   *qindxname;
 	char	   *qqindxname;
+	DumpId		dumpid;
 
 	/* Do nothing in data-only dump */
 	if (dopt->dataOnly)
@@ -16994,14 +17212,21 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 		free(indstatvalsarray);
 	}
 
+	/* Comments and stats share same .dep */
+	dumpid = is_constraint ? indxinfo->indexconstraint :
+		indxinfo->dobj.dumpId;
+
 	/* Dump Index Comments */
 	if (indxinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
 		dumpComment(fout, "INDEX", qindxname,
 					tbinfo->dobj.namespace->dobj.name,
 					tbinfo->rolname,
 					indxinfo->dobj.catId, 0,
-					is_constraint ? indxinfo->indexconstraint :
-					indxinfo->dobj.dumpId);
+					dumpid);
+
+	/* Dump Index Stats */
+	if (indxinfo->dobj.dump & DUMP_COMPONENT_STATISTICS)
+		dumpRelationStats(fout, &indxinfo->dobj, "INDEX", dumpid);
 
 	destroyPQExpBuffer(q);
 	destroyPQExpBuffer(delq);
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 9bc93520b4..d6a071ec28 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -101,6 +101,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 046c0dc3b3..69652aa205 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -105,6 +105,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -174,6 +175,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -453,6 +455,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -668,6 +672,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index c3beacdec1..2d326dec72 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -75,6 +75,7 @@ main(int argc, char **argv)
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
 	static int	no_subscriptions = 0;
+	static int  no_statistics = 0;
 	static int	strict_names = 0;
 
 	struct option cmdopts[] = {
@@ -126,6 +127,7 @@ main(int argc, char **argv)
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
+		{"no-statistics", no_argument, &no_statistics, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -358,6 +360,7 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
-- 
2.34.1

#82Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#81)
2 attachment(s)
Re: Statistics Import and Export

On Fri, Mar 29, 2024 at 2:25 AM Jeff Davis <pgsql@j-davis.com> wrote:

I also attached a patch implementing a different approach to the
pg_dump support. Instead of trying to create a query that uses SQL
"format()" to create more SQL, I did all the formatting in C. It turned
out to be about 30% fewer lines, and I find it more understandable and
consistent with the way other stuff in pg_dump happens.

That is fairly close to what I came up with per our conversation (attached
below), but I really like the att_stats_arginfo construct and I definitely
want to adopt that and expand it to a third dimension that flags the fields
that cannot be null. I will incorporate that into v15.

As for v14, here are the highlights:

0001:
- broke up pg_set_attribute_stats() into many functions. Every stat kind
gets its own validation function. Type derivation is now done in its own
function.
- removed check on inherited stats flag that required the table be
partitioned. that was in error
- added check for most_common_values to be unique in ascending order, and
tests to match
- no more mention of pg_dump in the function documentation
- function documentation cites pg-stats-view as reference for the
parameter's data requirements

0002:
- All relstats and attrstats calls are now their own statement instead of a
compound statement
- moved the archive TOC entry from post-data back to SECTION_NONE (as it
was modeled on object COMMENTs), which seems to work better.
- remove meta-query in favor of more conventional query building
- removed all changes to fe_utils/

Attachments:

v14-0001-Create-pg_set_relation_stats-pg_set_attribute_st.patchtext/x-patch; charset=US-ASCII; name=v14-0001-Create-pg_set_relation_stats-pg_set_attribute_st.patchDownload
From 67509e3d022b4991658ee3b2b8be7ef3544c45a6 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 21 Mar 2024 18:04:54 -0400
Subject: [PATCH v14 1/2] Create pg_set_relation_stats, pg_set_attribute_stats.

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

Both functions take an oid to identify the target relation that will
receive the statistics. There is nothing requiring that relation to be
the same one as the one exported, though the statistics have to make
sense in the context of the new relation.

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

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

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

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

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

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 07023ee61d..5dbb291410 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12192,4 +12192,19 @@
   proargtypes => 'int2',
   prosrc => 'gist_stratnum_identity' },
 
+# Statistics Import
+{ oid => '8048',
+  descr => 'set statistics on relation',
+  proname => 'pg_set_relation_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'void',
+  proargtypes => 'oid int4 float4 int4',
+  proargnames => '{relation,relpages,reltuples,relallvisible}',
+  prosrc => 'pg_set_relation_stats' },
+{ oid => '8049',
+  descr => 'set statistics on attribute',
+  proname => 'pg_set_attribute_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'void',
+  proargtypes => 'oid name bool float4 int4 float4 text _float4 text float4 text _float4 _float4 text float4 text',
+  proargnames => '{relation,attname,inherited,null_frac,avg_width,n_distinct,most_common_vals,most_common_freqs,histogram_bounds,correlation,most_common_elems,most_common_elem_freqs,elem_count_histogram,range_length_histogram,range_empty_frac,range_bounds_histogram}',
+  prosrc => 'pg_set_attribute_stats' },
 ]
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 7f2bf18716..1dddf96576 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -127,4 +127,6 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												int nclauses);
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
+extern Datum pg_set_relation_stats(PG_FUNCTION_ARGS);
+extern Datum pg_set_attribute_stats(PG_FUNCTION_ARGS);
 #endif							/* STATISTICS_H */
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index fe2bb50f46..22be7e6653 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -636,6 +636,24 @@ LANGUAGE INTERNAL
 CALLED ON NULL INPUT VOLATILE PARALLEL SAFE
 AS 'pg_stat_reset_slru';
 
+CREATE OR REPLACE FUNCTION
+  pg_set_attribute_stats(relation oid, attname name, inherited bool,
+                         null_frac real, avg_width integer, n_distinct real,
+                         most_common_vals text DEFAULT NULL,
+                         most_common_freqs real[] DEFAULT NULL,
+                         histogram_bounds text DEFAULT NULL,
+                         correlation real DEFAULT NULL,
+                         most_common_elems text DEFAULT NULL,
+                         most_common_elem_freqs real[] DEFAULT NULL,
+                         elem_count_histogram real[] DEFAULT NULL,
+                         range_length_histogram text DEFAULT NULL,
+                         range_empty_frac real DEFAULT NULL,
+                         range_bounds_histogram text DEFAULT NULL)
+RETURNS void
+LANGUAGE INTERNAL
+CALLED ON NULL INPUT VOLATILE
+AS 'pg_set_attribute_stats';
+
 --
 -- The default permissions for functions mean that anyone can execute them.
 -- A number of functions shouldn't be executable by just anyone, but rather
diff --git a/src/backend/statistics/Makefile b/src/backend/statistics/Makefile
index 89cf8c2797..e4f8ab7c4f 100644
--- a/src/backend/statistics/Makefile
+++ b/src/backend/statistics/Makefile
@@ -16,6 +16,7 @@ OBJS = \
 	dependencies.o \
 	extended_stats.o \
 	mcv.o \
-	mvdistinct.o
+	mvdistinct.o \
+	statistics.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/statistics/meson.build b/src/backend/statistics/meson.build
index 73b29a3d50..331e82c776 100644
--- a/src/backend/statistics/meson.build
+++ b/src/backend/statistics/meson.build
@@ -5,4 +5,5 @@ backend_sources += files(
   'extended_stats.c',
   'mcv.c',
   'mvdistinct.c',
+  'statistics.c',
 )
diff --git a/src/backend/statistics/statistics.c b/src/backend/statistics/statistics.c
new file mode 100644
index 0000000000..f0648aea99
--- /dev/null
+++ b/src/backend/statistics/statistics.c
@@ -0,0 +1,1283 @@
+/*-------------------------------------------------------------------------
+ * statistics.c
+ *
+ *	  POSTGRES statistics import
+ *
+ * Code supporting the direct importation of relation statistics, similar to
+ * what is done by the ANALYZE command.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *       src/backend/statistics/statistics.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "catalog/pg_database.h"
+#include "catalog/pg_operator.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_type.h"
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "nodes/nodeFuncs.h"
+#include "statistics/statistics.h"
+#include "utils/acl.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/float.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/rangetypes.h"
+#include "utils/syscache.h"
+#include "utils/typcache.h"
+
+/*
+ * A role has privileges to vacuum or analyze the relation if any of the
+ * following are true:
+ *   - the role owns the current database and the relation is not shared
+ *   - the role has the MAINTAIN privilege on the relation
+ *
+ */
+static bool
+can_modify_relation(Oid relid, Form_pg_class reltuple)
+{
+	return ((object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId())
+			 && !reltuple->relisshared) ||
+			pg_class_aclcheck(relid, GetUserId(), ACL_MAINTAIN) == ACLCHECK_OK);
+}
+
+/*
+ * A more encapsulated version of can_modify_relation for when the the
+ * HeapTuple and Form_pg_class are not needed later.
+ */
+static void
+check_relation_permissions(Relation rel)
+{
+	Oid			relid = RelationGetRelid(rel);
+	HeapTuple	ctup;
+	Form_pg_class pgcform;
+
+	/* Test existence of Relation */
+	ctup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+
+	if (!HeapTupleIsValid(ctup))
+		elog(ERROR, "cache lookup failed for relation %u", relid);
+
+	pgcform = (Form_pg_class) GETSTRUCT(ctup);
+
+	if (!can_modify_relation(relid, pgcform))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						RelationGetRelationName(rel))));
+
+	ReleaseSysCache(ctup);
+}
+
+/*
+ * Set statistics for a given pg_class entry.
+ *
+ * This does an in-place (i.e. non-transactional) update of pg_class, just as
+ * is done in ANALYZE.
+ *
+ */
+Datum
+pg_set_relation_stats(PG_FUNCTION_ARGS)
+{
+	/* Convenience enum to simulate naming the function arguments */
+	enum
+	{
+		P_RELATION = 0,			/* oid */
+		P_RELPAGES,				/* int */
+		P_RELTUPLES,			/* float4 */
+		P_RELALLVISIBLE,		/* int */
+		P_NUM_PARAMS
+	};
+
+	const char *param_names[] = {
+		"relation",
+		"relpages",
+		"reltuples",
+		"relallvisible"
+	};
+
+	Oid			relid;
+	Relation	rel;
+	HeapTuple	ctup;
+	Form_pg_class pgcform;
+	float4		reltuples;
+	int			relpages;
+	int			relallvisible;
+
+	/* Any NULL parameter is an error */
+	for (int i = P_RELATION; i < P_NUM_PARAMS; i++)
+		if (PG_ARGISNULL(i))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be NULL", param_names[i])));
+
+	relid = PG_GETARG_OID(P_RELATION);
+
+	/*
+	 * Open the relation, getting ShareUpdateExclusiveLock to ensure that no
+	 * other stat-setting operation can run on it concurrently.
+	 */
+	rel = table_open(RelationRelationId, ShareUpdateExclusiveLock);
+
+	ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(ctup))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_IN_USE),
+				 errmsg("pg_class entry for relid %u not found", relid)));
+
+	pgcform = (Form_pg_class) GETSTRUCT(ctup);
+
+	if (!can_modify_relation(relid, pgcform))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						RelationGetRelationName(rel))));
+
+
+	relpages = PG_GETARG_INT32(P_RELPAGES);
+	if (relpages < -1)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be < -1", param_names[P_RELPAGES])));
+	reltuples = PG_GETARG_FLOAT4(P_RELTUPLES);
+	if (reltuples < -1.0)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be < -1.0", param_names[P_RELTUPLES])));
+	relallvisible = PG_GETARG_INT32(P_RELALLVISIBLE);
+	if (relallvisible < -1)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be < -1", param_names[P_RELALLVISIBLE])));
+
+	/* Only update pg_class if there is a meaningful change */
+	if ((pgcform->reltuples != reltuples)
+		|| (pgcform->relpages != relpages)
+		|| (pgcform->relallvisible != relallvisible))
+	{
+		pgcform->relpages = PG_GETARG_INT32(P_RELPAGES);
+		pgcform->reltuples = PG_GETARG_FLOAT4(P_RELTUPLES);
+		pgcform->relallvisible = PG_GETARG_INT32(P_RELALLVISIBLE);
+
+		heap_inplace_update(rel, ctup);
+	}
+
+	table_close(rel, NoLock);
+
+	PG_RETURN_VOID();
+}
+
+
+
+/*
+ * Perform the cast of text to some array type
+ */
+static Datum
+cast_stavalues(FmgrInfo *finfo, Datum d, Oid typid, int32 typmod)
+{
+	char	   *s = TextDatumGetCString(d);
+	Datum		out = FunctionCall3(finfo, CStringGetDatum(s),
+									ObjectIdGetDatum(typid),
+									Int32GetDatum(typmod));
+
+	pfree(s);
+
+	return out;
+}
+
+/*
+ * Convenience routine to handle a common pattern where two function
+ * parameters must either both be NULL or both NOT NULL.
+ */
+static bool
+has_arg_pair(FunctionCallInfo fcinfo, const char **pnames, int p1, int p2)
+{
+	/* if on param is NULL and the other NOT NULL, report an error */
+	if (PG_ARGISNULL(p1) != PG_ARGISNULL(p2))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL when %s is NOT NULL",
+						pnames[(PG_ARGISNULL(p1)) ? p1 : p2],
+						pnames[(PG_ARGISNULL(p1)) ? p2 : p1])));
+
+	return (!PG_ARGISNULL(p1));
+}
+
+/*
+ * Test if the type is a scalar for MCELM purposes
+ */
+static bool
+type_is_scalar(Oid typid)
+{
+	HeapTuple	tp;
+	bool		result = false;
+
+	tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+	if (HeapTupleIsValid(tp))
+	{
+		Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+
+		result = (!OidIsValid(typtup->typanalyze));
+		ReleaseSysCache(tp);
+	}
+	return result;
+}
+
+static int
+value_array_len(ExpandedArrayHeader *arr, const char *name)
+{
+	if (arr->ndims != 1)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be a multidimensional array", name)));
+
+	return arr->dims[0];
+}
+
+/*
+ * Convenience routine to encapsulate all of the steps needed for any
+ * value array.
+ */
+static int
+value_not_null_array_len(ExpandedArrayHeader *arr, const char *name)
+{
+	const int	nelems = value_array_len(arr, name);
+
+	if (nelems > 0)
+	{
+		deconstruct_expanded_array(arr);
+
+		/* if there's a nulls array, all values must be false */
+		if (arr->dnulls != NULL)
+			for (int i = 0; i < nelems; i++)
+				if (arr->dnulls[i])
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("%s array cannot contain NULL values", name)));
+	}
+
+	return nelems;
+}
+
+/*
+ * Fetch datatype information, this is needed to derive the proper staopN
+ * and stacollN values.
+ *
+ * If this relation is an index and that index has expressions in it, and
+ * the attnum specified is known to be an expression, then we must walk
+ * the list attributes up to the specified attnum to get the right
+ * expression.
+ *
+ */
+static TypeCacheEntry *
+get_attr_stat_type(Relation rel, Name attname,
+				   int16 *attnum, int32 *typmod, Oid *typcoll)
+{
+	const int	operator_flags = TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR;
+	Oid			relid = RelationGetRelid(rel);
+	Form_pg_attribute attr;
+	HeapTuple	atup;
+	Oid			typid;
+
+	atup = SearchSysCache2(ATTNAME, ObjectIdGetDatum(relid),
+						   NameGetDatum(attname));
+
+	/* Attribute not found */
+	if (!HeapTupleIsValid(atup))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s has no attname %s",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+
+	attr = (Form_pg_attribute) GETSTRUCT(atup);
+	if (attr->attisdropped)
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s attname %s is dropped",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+
+	*attnum = attr->attnum;
+
+	if ((rel->rd_rel->relkind == RELKIND_INDEX
+		 || (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+		&& (rel->rd_indexprs != NIL)
+		&& (rel->rd_index->indkey.values[attr->attnum - 1] == 0))
+	{
+		ListCell   *indexpr_item = list_head(rel->rd_indexprs);
+		Node	   *expr;
+
+		for (int i = 0; i < attr->attnum - 1; i++)
+			if (rel->rd_index->indkey.values[i] == 0)
+				indexpr_item = lnext(rel->rd_indexprs, indexpr_item);
+
+		if (indexpr_item == NULL)	/* shouldn't happen */
+			elog(ERROR, "too few entries in indexprs list");
+
+		expr = (Node *) lfirst(indexpr_item);
+
+		typid = exprType(expr);
+		*typmod = exprTypmod(expr);
+
+		/*
+		 * If a collation has been specified for the index column, use that in
+		 * preference to anything else; but if not, fall back to whatever we
+		 * can get from the expression.
+		 */
+		if (OidIsValid(attr->attcollation))
+			*typcoll = attr->attcollation;
+		else
+			*typcoll = exprCollation(expr);
+	}
+	else
+	{
+		typid = attr->atttypid;
+		*typmod = attr->atttypmod;
+		*typcoll = attr->attcollation;
+	}
+	ReleaseSysCache(atup);
+
+	/* if it's a multirange, step down to the range type */
+	if (type_is_multirange(typid))
+		typid = get_multirange_range(typid);
+
+	return lookup_type_cache(typid, operator_flags);
+}
+
+
+/*
+ * Find the basetype for the given type.
+ */
+static TypeCacheEntry *
+get_base_typecache(TypeCacheEntry *typ)
+{
+	const int	operator_flags = TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR;
+	Oid			basetypid;
+
+	/*
+	 * if it's a range type, swap the subtype for the base type, otherwise get
+	 * the base element type
+	 */
+	if (typ->typtype == TYPTYPE_RANGE)
+		basetypid = get_range_subtype(typ->type_id);
+	else
+		basetypid = get_base_element_type(typ->type_id);
+
+	/* Type is already a base type */
+	if (basetypid == InvalidOid)
+		return typ;
+
+	return lookup_type_cache(basetypid, operator_flags);
+}
+
+/*
+ * The null_frac statistic must be in [0.0,1.0].
+ */
+static void
+validate_null_frac(float4 null_frac, const char *statname)
+{
+	const float4 min = 0.0;
+	const float4 max = 1.0;
+
+	if ((null_frac < min) || (null_frac > max))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s %f is out of range %.1f to %.1f",
+						statname, null_frac, min, max)));
+}
+
+/*
+ * The avg_width statistic must be non-negative.
+ */
+static void
+validate_avg_width(int32 avg_width, const char *statname)
+{
+	const int	min = 0;
+
+	if (avg_width < min)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s %d must be >= %d", statname, avg_width, min)));
+}
+
+/*
+ * The n_distinct statistic cannot be below -1.0.
+ */
+static void
+validate_n_distinct(float4 n_distinct, const char *statname)
+{
+	const float4 min = -1.0;
+
+	if (n_distinct < min)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s %f must be >= %.1f",
+						statname, n_distinct, min)));
+}
+
+/*
+ * Check correctness of MCV statistics pair.
+ *
+ * The length of the freqs array must be equal to the length of the values
+ * array. Neither array can contain NULL elements.
+ *
+ * The elements in the freqs array must be monotonically nondecreasing, and the
+ * sum of values in the array theoretically should not exceed 1.0, but we use a
+ * more relaxed limit to allow for compounded rounding errors.
+ */
+static void
+validate_mcv(Datum freqs, Datum values, const char *freqsname,
+			 const char *valuesname)
+{
+	ExpandedArrayHeader *freqsarr = DatumGetExpandedArray(freqs);
+	ExpandedArrayHeader *valsarr = DatumGetExpandedArray(values);
+
+	int			nvals = value_array_len(valsarr, valuesname);
+	int			nfreqs = value_not_null_array_len(freqsarr, freqsname);
+
+	if (nfreqs != nvals)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s has %d elements, but %s has %d elements, "
+						"but they must be equal",
+						freqsname, nfreqs, valuesname, nvals)));
+
+	/*
+	 * check that freqs sum to <= 1.0 or some number slightly higer to allow
+	 * for compounded rounding errors.
+	 */
+	if (nfreqs >= 1)
+	{
+		const float4 freqsummax = 1.1;
+
+		float4		prev = DatumGetFloat4(freqsarr->dvalues[0]);
+		float4		freqsum = prev;
+
+		for (int i = 1; i < nfreqs; i++)
+		{
+			float4		f = DatumGetFloat4(freqsarr->dvalues[i]);
+
+			if (f > prev)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s array values must be in descending "
+								"order, but %f > %f",
+								freqsname, f, prev)));
+
+			freqsum += f;
+			prev = f;
+		}
+
+		if (freqsum > freqsummax)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("The sum of elements in %s must not exceed "
+							"%.2f but is %f",
+							freqsname, freqsummax, freqsum)));
+	}
+}
+
+/*
+ * Check correctness of Histogram Bounds statistics.
+ *
+ * The array represents a histogram, which means that the values must be in
+ * monotonically non-decreasing order.
+ *
+ * If the attribute datatype in question uses collations then this validation
+ * has the chance to turn up any discrepancies in the source and destination
+ * collations if the datatype uses collations.
+ */
+static void
+validate_histogram_bounds(Datum stavalues, Datum srcstrvalue,
+						  Oid ltopr, Oid typcollation,
+						  const char *statname)
+{
+	ExpandedArrayHeader *arr = DatumGetExpandedArray(stavalues);
+	SortSupportData ssupd;
+
+	int			nelems = value_not_null_array_len(arr, statname);
+
+	memset(&ssupd, 0, sizeof(ssupd));
+	ssupd.ssup_cxt = CurrentMemoryContext;
+	ssupd.ssup_collation = typcollation;
+	ssupd.ssup_nulls_first = false;
+	ssupd.abbreviate = false;
+
+	PrepareSortSupportFromOrderingOp(ltopr, &ssupd);
+
+	/*
+	 * Array should be in * monotonically non-decreasing order. If we every
+	 * find a case where [n] > [n+1], raise an error.
+	 */
+	for (int i = 1; i < nelems; i++)
+	{
+		Datum		a = arr->dvalues[i - 1];
+		Datum		b = arr->dvalues[i];
+
+		if (ssupd.comparator(a, b, &ssupd) > 0)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s values must be in ascending order %s",
+							statname, TextDatumGetCString(srcstrvalue))));
+
+	}
+}
+
+/*
+ * Check correctness of Correlation statistics.
+ *
+ * Correlation must be in [-1.0,1,0].
+ */
+static void
+validate_correlation(float4 corr, const char *statname)
+{
+	const float4 min = -1.0;
+	const float4 max = 1.0;
+
+	if ((corr < min) || (corr > max))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s %f is out of range %.1f to %.1f",
+						statname, corr, min, max)));
+}
+
+/*
+ * Check correctness of Most Common Elements statistics.
+ *
+ * Neither array can contain NULL elements.
+ *
+ * The values array must be in monotonically incresing order. Elements may
+ * not repeat.
+ *
+ * Every element in the values array must have a corresponding element in the
+ * freqs array. The freqs array will have two additional elements, representing
+ * the frequency lower bound (LB) and frequency upper bound UB). The freqs
+ * array may have an optoinal extra value representing the null fraction of
+ * values in the column.
+ *
+ * The null fraction, LB and UB must be between [0.0,1.0].
+ *
+ * The LB must be <= the UB.
+ */
+static void
+validate_most_common_elements(Datum freqs, Datum elements, Datum srcstrvalue,
+							  const char *freqsname, const char *elemsname,
+							  Oid ltopr, Oid typcollation)
+{
+	ExpandedArrayHeader *freqsarr = DatumGetExpandedArray(freqs);
+	ExpandedArrayHeader *elemsarr = DatumGetExpandedArray(elements);
+
+	int			nfreqs = value_not_null_array_len(freqsarr, freqsname);
+	int			nelems = value_not_null_array_len(elemsarr, elemsname);
+
+	/*
+	 * The mcelem freqs array has either 2 or 3 additional values: the min
+	 * frequency, the max frequency, the optional null frequency.
+	 */
+	const int	nfreqsmin = nelems + 2;
+	const int	nfreqsmax = nelems + 3;
+
+	/*
+	 * the freqlowbound and freqhighbound must themselves be valid percentages
+	 */
+	const float4 frac_min = 0.0;
+	const float4 frac_max = 1.0;
+
+	float4		freqlowbound;
+	float4		freqhighbound;
+
+	if (nfreqs <= 0)
+		return;
+
+	if ((nfreqs < nfreqsmin) || (nfreqs > nfreqsmax))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s has %d elements, but must have between "
+						"%d and %d because %s has %d elements",
+						freqsname, nfreqs, nfreqsmin, nfreqsmax,
+						elemsname, nelems)));
+
+	/* first freq element past the length of the values is the min */
+	freqlowbound = DatumGetFloat4(freqsarr->dvalues[nelems]);
+	if ((freqlowbound < frac_min) || (freqlowbound > frac_max))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s %s frequency %f is out of "
+						"range %.1f to %.1f",
+						freqsname, "minimum", freqlowbound,
+						frac_min, frac_max)));
+
+	/* second freq element past the length of the values is the max */
+	freqhighbound = DatumGetFloat4(freqsarr->dvalues[nelems + 1]);
+	if ((freqhighbound < frac_min) || (freqhighbound > frac_max))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s %s frequency %f is out of "
+						"range %.1f to %.1f",
+						freqsname, "maximum", freqhighbound,
+						frac_min, frac_max)));
+
+	/* low bound must be < high bound */
+	if (freqlowbound > freqhighbound)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s frequency low bound %f cannot be greater "
+						"than high bound %f",
+						freqsname, freqlowbound, freqhighbound)));
+
+	/*
+	 * third freq element past the length of the values is the null frac
+	 */
+	if (nfreqs == nelems + 3)
+	{
+		float4		freqnullpct;
+
+		freqnullpct = DatumGetFloat4(freqsarr->dvalues[nelems + 2]);
+
+		if ((freqnullpct < frac_min) || (freqnullpct > frac_max))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s %s frequency %f is out of "
+							"range %.1f to %.1f",
+							freqsname, "null", freqnullpct,
+							frac_min, frac_max)));
+	}
+
+	/*
+	 * All the freqs that match up to a val must be between low/high bounds
+	 * (which is never less strict than frac_min/frac_max)
+	 *
+	 * Also, these frequencies do not sum to a number <= 1.0 as is the case
+	 * with MC_FREQS.
+	 */
+	for (int i = 0; i < nelems; i++)
+	{
+		float4		f = DatumGetFloat4(freqsarr->dvalues[i]);
+
+		if ((f < freqlowbound) || (f > freqhighbound))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s frequency %f is out of range %f to %f",
+							freqsname, f, freqlowbound, freqhighbound)));
+	}
+
+	/*
+	 * All the elements must be unique and in ascending order.
+	 */
+	if (nelems > 1)
+	{
+		SortSupportData ssupd;
+
+		memset(&ssupd, 0, sizeof(ssupd));
+		ssupd.ssup_cxt = CurrentMemoryContext;
+		ssupd.ssup_collation = typcollation;
+		ssupd.ssup_nulls_first = false;
+		ssupd.abbreviate = false;
+
+		PrepareSortSupportFromOrderingOp(ltopr, &ssupd);
+
+		for (int i = 1; i < nelems; i++)
+		{
+			Datum		a = elemsarr->dvalues[i - 1];
+			Datum		b = elemsarr->dvalues[i];
+
+			if (ssupd.comparator(a, b, &ssupd) >= 0)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s values must be unqiue and in ascending order %s",
+								elemsname, TextDatumGetCString(srcstrvalue))));
+
+		}
+	}
+}
+
+/*
+ * Check correctness of Distinct Elements Count Histogram statistics.
+ *
+ * All elements must be >= 0.0, and must be in monotonically nonincreasing
+ * order.
+ */
+static void
+validate_distinct_elements_count_histogram(Datum stanumbers,
+										   const char *statname)
+{
+	ExpandedArrayHeader *arr = DatumGetExpandedArray(stanumbers);
+
+	const float4 last_min = 0.0;
+
+	int			nelems = value_not_null_array_len(arr, statname);
+	float4		last;
+
+	/* Last element must be >= 0 */
+	last = DatumGetFloat4(arr->dvalues[nelems - 1]);
+	if (last < last_min)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s has last element %f < %.1f",
+						statname, last, last_min)));
+
+	/* all other elements must be monotonically nondecreasing */
+	if (nelems > 1)
+	{
+		float4		prev = DatumGetFloat4(arr->dvalues[0]);
+
+		for (int i = 1; i < nelems - 1; i++)
+		{
+			float4		f = DatumGetFloat4(arr->dvalues[i]);
+
+			if (f < prev)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s array values must be in ascending "
+								"order, but %f > %f",
+								statname, prev, f)));
+
+			prev = f;
+		}
+	}
+}
+
+/*
+ * Validate Range Bound Histogram statistic.
+ *
+ * The values in this array are ranges of the same scalar type as the attribute
+ * in question and cannot be empty. However those ranges are being used to
+ * represent two parallel arrays with inclusive/exclusive bounds, representing
+ * the histogram of lower bounds and the histogram of upper bounds. Each of
+ * those two arrays must each be monotonically nondecreasing.
+ */
+
+static void
+validate_bounds_histogram(Datum stavalues, Datum strvalue,
+						  TypeCacheEntry *typcache, const char *statname)
+{
+	ExpandedArrayHeader *arr = DatumGetExpandedArray(stavalues);
+
+	int			nelems = value_not_null_array_len(arr, statname);
+	RangeType  *prevrange;
+	RangeBound	prevlower;
+	RangeBound	prevupper;
+	bool		empty;
+
+	if (nelems <= 0)
+		return;
+
+	/* check first element for emptiness */
+	prevrange = DatumGetRangeTypeP(arr->dvalues[0]);
+	range_deserialize(typcache, prevrange, &prevlower,
+					  &prevupper, &empty);
+
+	if (empty)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s array bounds cannot have empty elements",
+						statname)));
+
+	/*
+	 * For element N, verify that: lower_bound(N) <= lower_bound(N+1), and
+	 * upper_bound(N) <= upper_bound(N+1)
+	 */
+	for (int i = 1; i < nelems; i++)
+	{
+		RangeType  *range = DatumGetRangeTypeP(arr->dvalues[i]);
+		RangeBound	lower;
+		RangeBound	upper;
+
+		range_deserialize(typcache, range, &lower, &upper, &empty);
+
+		if (empty)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s array bounds cannot have empty elements",
+							statname)));
+
+		if (range_cmp_bounds(typcache, &prevlower, &lower) == 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s array %s bounds must be in ascending order",
+							statname, "lower")));
+
+		if (range_cmp_bounds(typcache, &prevupper, &upper) == 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s array %s bounds must be in ascending order",
+							statname, "upper")));
+
+		memcpy(&lower, &prevlower, sizeof(RangeBound));
+		memcpy(&upper, &prevupper, sizeof(RangeBound));
+	}
+}
+
+/*
+ * Validate Range Length Histogram statistic pair.
+ *
+ * The empty_frac must be between in [0.0,1.0].
+ *
+ * rlhist is a histogram of float8[], so it must be in monotonically
+ * nondecreasing order.
+ */
+static void
+validate_range_length_histogram(Datum rlhist, float4 empty_frac,
+								const char *histname,
+								const char *fracname)
+{
+	ExpandedArrayHeader *arr = DatumGetExpandedArray(rlhist);
+
+	int			nelems = value_not_null_array_len(arr, histname);
+
+	const float4 min = 0.0;
+	const float4 max = 1.0;
+
+	if ((empty_frac < min) || (empty_frac > max))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s %f is out of range %.1f to %.1f",
+						fracname, empty_frac, min, max)));
+
+	if (nelems > 1)
+	{
+		float8		prev = DatumGetFloat8(arr->dvalues[0]);
+
+		for (int i = 1; i < nelems; i++)
+		{
+			float8		f = DatumGetFloat8(arr->dvalues[i]);
+
+			if (f < prev)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s array values must be in ascending "
+								"order, but %f > %f",
+								histname, prev, f)));
+
+			prev = f;
+		}
+	}
+}
+
+/*
+ * Update the pg_statistic record.
+ */
+static void
+update_pg_statistic(Datum values[], bool nulls[])
+{
+	Relation	sd = table_open(StatisticRelationId, RowExclusiveLock);
+	CatalogIndexState indstate = CatalogOpenIndexes(sd);
+	HeapTuple	oldtup;
+	HeapTuple	stup;
+
+	/* Is there already a pg_statistic tuple for this attribute? */
+	oldtup = SearchSysCache3(STATRELATTINH,
+							 values[Anum_pg_statistic_starelid - 1],
+							 values[Anum_pg_statistic_staattnum - 1],
+							 values[Anum_pg_statistic_stainherit - 1]);
+
+	if (HeapTupleIsValid(oldtup))
+	{
+		/* Yes, replace it */
+		bool		replaces[Natts_pg_statistic];
+
+		for (int i = 0; i < Natts_pg_statistic; i++)
+			replaces[i] = true;
+
+		stup = heap_modify_tuple(oldtup, RelationGetDescr(sd),
+								 values, nulls, replaces);
+		ReleaseSysCache(oldtup);
+		CatalogTupleUpdateWithInfo(sd, &stup->t_self, stup, indstate);
+	}
+	else
+	{
+		/* No, insert new tuple */
+		stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
+		CatalogTupleInsertWithInfo(sd, stup, indstate);
+	}
+
+	heap_freetuple(stup);
+	CatalogCloseIndexes(indstate);
+	table_close(sd, NoLock);
+}
+
+/*
+ * Import statistics for a given relation attribute.
+ *
+ * This will insert/replace a row in pg_statistic for the given relation and
+ * attribute name.
+ *
+ * The function takes input parameters that correspond to columns in the view
+ * pg_stats.
+ *
+ * Of those, the columns attname, inherited, null_frac, avg_width, and
+ * n_distinct all correspond to NOT NULL columns in pg_statistic. These
+ * parameters have no default value and passing NULL to them will result
+ * in an error.
+ *
+ * If there is no attribute with a matching attname in the relation, the
+ * function will raise an error. Likewise for setting inherited statistics
+ * on a table that is not partitioned.
+ *
+ * The remaining parameters all belong to a specific stakind. Some stakinds
+ * have multiple parameters, and in those cases both parameters must be
+ * NOT NULL or both NULL, otherwise an error will be raised.
+ *
+ * Omitting a parameter or explicitly passing NULL means that that particular
+ * stakind is not associated with the attribute.
+ *
+ * Parameters that are NOT NULL will be inspected for consistency checks,
+ * any of which can raise an error.
+ *
+ * Parameters corresponding to ANYARRAY columns are instead passed in as text
+ * values, which is a valid input string for an array of the type or basetype
+ * of the attribute. Any error generated by the array_in() function will in
+ * turn fail the function.
+ */
+Datum
+pg_set_attribute_stats(PG_FUNCTION_ARGS)
+{
+	/* Convenience enum to simulate naming the function arguments */
+	enum
+	{
+		P_RELATION = 0,			/* oid */
+		P_ATTNAME,				/* name */
+		P_INHERITED,			/* bool */
+		P_NULL_FRAC,			/* float4 */
+		P_AVG_WIDTH,			/* int32 */
+		P_N_DISTINCT,			/* float4 */
+		P_MC_VALS,				/* text, null */
+		P_MC_FREQS,				/* float4[], null */
+		P_HIST_BOUNDS,			/* text, null */
+		P_CORRELATION,			/* float4, null */
+		P_MC_ELEMS,				/* text, null */
+		P_MC_ELEM_FREQS,		/* float4[], null */
+		P_ELEM_COUNT_HIST,		/* float4[], null */
+		P_RANGE_LENGTH_HIST,	/* text, null */
+		P_RANGE_EMPTY_FRAC,		/* float4, null */
+		P_RANGE_BOUNDS_HIST,	/* text, null */
+		P_NUM_PARAMS
+	};
+
+	/* names of arguments indexed by above enum */
+	const char *param_names[] = {
+		"relation",
+		"attname",
+		"inherited",
+		"null_frac",
+		"avg_width",
+		"n_distinct",
+		"most_common_vals",
+		"most_common_freqs",
+		"histogram_bounds",
+		"correlation",
+		"most_common_elems",
+		"most_common_elem_freqs",
+		"elem_count_histogram",
+		"range_length_histogram",
+		"range_empty_frac",
+		"range_bounds_histogram"
+	};
+
+	Oid			relid;
+	Name		attname;
+	Relation	rel;
+
+	TypeCacheEntry *typcache;
+	TypeCacheEntry *basetypcache;
+
+	int16		attnum;
+	int32		typmod;
+	Oid			typcoll;
+
+	Datum		values[Natts_pg_statistic] = {0};
+	bool		nulls[Natts_pg_statistic] = {false};
+
+	FmgrInfo	finfo;
+
+	bool		has_mcv;
+	bool		has_mc_elems;
+	bool		has_rl_hist;
+	int			stakind_count;
+
+	int			k = 0;
+
+	/*
+	 * A null in a required parameter is an error.
+	 */
+	for (int i = P_RELATION; i <= P_N_DISTINCT; i++)
+		if (PG_ARGISNULL(i))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be NULL", param_names[i])));
+
+	/*
+	 * Check all parameter pairs up front.
+	 */
+	has_mcv = has_arg_pair(fcinfo, param_names,
+						   P_MC_VALS, P_MC_FREQS);
+	has_mc_elems = has_arg_pair(fcinfo, param_names,
+								P_MC_ELEMS, P_MC_ELEM_FREQS);
+	has_rl_hist = has_arg_pair(fcinfo, param_names,
+							   P_RANGE_LENGTH_HIST, P_RANGE_EMPTY_FRAC);
+
+	/*
+	 * If a caller specifies more stakind-stats than we have slots to store
+	 * them, raise an error.
+	 */
+	stakind_count = (int) has_mcv + (int) has_mc_elems + (int) has_rl_hist +
+		(int) !PG_ARGISNULL(P_HIST_BOUNDS) +
+		(int) !PG_ARGISNULL(P_CORRELATION) +
+		(int) !PG_ARGISNULL(P_ELEM_COUNT_HIST) +
+		(int) !PG_ARGISNULL(P_RANGE_BOUNDS_HIST);
+
+	if (stakind_count > STATISTIC_NUM_SLOTS)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("imported statistics must have a maximum of %d slots "
+						"but %d given",
+						STATISTIC_NUM_SLOTS, stakind_count)));
+
+	relid = PG_GETARG_OID(P_RELATION);
+
+	rel = relation_open(relid, ShareUpdateExclusiveLock);
+
+	check_relation_permissions(rel);
+
+	attname = PG_GETARG_NAME(P_ATTNAME);
+	typcache = get_attr_stat_type(rel, attname, &attnum, &typmod, &typcoll);
+
+	basetypcache = get_base_typecache(typcache);
+
+	/* P_HIST_BOUNDS and P_CORRELATION must have a base type < operator */
+	if (basetypcache->lt_opr == InvalidOid)
+		for (int i = P_HIST_BOUNDS; i <= P_CORRELATION; i++)
+			if (!PG_ARGISNULL(i))
+				ereport(ERROR,
+						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+						 errmsg("Relation %s attname %s cannot "
+								"have stats of type %s",
+								RelationGetRelationName(rel),
+								NameStr(*attname),
+								param_names[i])));
+
+	/* Scalar types can't have P_MC_ELEMS, P_MC_ELEM_FREQS, P_ELEM_COUNT_HIST */
+	if (type_is_scalar(typcache->type_id))
+		for (int i = P_MC_ELEMS; i <= P_ELEM_COUNT_HIST; i++)
+			if (!PG_ARGISNULL(i))
+				ereport(ERROR,
+						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+						 errmsg("Relation %s attname %s is a scalar type, "
+								"cannot have stats of type %s",
+								RelationGetRelationName(rel),
+								NameStr(*attname),
+								param_names[i])));
+
+	/* Only range types can have P_RANGE_x */
+	if ((typcache->typtype != TYPTYPE_RANGE) &&
+		(typcache->typtype != TYPTYPE_RANGE))
+		for (int i = P_RANGE_LENGTH_HIST; i <= P_RANGE_BOUNDS_HIST; i++)
+			if (!PG_ARGISNULL(i))
+				ereport(ERROR,
+						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+						 errmsg("Relation %s attname %s is not a range type, "
+								"cannot have stats of type %s",
+								RelationGetRelationName(rel),
+								NameStr(*attname),
+								param_names[i])));
+
+	validate_null_frac(PG_GETARG_FLOAT4(P_NULL_FRAC),
+					   param_names[P_NULL_FRAC]);
+	validate_avg_width(PG_GETARG_INT32(P_AVG_WIDTH),
+					   param_names[P_AVG_WIDTH]);
+	validate_n_distinct(PG_GETARG_FLOAT4(P_N_DISTINCT),
+						param_names[P_N_DISTINCT]);
+	values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(relid);
+	values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(attnum);
+	values[Anum_pg_statistic_stainherit - 1] = PG_GETARG_DATUM(P_INHERITED);
+	values[Anum_pg_statistic_stanullfrac - 1] = PG_GETARG_DATUM(P_NULL_FRAC);
+	values[Anum_pg_statistic_stawidth - 1] = PG_GETARG_DATUM(P_AVG_WIDTH);
+	values[Anum_pg_statistic_stadistinct - 1] = PG_GETARG_DATUM(P_N_DISTINCT);
+
+	fmgr_info(F_ARRAY_IN, &finfo);
+
+	/* MC_VALS && MC_FREQS => STATISTIC_KIND_MCV */
+	if (has_mcv)
+	{
+		Datum		stanumbers = PG_GETARG_DATUM(P_MC_FREQS);
+		Datum		stavalues = cast_stavalues(&finfo, PG_GETARG_DATUM(P_MC_VALS),
+											   typcache->type_id, typmod);
+
+		validate_mcv(stanumbers, stavalues, param_names[P_MC_FREQS],
+					 param_names[P_MC_VALS]);
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_MCV);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(typcache->eq_opr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+		values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+		k++;
+	}
+
+	/* HIST_BOUNDS => STATISTIC_KIND_HISTOGRAM */
+	if (!PG_ARGISNULL(P_HIST_BOUNDS))
+	{
+		Datum		strvalue = PG_GETARG_DATUM(P_HIST_BOUNDS);
+		Datum		stavalues = cast_stavalues(&finfo, strvalue,
+											   typcache->type_id, typmod);
+
+		validate_histogram_bounds(stavalues, strvalue, typcache->lt_opr, typcoll,
+								  param_names[P_HIST_BOUNDS]);
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_HISTOGRAM);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(typcache->lt_opr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+		k++;
+	}
+
+	/* CORRELATION => STATISTIC_KIND_CORRELATION */
+	if (!PG_ARGISNULL(P_CORRELATION))
+	{
+		Datum		elem = PG_GETARG_DATUM(P_CORRELATION);
+		Datum		elems[] = {elem};
+		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+		Datum		stanumbers = PointerGetDatum(arry);
+
+		validate_correlation(DatumGetFloat4(elem), param_names[P_CORRELATION]);
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_CORRELATION);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(typcache->lt_opr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+		k++;
+	}
+
+	/* MC_ELEMS && MC_ELEM_FREQS => STATISTIC_KIND_MCELEM */
+	if (has_mc_elems)
+	{
+		Datum		srcstrvalue = PG_GETARG_DATUM(P_MC_ELEMS);
+		Datum		stanumbers = PG_GETARG_DATUM(P_MC_ELEM_FREQS);
+		Datum		stavalues = cast_stavalues(&finfo, srcstrvalue,
+											   basetypcache->type_id, typmod);
+
+		validate_most_common_elements(stanumbers, stavalues, srcstrvalue,
+									  param_names[P_MC_ELEM_FREQS],
+									  param_names[P_MC_ELEMS],
+									  basetypcache->lt_opr, typcoll);
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_MCELEM);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(basetypcache->eq_opr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+		values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+		k++;
+	}
+
+	/* ELEM_COUNT_HIST => STATISTIC_KIND_DECHIST */
+	if (!PG_ARGISNULL(P_ELEM_COUNT_HIST))
+	{
+		Datum		stanumbers = PG_GETARG_DATUM(P_ELEM_COUNT_HIST);
+
+		validate_distinct_elements_count_histogram(stanumbers,
+												   param_names[P_ELEM_COUNT_HIST]);
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_DECHIST);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(basetypcache->eq_opr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+		k++;
+	}
+
+	/*
+	 * RANGE_BOUNDS_HIST => STATISTIC_KIND_BOUNDS_HISTOGRAM
+	 *
+	 * This stakind appears before STATISTIC_KIND_BOUNDS_HISTOGRAM even though
+	 * it is numerically greater, and all other stakinds appear in numerical
+	 * order. We duplicate this quirk to make before/after tests of
+	 * pg_statistic records easier.
+	 */
+	if (!PG_ARGISNULL(P_RANGE_BOUNDS_HIST))
+	{
+		Datum		strvalue = PG_GETARG_DATUM(P_RANGE_BOUNDS_HIST);
+		Datum		stavalues = cast_stavalues(&finfo, strvalue,
+											   typcache->type_id, typmod);
+
+		validate_bounds_histogram(stavalues, strvalue, typcache,
+								  param_names[P_RANGE_BOUNDS_HIST]);
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_BOUNDS_HISTOGRAM);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+		k++;
+	}
+
+	/*
+	 * P_RANGE_LENGTH_HIST && P_RANGE_EMPTY_FRAC =>
+	 * STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM
+	 */
+	if (has_rl_hist)
+	{
+		Datum		strvalue = PG_GETARG_DATUM(P_RANGE_LENGTH_HIST);
+
+		/* The anyarray is always a float8[] for this stakind */
+		Datum		stavalues = cast_stavalues(&finfo, strvalue, FLOAT8OID, 0);
+		Datum		elem = PG_GETARG_DATUM(P_RANGE_EMPTY_FRAC);
+		Datum		elems[] = {elem};
+		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+
+		validate_range_length_histogram(stavalues,
+										DatumGetFloat4(elem),
+										param_names[P_RANGE_LENGTH_HIST],
+										param_names[P_RANGE_EMPTY_FRAC]);
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(Float8LessOperator);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] = PointerGetDatum(arry);
+		values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+		k++;
+	}
+
+	/* fill in all remaining slots */
+	for (; k < STATISTIC_NUM_SLOTS; k++)
+	{
+		values[Anum_pg_statistic_stakind1 - 1 + k] = Int16GetDatum(0);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+	}
+
+	update_pg_statistic(values, nulls);
+
+	relation_close(rel, NoLock);
+	PG_RETURN_VOID();
+}
diff --git a/src/test/regress/expected/stats_export_import.out b/src/test/regress/expected/stats_export_import.out
new file mode 100644
index 0000000000..482f1eb2a3
--- /dev/null
+++ b/src/test/regress/expected/stats_export_import.out
@@ -0,0 +1,942 @@
+CREATE SCHEMA stats_export_import;
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+        0 |        -1 |             0
+(1 row)
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass, 999, 3.6::real, 15000);
+ pg_set_relation_stats 
+-----------------------
+ 
+(1 row)
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+      999 |       3.6 |         15000
+(1 row)
+
+-- error: relpages null
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass, NULL, 3.6::real, 15000);
+ERROR:  relpages cannot be NULL
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+      999 |       3.6 |         15000
+(1 row)
+
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => '0'::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  could not open relation with OID 0
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => NULL::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  relation cannot be NULL
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => NULL::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  attname cannot be NULL
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => NULL::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  inherited cannot be NULL
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => NULL::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  null_frac cannot be NULL
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => NULL::integer,
+    n_distinct => 0.3::real);
+ERROR:  avg_width cannot be NULL
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => NULL::real);
+ERROR:  n_distinct cannot be NULL
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.1 |         2 |        0.3 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- error: null_frac < 0
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => -0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  null_frac -0.100000 is out of range 0.0 to 1.0
+-- error: null_frac > 1
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 1.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  null_frac 1.100000 is out of range 0.0 to 1.0
+-- error: avg_width < 0
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => -1::integer,
+    n_distinct => 0.3::real);
+ERROR:  avg_width -1 must be >= 0
+-- error: n_distinct < -1
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -1.1::real);
+ERROR:  n_distinct -1.100000 must be >= -1.0
+-- error: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => NULL::text,
+    most_common_freqs => '{0.1,0.2,0.3}'::real[]
+    );
+ERROR:  most_common_vals cannot be NULL when most_common_freqs is NOT NULL
+-- error: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{1,2,3}'::text,
+    most_common_freqs => NULL::real[]
+    );
+ERROR:  most_common_freqs cannot be NULL when most_common_vals is NOT NULL
+-- error: mcv / mcf length mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.2,0.1}'::real[]
+    );
+ERROR:  most_common_freqs has 2 elements, but most_common_vals has 3 elements, but they must be equal
+-- error: mcf sum bad
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.6,0.5,0.3}'::real[]
+    );
+ERROR:  The sum of elements in most_common_freqs must not exceed 1.10 but is 1.400000
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- error: histogram elements null value
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,NULL,3,4}'::text
+    );
+ERROR:  histogram_bounds array cannot contain NULL values
+-- error: histogram elements must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,20,3,4}'::text
+    );
+ERROR:  histogram_bounds values must be in ascending order {1,20,3,4}
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,2,3,4}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- error: correlation low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => -1.1::real);
+ERROR:  correlation -1.100000 is out of range -1.0 to 1.0
+-- error: correlation high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 1.1::real);
+ERROR:  correlation 1.100000 is out of range -1.0 to 1.0
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 0.5::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |         0.5 |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- error: scalars can't have mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{1,3}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+ERROR:  Relation test attname id is a scalar type, cannot have stats of type most_common_elems
+-- error: mcelem / mcelem null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,two}'::text,
+    most_common_elem_freqs => NULL::real[]
+    );
+ERROR:  most_common_elem_freqs cannot be NULL when most_common_elems is NOT NULL
+-- error: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => NULL::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
+    );
+ERROR:  most_common_elems cannot be NULL when most_common_elem_freqs is NOT NULL
+-- error: mcelem / mcelem length mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2}'::real[]
+    );
+ERROR:  most_common_elem_freqs has 2 elements, but must have between 4 and 5 because most_common_elems has 2 elements
+-- error: mcelem freq element out of bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.1,0.2,0.3,0.0}'::real[]
+    );
+ERROR:  most_common_elem_freqs frequency 0.100000 is out of range 0.200000 to 0.300000
+-- error: mcelem freq low-high mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.4,0.3,0.0}'::real[]
+    );
+ERROR:  most_common_elem_freqs frequency low bound 0.400000 cannot be greater than high bound 0.300000
+-- error: mcelem freq null pct invalid
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,-0.0001}'::real[]
+    );
+ERROR:  most_common_elem_freqs null frequency -0.000100 is out of range 0.0 to 1.0
+-- error: mcelem freq bad low bound low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,-0.15,0.3,0.1}'::real[]
+    );
+ERROR:  most_common_elem_freqs minimum frequency -0.150000 is out of range 0.0 to 1.0
+-- error: mcelem freq bad low bound high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,1.5,0.3,0.1}'::real[]
+    );
+ERROR:  most_common_elem_freqs minimum frequency 1.500000 is out of range 0.0 to 1.0
+-- error: mcelem freq bad high bound low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.4,-0.3,0.1}'::real[]
+    );
+ERROR:  most_common_elem_freqs maximum frequency -0.300000 is out of range 0.0 to 1.0
+-- error: mcelem freq bad high bound high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.4,3.0,0.1}'::real[]
+    );
+ERROR:  most_common_elem_freqs maximum frequency 3.000000 is out of range 0.0 to 1.0
+-- error mcelem values must be monotonically increasing
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.3,0.2,0.3,0.0}'::real[]
+    );
+ERROR:  most_common_elems values must be unqiue and in ascending order {three,one}
+-- error mcelem values must be unique
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three,three}'::text,
+    most_common_elem_freqs => '{0.3,0.4,0.4,0.2,0.5,0.0}'::real[]
+    );
+ERROR:  most_common_elems values must be unqiue and in ascending order {one,three,three}
+-- ok: mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             | {one,three}       | {0.3,0.2,0.2,0.3,0}    |                      |                        |                  | 
+(1 row)
+
+-- error: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ERROR:  Relation test attname id is a scalar type, cannot have stats of type elem_count_histogram
+-- error: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ERROR:  elem_count_histogram array cannot contain NULL values
+-- error: elem_count_histogram must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ERROR:  elem_count_histogram array values must be in ascending order, but 1.000000 > 0.000000
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+(1 row)
+
+-- error: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ERROR:  Relation test attname id is not a range type, cannot have stats of type range_length_histogram
+-- error: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ERROR:  range_empty_frac cannot be NULL when range_length_histogram is NOT NULL
+-- error: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real
+    );
+ERROR:  range_length_histogram cannot be NULL when range_empty_frac is NOT NULL
+-- error: range_empty_frac low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => -0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ERROR:  range_empty_frac -0.500000 is out of range 0.0 to 1.0
+-- error: range_empty_frac high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 1.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ERROR:  range_empty_frac 1.500000 is out of range 0.0 to 1.0
+-- error: range_length_histogram not ascending
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,Infinity,499}'::text
+    );
+ERROR:  range_length_histogram array values must be in ascending order, but Infinity > 499.000000
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
+(1 row)
+
+-- error: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ERROR:  Relation test attname id is not a range type, cannot have stats of type range_bounds_histogram
+-- error: range_bound_hist low bounds must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[-2,4)","[1,4)","[1,100)"}'::text
+    );
+ERROR:  range_bounds_histogram array lower bounds must be in ascending order
+-- error: range_bound_hist high bounds must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,11)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ERROR:  range_bounds_histogram array upper bounds must be in ascending order
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
+ stats_export_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      |                        |                  | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+(1 row)
+
+-- error: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[],
+    histogram_bounds => '{1,2,3,4}'::text,
+    correlation => 1.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    range_empty_frac => -0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ERROR:  imported statistics must have a maximum of 5 slots but 6 given
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+   relname    | num_stats 
+--------------+-----------
+ is_odd       |         1
+ is_odd_clone |         1
+ test         |         5
+ test_clone   |         5
+(4 rows)
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 5ac6e871f5..a7a4dfd411 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,8 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc stats_export_import
+
 
 # ----------
 # Load huge amounts of data
diff --git a/src/test/regress/sql/stats_export_import.sql b/src/test/regress/sql/stats_export_import.sql
new file mode 100644
index 0000000000..1a7d02a2c7
--- /dev/null
+++ b/src/test/regress/sql/stats_export_import.sql
@@ -0,0 +1,836 @@
+CREATE SCHEMA stats_export_import;
+
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass, 999, 3.6::real, 15000);
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+-- error: relpages null
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass, NULL, 3.6::real, 15000);
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => '0'::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => NULL::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => NULL::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => NULL::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => NULL::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => NULL::integer,
+    n_distinct => 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => NULL::real);
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- error: null_frac < 0
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => -0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: null_frac > 1
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 1.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: avg_width < 0
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => -1::integer,
+    n_distinct => 0.3::real);
+
+-- error: n_distinct < -1
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -1.1::real);
+
+-- error: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => NULL::text,
+    most_common_freqs => '{0.1,0.2,0.3}'::real[]
+    );
+
+-- error: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{1,2,3}'::text,
+    most_common_freqs => NULL::real[]
+    );
+
+-- error: mcv / mcf length mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.2,0.1}'::real[]
+    );
+
+-- error: mcf sum bad
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.6,0.5,0.3}'::real[]
+    );
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- error: histogram elements null value
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,NULL,3,4}'::text
+    );
+-- error: histogram elements must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,20,3,4}'::text
+    );
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,2,3,4}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- error: correlation low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => -1.1::real);
+
+-- error: correlation high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 1.1::real);
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 0.5::real);
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- error: scalars can't have mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{1,3}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+-- error: mcelem / mcelem null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,two}'::text,
+    most_common_elem_freqs => NULL::real[]
+    );
+
+-- error: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => NULL::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
+    );
+
+-- error: mcelem / mcelem length mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2}'::real[]
+    );
+
+-- error: mcelem freq element out of bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.1,0.2,0.3,0.0}'::real[]
+    );
+
+-- error: mcelem freq low-high mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.4,0.3,0.0}'::real[]
+    );
+
+-- error: mcelem freq null pct invalid
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,-0.0001}'::real[]
+    );
+
+-- error: mcelem freq bad low bound low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,-0.15,0.3,0.1}'::real[]
+    );
+
+-- error: mcelem freq bad low bound high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,1.5,0.3,0.1}'::real[]
+    );
+
+-- error: mcelem freq bad high bound low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.4,-0.3,0.1}'::real[]
+    );
+
+-- error: mcelem freq bad high bound high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.4,3.0,0.1}'::real[]
+    );
+
+-- error mcelem values must be monotonically increasing
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.3,0.2,0.3,0.0}'::real[]
+    );
+-- error mcelem values must be unique
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three,three}'::text,
+    most_common_elem_freqs => '{0.3,0.4,0.4,0.2,0.5,0.0}'::real[]
+    );
+
+-- ok: mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- error: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- error: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- error: elem_count_histogram must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- error: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+-- error: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+-- error: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real
+    );
+-- error: range_empty_frac low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => -0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+-- error: range_empty_frac high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 1.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+-- error: range_length_histogram not ascending
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,Infinity,499}'::text
+    );
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- error: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+-- error: range_bound_hist low bounds must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[-2,4)","[1,4)","[1,100)"}'::text
+    );
+-- error: range_bound_hist high bounds must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,11)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- error: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[],
+    histogram_bounds => '{1,2,3,4}'::text,
+    correlation => 1.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    range_empty_frac => -0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+
+SELECT
+    format('SELECT pg_catalog.pg_set_attribute_stats( '
+            || 'relation => %L::regclass::oid, attname => %L::name, '
+            || 'inherited => %L::boolean, null_frac => %L::real, '
+            || 'avg_width => %L::integer, n_distinct => %L::real, '
+            || 'most_common_vals => %L::text, '
+            || 'most_common_freqs => %L::real[], '
+            || 'histogram_bounds => %L::text, '
+            || 'correlation => %L::real, '
+            || 'most_common_elems => %L::text, '
+            || 'most_common_elem_freqs => %L::real[], '
+            || 'elem_count_histogram => %L::real[], '
+            || 'range_length_histogram => %L::text, '
+            || 'range_empty_frac => %L::real, '
+            || 'range_bounds_histogram => %L::text) ',
+        'stats_export_import.' || s.tablename || '_clone', s.attname,
+        s.inherited, s.null_frac,
+        s.avg_width, s.n_distinct,
+        s.most_common_vals, s.most_common_freqs, s.histogram_bounds,
+        s.correlation, s.most_common_elems, s.most_common_elem_freqs,
+        s.elem_count_histogram, s.range_length_histogram,
+        s.range_empty_frac, s.range_bounds_histogram)
+FROM pg_catalog.pg_stats AS s
+WHERE s.schemaname = 'stats_export_import'
+AND s.tablename IN ('test', 'is_odd')
+\gexec
+
+-- restore ECHO to original value
+\set ECHO :orig_ECHO
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 93b0bc2bc6..153d0dc6ac 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29123,6 +29123,128 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
     in identifying the specific disk files associated with database objects.
    </para>
 
+   <table id="functions-admin-statsimport">
+    <title>Database Object Statistics Import Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_relation_stats</primary>
+        </indexterm>
+        <function>pg_set_relation_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>relpages</parameter> <type>integer</type>,
+         <parameter>reltuples</parameter> <type>real</type>,
+         <parameter>relallvisible</parameter> <type>integer</type> )
+        <returnvalue>void</returnvalue>
+       </para>
+       <para>
+        Updates the <structname>pg_class</structname> row for the specified
+        <parameter>relation</parameter>, setting the values for the columns
+        <structfield>reltuples</structfield>,
+        <structfield>relpages</structfield>, and
+        <structfield>relallvisible</structfield>.
+        To avoid table bloat in <structname>pg_class</structname>, this change
+        is made with an in-place update, and therefore cannot be rolled back
+        through normal transaction processing.
+       </para>
+       <para>
+        This function mimics the behavior of <command>ANALYZE</command> in its
+        effects on the values in <structname>pg_class</structname>, except that
+        the values are supplied as parameters rather than derived from table
+        sampling.
+       </para>
+       <para>
+        The caller must either be the owner of the relation, or have superuser
+        privileges.
+       </para>
+       <para>
+        The parameters supplied must all be NOT NULL. The value of
+        <structfield>relpages</structfield> must not be less than 0.  The
+        value of <structfield>reltuples</structfield> must not be less than
+        -1.0.  The value of <structfield>relallvisible</structfield> must not
+        be less than 0.
+       </para>
+       </entry>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_attribute_stats</primary>
+        </indexterm>
+        <function>pg_set_attribute_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>
+         , <parameter>attname</parameter> <type>name</type>
+         , <parameter>inherited</parameter> <type>boolean</type>
+         , <parameter>null_frac</parameter> <type>real</type>
+         , <parameter>avg_width</parameter> <type>integer</type>
+         , <parameter>n_distinct</parameter> <type>real</type>
+         [, <parameter>most_common_vals</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_freqs</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>histogram_bounds</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>correlation</parameter> <type>real</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_elems</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_elem_freqs</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>elem_count_histogram</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_length_histogram</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_empty_frac</parameter> <type>real</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_bounds_histogram</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ] )
+        <returnvalue>void</returnvalue>
+       </para>
+       <para>
+        Replaces the <structname>pg_statistic</structname> row for the
+        <structname>pg_attribute</structname> row specified by
+        <parameter>relation</parameter>, <parameter>attname</parameter>
+        and <parameter>inherited</parameter>. The remaining parameters
+        all correspond to attributes of the same name found in
+        <link linkend="view-pg-stats"><structname>pg_stats</structname></link>,
+        and the values supplied in the parameter must meet the requirements of
+        the corresponding attribute.
+       </para>
+       <para>
+        This function mimics the behavior of <command>ANALYZE</command> in its
+        effects on the values in <structname>pg_statistic</structname>, except
+        that the values are supplied as parameters rather than derived from
+        table sampling.
+       </para>
+       <para>
+        The purpose of this function is to apply statistics values in an
+        upgrade situation that are "good enough" for system operation until
+        they are replaced by the next <command>ANALYZE</command>, usually via
+        <command>autovacuum</command> This function is used by
+        <command>pg_upgrade</command> and <command>pg_restore</command> to
+        convey the statistics from the old system version into the new one.
+       </para>
+       <para>
+        The caller must either be the owner of the relation, or have superuser
+        privileges.
+       </para>
+       </entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
    <table id="functions-admin-dblocation">
     <title>Database Object Location Functions</title>
     <tgroup cols="1">
-- 
2.44.0

v14-0002-Enable-dumping-of-table-index-stats-in-pg_dump.patchtext/x-patch; charset=US-ASCII; name=v14-0002-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From c70f5cb4ef5d5f919036725331ff89a31234d215 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH v14 2/2] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will also generate a statement
that calls all of the pg_set_relation_stats() and
pg_set_attribute_stats() calls necessary to restore the statistics of
the current system onto the destination system.

As is the pattern with pg_dump options, this can be disabled with
--no-statistics.
---
 src/bin/pg_dump/pg_backup.h          |   2 +
 src/bin/pg_dump/pg_backup_archiver.c |   5 +
 src/bin/pg_dump/pg_dump.c            | 326 ++++++++++++++++++++++++++-
 src/bin/pg_dump/pg_dump.h            |   1 +
 src/bin/pg_dump/pg_dumpall.c         |   5 +
 src/bin/pg_dump/pg_restore.c         |   3 +
 6 files changed, 340 insertions(+), 2 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 9ef2f2017e..1db5cf52eb 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -112,6 +112,7 @@ typedef struct _restoreOptions
 	int			no_publications;	/* Skip publication entries */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
@@ -179,6 +180,7 @@ typedef struct _dumpOptions
 	int			no_security_labels;
 	int			no_publications;
 	int			no_subscriptions;
+	int			no_statistics;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index d97ebaff5b..d5f61399d9 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2833,6 +2833,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's a stats dump, maybe ignore it */
+	if (ropt->no_statistics && strcmp(te->desc, "STATISTICS") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2862,6 +2866,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index b1c4c3ec7f..29029909e9 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -428,6 +428,7 @@ main(int argc, char **argv)
 		{"no-comments", no_argument, &dopt.no_comments, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -1144,6 +1145,7 @@ help(const char *progname)
 	printf(_("  --no-comments                do not dump comments\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -7001,6 +7003,7 @@ getTables(Archive *fout, int *numTables)
 
 		/* Tables have data */
 		tblinfo[i].dobj.components |= DUMP_COMPONENT_DATA;
+		tblinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
 
 		/* Mark whether table has an ACL */
 		if (!PQgetisnull(res, i, i_relacl))
@@ -7498,6 +7501,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -10247,6 +10251,309 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * Convenience routine for constructing parameters of the form:
+ *  paraname => 'value'::type
+ */
+static void
+appendNamedCastedParam(PQExpBuffer str, const char *param, const char *value,
+					   const char *type, Archive *fout)
+{
+	appendPQExpBuffer(str, "%s => ", param);
+	appendStringLiteralAH(str, value, fout);
+	appendPQExpBuffer(str, "::%s", type);
+}
+
+/*
+ * Append a formatted pg_set_relation_stats statement.
+ */
+static void
+appendSetRelationStats(PQExpBuffer str, const char *qualrelname,
+					   PGresult *res, Archive *fout)
+{
+	int			i_relpages = PQfnumber(res, "relpages");
+	int			i_reltuples = PQfnumber(res, "relpages");
+	int			i_relallvisible = PQfnumber(res, "relallvisible");
+	char	   *val;
+
+	if (PQgetisnull(res, 0, i_relpages))
+		pg_fatal("Unexpected NULL found in %s", "relpages");
+	if (PQgetisnull(res, 0, i_reltuples))
+		pg_fatal("Unexpected NULL found in %s", "reltuples");
+	if (PQgetisnull(res, 0, i_relallvisible))
+		pg_fatal("Unexpected NULL found in %s", "relallvisible");
+
+	appendPQExpBufferStr(str, "SELECT pg_catalog.pg_set_relation_stats(\n");
+	appendNamedCastedParam(str, "relation",
+						   qualrelname, "regclass", fout);
+	appendPQExpBufferStr(str, ",\n");
+	val = PQgetvalue(res, 0, i_relpages);
+	appendNamedCastedParam(str, "relpages", val, "integer", fout);
+	appendPQExpBufferStr(str, ",\n");
+	val = PQgetvalue(res, 0, i_reltuples);
+	appendNamedCastedParam(str, "reltuples", val, "real", fout);
+	appendPQExpBufferStr(str, ",\n");
+	val = PQgetvalue(res, 0, i_relallvisible);
+	appendNamedCastedParam(str, "relallvisible", val, "integer", fout);
+	appendPQExpBufferStr(str, ");\n");
+}
+
+/*
+ * Append a series of formatted pg_set_attribute_stats statements.
+ */
+static void
+appendSetAttributeStats(PQExpBuffer str, const char *qualrelname,
+						PGresult *res, Archive *fout)
+{
+	/* these are required */
+	int			i_attname = PQfnumber(res, "attname");
+	int			i_inherited = PQfnumber(res, "inherited");
+	int			i_null_frac = PQfnumber(res, "null_frac");
+	int			i_avg_width = PQfnumber(res, "avg_width");
+	int			i_n_distinct = PQfnumber(res, "n_distinct");
+
+	/* these are optional, can be NULL */
+	int			i_most_common_vals = PQfnumber(res, "most_common_vals");
+	int			i_most_common_freqs = PQfnumber(res, "most_common_freqs");
+	int			i_histogram_bounds = PQfnumber(res, "histogram_bounds");
+	int			i_correlation = PQfnumber(res, "correlation");
+	int			i_most_common_elems = PQfnumber(res, "most_common_elems");
+	int			i_most_common_elem_freqs = PQfnumber(res, "most_common_elem_freqs");
+	int			i_elem_count_histogram = PQfnumber(res, "elem_count_histogram");
+	int			i_range_length_histogram = PQfnumber(res, "range_length_histogram");
+	int			i_range_empty_frac = PQfnumber(res, "range_empty_frac");
+	int			i_range_bounds_histogram = PQfnumber(res, "range_bounds_histogram");
+
+	int			ntups = PQntuples(res);
+
+	for (int i = 0; i < ntups; i++)
+	{
+		char	   *val;
+
+		if (PQgetisnull(res, i, i_attname))
+			pg_fatal("Unexpected NULL found in %s", "attname");
+		if (PQgetisnull(res, i, i_inherited))
+			pg_fatal("Unexpected NULL found in %s", "inherited");
+		if (PQgetisnull(res, i, i_null_frac))
+			pg_fatal("Unexpected NULL found in %s", "null_frac");
+		if (PQgetisnull(res, i, i_avg_width))
+			pg_fatal("Unexpected NULL found in %s", "avg_width");
+		if (PQgetisnull(res, i, i_n_distinct))
+			pg_fatal("Unexpected NULL found in %s", "n_distinct");
+
+		appendPQExpBufferStr(str,
+							 "SELECT pg_catalog.pg_set_attribute_stats(\n");
+		appendNamedCastedParam(str, "relation", qualrelname, "regclass", fout);
+		appendPQExpBufferStr(str, ",\n");
+		val = PQgetvalue(res, i, i_attname);
+		appendNamedCastedParam(str, "attname", val, "name", fout);
+		appendPQExpBufferStr(str, ",\n");
+		val = PQgetvalue(res, i, i_inherited);
+		appendNamedCastedParam(str, "inherited", val, "boolean", fout);
+		appendPQExpBufferStr(str, ",\n");
+		val = PQgetvalue(res, i, i_null_frac);
+		appendNamedCastedParam(str, "null_frac", val, "real", fout);
+		appendPQExpBufferStr(str, ",\n");
+		val = PQgetvalue(res, i, i_avg_width);
+		appendNamedCastedParam(str, "avg_width", val, "integer", fout);
+		appendPQExpBufferStr(str, ",\n");
+		val = PQgetvalue(res, i, i_n_distinct),
+			appendNamedCastedParam(str, "n_distinct", val, "real", fout);
+
+		/* Optional parameters */
+		if (!PQgetisnull(res, i, i_most_common_vals))
+		{
+			appendPQExpBufferStr(str, ",\n");
+			val = PQgetvalue(res, i, i_most_common_vals);
+			appendNamedCastedParam(str, "most_common_vals", val, "text", fout);
+		}
+
+		if (!PQgetisnull(res, i, i_most_common_freqs))
+		{
+			appendPQExpBufferStr(str, ",\n");
+			val = PQgetvalue(res, i, i_most_common_freqs);
+			appendNamedCastedParam(str, "most_common_freqs", val, "real[]", fout);
+		}
+
+		if (!PQgetisnull(res, i, i_histogram_bounds))
+		{
+			appendPQExpBufferStr(str, ",\n");
+			val = PQgetvalue(res, i, i_histogram_bounds);
+			appendNamedCastedParam(str, "histogram_bounds", val, "text", fout);
+		}
+
+		if (!PQgetisnull(res, i, i_correlation))
+		{
+			appendPQExpBufferStr(str, ",\n");
+			val = PQgetvalue(res, i, i_correlation);
+			appendNamedCastedParam(str, "correlation", val, "real", fout);
+		}
+
+		if (!PQgetisnull(res, i, i_most_common_elems))
+		{
+			appendPQExpBufferStr(str, ",\n");
+			val = PQgetvalue(res, i, i_most_common_elems);
+			appendNamedCastedParam(str, "most_common_elems", val, "text", fout);
+		}
+
+		if (!PQgetisnull(res, i, i_most_common_elem_freqs))
+		{
+			appendPQExpBufferStr(str, ",\n");
+			val = PQgetvalue(res, i, i_most_common_elem_freqs);
+			appendNamedCastedParam(str, "most_common_elem_freqs", val, "real[]", fout);
+		}
+
+		if (!PQgetisnull(res, i, i_elem_count_histogram))
+		{
+			appendPQExpBufferStr(str, ",\n");
+			val = PQgetvalue(res, i, i_elem_count_histogram);
+			appendNamedCastedParam(str, "elem_count_histogram", val, "real[]", fout);
+		}
+
+		if (!PQgetisnull(res, i, i_range_length_histogram))
+		{
+			appendPQExpBufferStr(str, ",\n");
+			val = PQgetvalue(res, i, i_range_length_histogram);
+			appendNamedCastedParam(str, "range_length_histogram", val, "text", fout);
+		}
+
+		if (!PQgetisnull(res, i, i_range_empty_frac))
+		{
+			appendPQExpBufferStr(str, ",\n");
+			val = PQgetvalue(res, i, i_range_empty_frac);
+			appendNamedCastedParam(str, "range_empty_frac", val, "real", fout);
+		}
+
+		if (!PQgetisnull(res, i, i_range_bounds_histogram))
+		{
+			appendPQExpBufferStr(str, ",\n");
+			val = PQgetvalue(res, i, i_range_bounds_histogram);
+			appendNamedCastedParam(str, "range_bounds_histogram", val, "text", fout);
+		}
+		appendPQExpBufferStr(str, ");\n");
+	}
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const DumpableObject *dobj,
+				  const char *reltypename, DumpId dumpid)
+{
+	const char *nspname = dobj->namespace->dobj.name;
+	const char *relname = dobj->name;
+
+	static bool prepared = false;
+
+	char	   *qualrelname = pg_strdup(fmtQualifiedId(nspname, relname));
+	PQExpBuffer query = createPQExpBuffer();	/* the extract queries */
+	PQExpBuffer out = createPQExpBuffer();	/* the dumped statements */
+	PGresult   *res;
+	PQExpBuffer tag = createPQExpBuffer();
+
+	/* do nothing, if --no-statistics is supplied */
+	if (fout->dopt->no_statistics)
+		return;
+
+	if (!prepared)
+	{
+		ArchiveHandle *AH = (ArchiveHandle *) fout;
+		int			ver = PQserverVersion(AH->connection);
+
+		/* Prepare getRelStats */
+		appendPQExpBufferStr(query,
+							 "PREPARE getRelStats(pg_catalog.oid) AS\n");
+
+		if (ver >= fout->minRemoteVersion)
+			appendPQExpBufferStr(query,
+								 "SELECT c.relpages, c.reltuples, "
+								 "c.relallvisible\n"
+								 "FROM pg_catalog.pg_class AS c\n");
+		else
+			pg_fatal("could not prepare stats export query for "
+					 "server version %d", ver);
+
+		appendPQExpBufferStr(query, "WHERE c.oid = $1");
+
+		ExecuteSqlStatement(fout, query->data);
+
+		resetPQExpBuffer(query);
+
+		/* Prepare getAttrStats */
+		appendPQExpBufferStr(query,
+							 "PREPARE getAttrStats(pg_catalog.oid) AS\n"
+							 "SELECT s.attname, s.inherited, s.null_frac, "
+							 "s.avg_width, s.n_distinct, s.most_common_vals,\n"
+							 "s.most_common_freqs, s.histogram_bounds, "
+							 "s.correlation, s.most_common_elems,\n"
+							 "s.most_common_elem_freqs, "
+							 "s.elem_count_histogram,\n");
+
+		if (ver >= 170000)
+			appendPQExpBufferStr(query,
+								 "s.range_length_histogram, "
+								 "s.range_empty_frac, "
+								 "s.range_bounds_histogram\n");
+		else if (ver >= fout->minRemoteVersion)
+			appendPQExpBufferStr(query,
+								 "NULL::text AS range_length_histogram, "
+								 "NULL::real AS range_empty_frac, "
+								 "NULL::text AS range_bounds_histogram\n");
+		else
+			pg_fatal("could not prepare stats export query for "
+					 "server version %d", ver);
+
+		appendPQExpBuffer(query,
+						  "FROM pg_catalog.pg_class AS c\n"
+						  "JOIN pg_catalog.pg_namespace AS n "
+						  "ON n.oid = c.relnamespace\n"
+						  "JOIN pg_catalog.pg_stats AS s "
+						  "ON s.schemaname = n.nspname "
+						  "AND s.tablename = c.relname\n"
+						  "WHERE c.oid = $1");
+
+		ExecuteSqlStatement(fout, query->data);
+
+		resetPQExpBuffer(query);
+
+		prepared = true;
+	}
+
+	printfPQExpBuffer(query, "EXECUTE getRelStats('%u')", dobj->catId.oid);
+
+	res = ExecuteSqlQueryForSingleRow(fout, query->data);
+
+	appendSetRelationStats(out, qualrelname, res, fout);
+
+	PQclear(res);
+
+	printfPQExpBuffer(query, "EXECUTE getAttrStats('%u')", dobj->catId.oid);
+
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+	appendSetAttributeStats(out, qualrelname, res, fout);
+
+	PQclear(res);
+
+	appendPQExpBuffer(tag, "%s %s", reltypename, fmtId(dobj->name));
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATISTICS DATA",
+							  .section = SECTION_NONE,
+							  .createStmt = out->data,
+							  .deps = &dumpid,
+							  .nDeps = 1));
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(out);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -16681,6 +16988,13 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 	if (tbinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
 		dumpTableSecLabel(fout, tbinfo, reltypename);
 
+	/* Statistics are dependent on the definition, not the data */
+	/* Views don't have stats */
+	if ((tbinfo->dobj.dump & DUMP_COMPONENT_STATISTICS) &&
+		(tbinfo->relkind != RELKIND_VIEW))
+		dumpRelationStats(fout, &tbinfo->dobj, reltypename,
+						  tbinfo->dobj.dumpId);
+
 	/* Dump comments on inlined table constraints */
 	for (j = 0; j < tbinfo->ncheck; j++)
 	{
@@ -16882,6 +17196,7 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 	PQExpBuffer delq;
 	char	   *qindxname;
 	char	   *qqindxname;
+	DumpId		dumpid;
 
 	/* Do nothing in data-only dump */
 	if (dopt->dataOnly)
@@ -16994,14 +17309,21 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 		free(indstatvalsarray);
 	}
 
+	/* Comments and stats share same .dep */
+	dumpid = is_constraint ? indxinfo->indexconstraint :
+		indxinfo->dobj.dumpId;
+
 	/* Dump Index Comments */
 	if (indxinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
 		dumpComment(fout, "INDEX", qindxname,
 					tbinfo->dobj.namespace->dobj.name,
 					tbinfo->rolname,
 					indxinfo->dobj.catId, 0,
-					is_constraint ? indxinfo->indexconstraint :
-					indxinfo->dobj.dumpId);
+					dumpid);
+
+	/* Dump Index Stats */
+	if (indxinfo->dobj.dump & DUMP_COMPONENT_STATISTICS)
+		dumpRelationStats(fout, &indxinfo->dobj, "INDEX", dumpid);
 
 	destroyPQExpBuffer(q);
 	destroyPQExpBuffer(delq);
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 9bc93520b4..d6a071ec28 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -101,6 +101,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 046c0dc3b3..69652aa205 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -105,6 +105,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -174,6 +175,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -453,6 +455,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -668,6 +672,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index c3beacdec1..2d326dec72 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -75,6 +75,7 @@ main(int argc, char **argv)
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
 	static int	no_subscriptions = 0;
+	static int  no_statistics = 0;
 	static int	strict_names = 0;
 
 	struct option cmdopts[] = {
@@ -126,6 +127,7 @@ main(int argc, char **argv)
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
+		{"no-statistics", no_argument, &no_statistics, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -358,6 +360,7 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
-- 
2.44.0

#83Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#82)
Re: Statistics Import and Export

On Fri, 2024-03-29 at 05:32 -0400, Corey Huinker wrote:

That is fairly close to what I came up with per our conversation
(attached below), but I really like the att_stats_arginfo construct
and I definitely want to adopt that and expand it to a third
dimension that flags the fields that cannot be null. I will
incorporate that into v15.

Sounds good. I think it cuts down on the boilerplate.

0002:
- All relstats and attrstats calls are now their own statement
instead of a compound statement
- moved the archive TOC entry from post-data back to SECTION_NONE (as
it was modeled on object COMMENTs), which seems to work better.
- remove meta-query in favor of more conventional query building
- removed all changes to fe_utils/

Can we get a consensus on whether the default should be with stats or
without? That seems like the most important thing remaining in the
pg_dump changes.

There's still a failure in the pg_upgrade TAP test. One seems to be
ordering, so perhaps we need to ORDER BY the attribute number. Others
seem to be missing relstats and I'm not sure why yet. I suggest doing
some manual pg_upgrade tests and comparing the before/after dumps to
see if you can reproduce a smaller version of the problem.

Regards,
Jeff Davis

#84Stephen Frost
sfrost@snowman.net
In reply to: Jeff Davis (#83)
Re: Statistics Import and Export

Greetings,

On Fri, Mar 29, 2024 at 11:05 Jeff Davis <pgsql@j-davis.com> wrote:

On Fri, 2024-03-29 at 05:32 -0400, Corey Huinker wrote:

0002:
- All relstats and attrstats calls are now their own statement
instead of a compound statement
- moved the archive TOC entry from post-data back to SECTION_NONE (as
it was modeled on object COMMENTs), which seems to work better.
- remove meta-query in favor of more conventional query building
- removed all changes to fe_utils/

Can we get a consensus on whether the default should be with stats or
without? That seems like the most important thing remaining in the
pg_dump changes.

I’d certainly think “with stats” would be the preferred default of our
users.

Thanks!

Stephen

#85Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#83)
Re: Statistics Import and Export

There's still a failure in the pg_upgrade TAP test. One seems to be
ordering, so perhaps we need to ORDER BY the attribute number. Others
seem to be missing relstats and I'm not sure why yet. I suggest doing
some manual pg_upgrade tests and comparing the before/after dumps to
see if you can reproduce a smaller version of the problem.

That's fixed in my current working version, as is a tsvector-specific
issue. Working on the TAP issue.

#86Jeff Davis
pgsql@j-davis.com
In reply to: Stephen Frost (#84)
Re: Statistics Import and Export

On Fri, 2024-03-29 at 18:02 -0400, Stephen Frost wrote:

I’d certainly think “with stats” would be the preferred default of
our users.

I'm concerned there could still be paths that lead to an error. For
pg_restore, or when loading a SQL file, a single error isn't fatal
(unless -e is specified), but it still could be somewhat scary to see
errors during a reload.

Also, it's new behavior, so it may cause some minor surprises, or there
might be minor interactions to work out. For instance, dumping stats
doesn't make a lot of sense if pg_upgrade (or something else) is just
going to run analyze anyway.

What do you think about starting off with it as non-default, and then
switching it to default in 18?

Regards,
Jeff Davis

#87Tom Lane
tgl@sss.pgh.pa.us
In reply to: Jeff Davis (#86)
Re: Statistics Import and Export

Jeff Davis <pgsql@j-davis.com> writes:

On Fri, 2024-03-29 at 18:02 -0400, Stephen Frost wrote:

I’d certainly think “with stats” would be the preferred default of
our users.

What do you think about starting off with it as non-default, and then
switching it to default in 18?

I'm with Stephen: I find it very hard to imagine that there's any
users who wouldn't want this as default. If we do what you suggest,
then there will be three historical behaviors to cope with not two.
That doesn't sound like it'll make anyone's life better.

As for the "it might break" argument, that could be leveled against
any nontrivial patch. You're at least offering an opt-out switch,
which is something we more often don't do.

(I've not read the patch yet, but I assume the switch works like
other pg_dump filters in that you can apply it on the restore
side?)

regards, tom lane

#88Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#86)
Re: Statistics Import and Export

On Fri, Mar 29, 2024 at 7:34 PM Jeff Davis <pgsql@j-davis.com> wrote:

On Fri, 2024-03-29 at 18:02 -0400, Stephen Frost wrote:

I’d certainly think “with stats” would be the preferred default of
our users.

I'm concerned there could still be paths that lead to an error. For
pg_restore, or when loading a SQL file, a single error isn't fatal
(unless -e is specified), but it still could be somewhat scary to see
errors during a reload.

To that end, I'm going to be modifying the "Optimizer statistics are not
transferred by pg_upgrade..." message when stats _were_ transferred,
width additional instructions that the user should treat any stats-ish
error messages encountered as a reason to manually analyze that table. We
should probably say something about extended stats as well.

#89Corey Huinker
corey.huinker@gmail.com
In reply to: Tom Lane (#87)
Re: Statistics Import and Export

(I've not read the patch yet, but I assume the switch works like
other pg_dump filters in that you can apply it on the restore
side?)

Correct. It follows the existing --no-something pattern.

#90Stephen Frost
sfrost@snowman.net
In reply to: Jeff Davis (#86)
Re: Statistics Import and Export

Greetings,

On Fri, Mar 29, 2024 at 19:35 Jeff Davis <pgsql@j-davis.com> wrote:

On Fri, 2024-03-29 at 18:02 -0400, Stephen Frost wrote:

I’d certainly think “with stats” would be the preferred default of
our users.

I'm concerned there could still be paths that lead to an error. For
pg_restore, or when loading a SQL file, a single error isn't fatal
(unless -e is specified), but it still could be somewhat scary to see
errors during a reload.

I understand that point.

Also, it's new behavior, so it may cause some minor surprises, or there

might be minor interactions to work out. For instance, dumping stats
doesn't make a lot of sense if pg_upgrade (or something else) is just
going to run analyze anyway.

But we don’t expect anything to run analyze … do we? So I’m not sure why
it makes sense to raise this as a concern.

What do you think about starting off with it as non-default, and then

switching it to default in 18?

What’s different, given the above arguments, in making the change with 18
instead of now? I also suspect that if we say “we will change the default
later” … that later won’t ever come and we will end up making our users
always have to remember to say “with-stats” instead.

The stats are important which is why the effort is being made in the first
place. If just doing an analyze after loading the data was good enough then
this wouldn’t be getting worked on.

Independently, I had a thought around doing an analyze as the data is being
loaded .. but we can’t do that for indexes (but we could perhaps analyze
the indexed values as we build the index..). This works when we do a
truncate or create the table in the same transaction, so we would tie into
some of the existing logic that we have around that. Would also adjust
COPY to accept an option that specifies the anticipated number of rows
being loaded (which we can figure out during the dump phase reasonably..).
Perhaps this would lead to a pg_dump option to do the data load as a
transaction with a truncate before the copy (point here being to be able to
still do parallel load while getting the benefits from knowing that we are
completely reloading the table). Just some other thoughts- which I don’t
intend to take away from the current effort at all, which I see as valuable
and should be enabled by default.

Thanks!

Stephen

Show quoted text
#91Jeff Davis
pgsql@j-davis.com
In reply to: Stephen Frost (#90)
Re: Statistics Import and Export

On Fri, 2024-03-29 at 20:54 -0400, Stephen Frost wrote:

What’s different, given the above arguments, in making the change
with 18 instead of now?

Acknowledged. You, Tom, and Corey (and perhaps everyone else) seem to
be aligned here, so that's consensus enough for me. Default is with
stats, --no-statistics to disable them.

Independently, I had a thought around doing an analyze as the data is
being loaded ..

Right, I think there are some interesting things to pursue here. I also
had an idea to use logical decoding to get a streaming sample, which
would be better randomness than block sampling. At this point that's
just an idea, I haven't looked into it seriously.

Regards,
Jeff Davis

#92Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#91)
2 attachment(s)
Re: Statistics Import and Export

Right, I think there are some interesting things to pursue here. I also
had an idea to use logical decoding to get a streaming sample, which
would be better randomness than block sampling. At this point that's
just an idea, I haven't looked into it seriously.

Regards,
Jeff Davis

v15 attached

0001:
- fixed an error involving tsvector types
- only derive element type if element stats available
- general cleanup

0002:

- 002pg_upgrade.pl now dumps before/after databases with --no-statistics. I
tried to find out why some tables were getting their relstats either not
set, or set and reset, never affecting the attribute stats. I even tried
turning autovacuum off for both instances, but nothing seemed to change the
fact that the same tables were having their relstats reset.

TODO list:

- decision on whether suppressing stats in the pg_upgrade TAP check is for
the best
- pg_upgrade option to suppress stats import, there is no real pattern to
follow there
- what message text to convey to the user about the potential stats import
errors and their remediation, and to what degree that replaces the "you
ought to run vacuumdb" message.
- what additional error context we want to add to the array_in() imports of
anyarray strings

Attachments:

v15-0001-Create-pg_set_relation_stats-pg_set_attribute_st.patchtext/x-patch; charset=US-ASCII; name=v15-0001-Create-pg_set_relation_stats-pg_set_attribute_st.patchDownload
From 210c45ac73361b33834aed5af2c90f3d53d345e7 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 21 Mar 2024 18:04:54 -0400
Subject: [PATCH v15 1/2] Create pg_set_relation_stats, pg_set_attribute_stats.

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

Both functions take an oid to identify the target relation that will
receive the statistics. There is nothing requiring that relation to be
the same one as the one exported, though the statistics have to make
sense in the context of the new relation.

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

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

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

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

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

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 07023ee61d..5dbb291410 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12192,4 +12192,19 @@
   proargtypes => 'int2',
   prosrc => 'gist_stratnum_identity' },
 
+# Statistics Import
+{ oid => '8048',
+  descr => 'set statistics on relation',
+  proname => 'pg_set_relation_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'void',
+  proargtypes => 'oid int4 float4 int4',
+  proargnames => '{relation,relpages,reltuples,relallvisible}',
+  prosrc => 'pg_set_relation_stats' },
+{ oid => '8049',
+  descr => 'set statistics on attribute',
+  proname => 'pg_set_attribute_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'void',
+  proargtypes => 'oid name bool float4 int4 float4 text _float4 text float4 text _float4 _float4 text float4 text',
+  proargnames => '{relation,attname,inherited,null_frac,avg_width,n_distinct,most_common_vals,most_common_freqs,histogram_bounds,correlation,most_common_elems,most_common_elem_freqs,elem_count_histogram,range_length_histogram,range_empty_frac,range_bounds_histogram}',
+  prosrc => 'pg_set_attribute_stats' },
 ]
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 7f2bf18716..1dddf96576 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -127,4 +127,6 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												int nclauses);
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
+extern Datum pg_set_relation_stats(PG_FUNCTION_ARGS);
+extern Datum pg_set_attribute_stats(PG_FUNCTION_ARGS);
 #endif							/* STATISTICS_H */
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index fe2bb50f46..22be7e6653 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -636,6 +636,24 @@ LANGUAGE INTERNAL
 CALLED ON NULL INPUT VOLATILE PARALLEL SAFE
 AS 'pg_stat_reset_slru';
 
+CREATE OR REPLACE FUNCTION
+  pg_set_attribute_stats(relation oid, attname name, inherited bool,
+                         null_frac real, avg_width integer, n_distinct real,
+                         most_common_vals text DEFAULT NULL,
+                         most_common_freqs real[] DEFAULT NULL,
+                         histogram_bounds text DEFAULT NULL,
+                         correlation real DEFAULT NULL,
+                         most_common_elems text DEFAULT NULL,
+                         most_common_elem_freqs real[] DEFAULT NULL,
+                         elem_count_histogram real[] DEFAULT NULL,
+                         range_length_histogram text DEFAULT NULL,
+                         range_empty_frac real DEFAULT NULL,
+                         range_bounds_histogram text DEFAULT NULL)
+RETURNS void
+LANGUAGE INTERNAL
+CALLED ON NULL INPUT VOLATILE
+AS 'pg_set_attribute_stats';
+
 --
 -- The default permissions for functions mean that anyone can execute them.
 -- A number of functions shouldn't be executable by just anyone, but rather
diff --git a/src/backend/statistics/Makefile b/src/backend/statistics/Makefile
index 89cf8c2797..e4f8ab7c4f 100644
--- a/src/backend/statistics/Makefile
+++ b/src/backend/statistics/Makefile
@@ -16,6 +16,7 @@ OBJS = \
 	dependencies.o \
 	extended_stats.o \
 	mcv.o \
-	mvdistinct.o
+	mvdistinct.o \
+	statistics.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/statistics/meson.build b/src/backend/statistics/meson.build
index 73b29a3d50..331e82c776 100644
--- a/src/backend/statistics/meson.build
+++ b/src/backend/statistics/meson.build
@@ -5,4 +5,5 @@ backend_sources += files(
   'extended_stats.c',
   'mcv.c',
   'mvdistinct.c',
+  'statistics.c',
 )
diff --git a/src/backend/statistics/statistics.c b/src/backend/statistics/statistics.c
new file mode 100644
index 0000000000..04868c587f
--- /dev/null
+++ b/src/backend/statistics/statistics.c
@@ -0,0 +1,1289 @@
+/*-------------------------------------------------------------------------
+ * statistics.c
+ *
+ *	  POSTGRES statistics import
+ *
+ * Code supporting the direct importation of relation statistics, similar to
+ * what is done by the ANALYZE command.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *       src/backend/statistics/statistics.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "catalog/pg_collation.h"
+#include "catalog/pg_database.h"
+#include "catalog/pg_operator.h"
+#include "catalog/pg_type.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_type.h"
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "nodes/nodeFuncs.h"
+#include "statistics/statistics.h"
+#include "utils/acl.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/float.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/rangetypes.h"
+#include "utils/syscache.h"
+#include "utils/typcache.h"
+
+/*
+ * A role has privileges to vacuum or analyze the relation if any of the
+ * following are true:
+ *   - the role owns the current database and the relation is not shared
+ *   - the role has the MAINTAIN privilege on the relation
+ *
+ */
+static bool
+can_modify_relation(Oid relid, Form_pg_class reltuple)
+{
+	return ((object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId())
+			 && !reltuple->relisshared) ||
+			pg_class_aclcheck(relid, GetUserId(), ACL_MAINTAIN) == ACLCHECK_OK);
+}
+
+/*
+ * A more encapsulated version of can_modify_relation for when the the
+ * HeapTuple and Form_pg_class are not needed later.
+ */
+static void
+check_relation_permissions(Relation rel)
+{
+	Oid			relid = RelationGetRelid(rel);
+	HeapTuple	ctup;
+	Form_pg_class pgcform;
+
+	/* Test existence of Relation */
+	ctup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+
+	if (!HeapTupleIsValid(ctup))
+		elog(ERROR, "cache lookup failed for relation %u", relid);
+
+	pgcform = (Form_pg_class) GETSTRUCT(ctup);
+
+	if (!can_modify_relation(relid, pgcform))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						RelationGetRelationName(rel))));
+
+	ReleaseSysCache(ctup);
+}
+
+/*
+ * Set statistics for a given pg_class entry.
+ *
+ * This does an in-place (i.e. non-transactional) update of pg_class, just as
+ * is done in ANALYZE.
+ *
+ */
+Datum
+pg_set_relation_stats(PG_FUNCTION_ARGS)
+{
+	/* Convenience enum to simulate naming the function arguments */
+	enum
+	{
+		P_RELATION = 0,			/* oid */
+		P_RELPAGES,				/* int */
+		P_RELTUPLES,			/* float4 */
+		P_RELALLVISIBLE,		/* int */
+		P_NUM_PARAMS
+	};
+
+	const char *param_names[] = {
+		"relation",
+		"relpages",
+		"reltuples",
+		"relallvisible"
+	};
+
+	Oid			relid;
+	Relation	rel;
+	HeapTuple	ctup;
+	Form_pg_class pgcform;
+	float4		reltuples;
+	int			relpages;
+	int			relallvisible;
+
+	/* Any NULL parameter is an error */
+	for (int i = P_RELATION; i < P_NUM_PARAMS; i++)
+		if (PG_ARGISNULL(i))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be NULL", param_names[i])));
+
+	relid = PG_GETARG_OID(P_RELATION);
+
+	/*
+	 * Open the relation, getting ShareUpdateExclusiveLock to ensure that no
+	 * other stat-setting operation can run on it concurrently.
+	 */
+	rel = table_open(RelationRelationId, ShareUpdateExclusiveLock);
+
+	ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(ctup))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_IN_USE),
+				 errmsg("pg_class entry for relid %u not found", relid)));
+
+	pgcform = (Form_pg_class) GETSTRUCT(ctup);
+
+	if (!can_modify_relation(relid, pgcform))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						RelationGetRelationName(rel))));
+
+
+	relpages = PG_GETARG_INT32(P_RELPAGES);
+	if (relpages < -1)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be < -1", param_names[P_RELPAGES])));
+	reltuples = PG_GETARG_FLOAT4(P_RELTUPLES);
+	if (reltuples < -1.0)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be < -1.0", param_names[P_RELTUPLES])));
+	relallvisible = PG_GETARG_INT32(P_RELALLVISIBLE);
+	if (relallvisible < -1)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be < -1", param_names[P_RELALLVISIBLE])));
+
+	/* Only update pg_class if there is a meaningful change */
+	if ((pgcform->reltuples != reltuples)
+		|| (pgcform->relpages != relpages)
+		|| (pgcform->relallvisible != relallvisible))
+	{
+		pgcform->relpages = PG_GETARG_INT32(P_RELPAGES);
+		pgcform->reltuples = PG_GETARG_FLOAT4(P_RELTUPLES);
+		pgcform->relallvisible = PG_GETARG_INT32(P_RELALLVISIBLE);
+
+		heap_inplace_update(rel, ctup);
+	}
+
+	table_close(rel, NoLock);
+
+	PG_RETURN_VOID();
+}
+
+
+
+/*
+ * Perform the cast of text to some array type
+ */
+static Datum
+cast_stavalues(FmgrInfo *finfo, Datum d, Oid typid, int32 typmod)
+{
+	char	   *s = TextDatumGetCString(d);
+	Datum		out = FunctionCall3(finfo, CStringGetDatum(s),
+									ObjectIdGetDatum(typid),
+									Int32GetDatum(typmod));
+
+	pfree(s);
+
+	return out;
+}
+
+/*
+ * Convenience routine to handle a common pattern where two function
+ * parameters must either both be NULL or both NOT NULL.
+ */
+static bool
+has_arg_pair(FunctionCallInfo fcinfo, const char **pnames, int p1, int p2)
+{
+	/* if on param is NULL and the other NOT NULL, report an error */
+	if (PG_ARGISNULL(p1) != PG_ARGISNULL(p2))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL when %s is NOT NULL",
+						pnames[(PG_ARGISNULL(p1)) ? p1 : p2],
+						pnames[(PG_ARGISNULL(p1)) ? p2 : p1])));
+
+	return (!PG_ARGISNULL(p1));
+}
+
+/*
+ * Test if the type is a scalar for MCELEM purposes
+ */
+static bool
+type_is_scalar(Oid typid)
+{
+	HeapTuple	tp;
+	bool		result = false;
+
+	tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+	if (HeapTupleIsValid(tp))
+	{
+		Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+
+		result = (!OidIsValid(typtup->typanalyze));
+		ReleaseSysCache(tp);
+	}
+	return result;
+}
+
+static int
+value_array_len(ExpandedArrayHeader *arr, const char *name)
+{
+	if (arr->ndims != 1)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be a multidimensional array", name)));
+
+	return arr->dims[0];
+}
+
+/*
+ * Convenience routine to encapsulate all of the steps needed for any
+ * value array.
+ */
+static int
+value_not_null_array_len(ExpandedArrayHeader *arr, const char *name)
+{
+	const int	nelems = value_array_len(arr, name);
+
+	if (nelems > 0)
+	{
+		deconstruct_expanded_array(arr);
+
+		/* if there's a nulls array, all values must be false */
+		if (arr->dnulls != NULL)
+			for (int i = 0; i < nelems; i++)
+				if (arr->dnulls[i])
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("%s array cannot contain NULL values", name)));
+	}
+
+	return nelems;
+}
+
+/*
+ * Fetch datatype information, this is needed to derive the proper staopN
+ * and stacollN values.
+ *
+ * If this relation is an index and that index has expressions in it, and
+ * the attnum specified is known to be an expression, then we must walk
+ * the list attributes up to the specified attnum to get the right
+ * expression.
+ *
+ */
+static TypeCacheEntry *
+get_attr_stat_type(Relation rel, Name attname,
+				   int16 *attnum, int32 *typmod, Oid *typcoll)
+{
+	Oid			relid = RelationGetRelid(rel);
+	Form_pg_attribute attr;
+	HeapTuple	atup;
+	Oid			typid;
+
+	atup = SearchSysCache2(ATTNAME, ObjectIdGetDatum(relid),
+						   NameGetDatum(attname));
+
+	/* Attribute not found */
+	if (!HeapTupleIsValid(atup))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s has no attname %s",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+
+	attr = (Form_pg_attribute) GETSTRUCT(atup);
+	if (attr->attisdropped)
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s attname %s is dropped",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+
+	*attnum = attr->attnum;
+
+	if ((rel->rd_rel->relkind == RELKIND_INDEX
+		 || (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+		&& (rel->rd_indexprs != NIL)
+		&& (rel->rd_index->indkey.values[attr->attnum - 1] == 0))
+	{
+		ListCell   *indexpr_item = list_head(rel->rd_indexprs);
+		Node	   *expr;
+
+		for (int i = 0; i < attr->attnum - 1; i++)
+			if (rel->rd_index->indkey.values[i] == 0)
+				indexpr_item = lnext(rel->rd_indexprs, indexpr_item);
+
+		if (indexpr_item == NULL)	/* shouldn't happen */
+			elog(ERROR, "too few entries in indexprs list");
+
+		expr = (Node *) lfirst(indexpr_item);
+
+		typid = exprType(expr);
+		*typmod = exprTypmod(expr);
+
+		/*
+		 * If a collation has been specified for the index column, use that in
+		 * preference to anything else; but if not, fall back to whatever we
+		 * can get from the expression.
+		 */
+		if (OidIsValid(attr->attcollation))
+			*typcoll = attr->attcollation;
+		else
+			*typcoll = exprCollation(expr);
+	}
+	else
+	{
+		typid = attr->atttypid;
+		*typmod = attr->atttypmod;
+		*typcoll = attr->attcollation;
+	}
+	ReleaseSysCache(atup);
+
+	/* if it's a multirange, step down to the range type */
+	if (type_is_multirange(typid))
+		typid = get_multirange_range(typid);
+
+	return lookup_type_cache(typid, 
+							 TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR);
+}
+
+
+/*
+ * The null_frac statistic must be in [0.0,1.0].
+ */
+static void
+validate_null_frac(float4 null_frac, const char *statname)
+{
+	const float4 min = 0.0;
+	const float4 max = 1.0;
+
+	if ((null_frac < min) || (null_frac > max))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s %f is out of range %.1f to %.1f",
+						statname, null_frac, min, max)));
+}
+
+/*
+ * The avg_width statistic must be non-negative.
+ */
+static void
+validate_avg_width(int32 avg_width, const char *statname)
+{
+	const int	min = 0;
+
+	if (avg_width < min)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s %d must be >= %d", statname, avg_width, min)));
+}
+
+/*
+ * The n_distinct statistic cannot be below -1.0.
+ */
+static void
+validate_n_distinct(float4 n_distinct, const char *statname)
+{
+	const float4 min = -1.0;
+
+	if (n_distinct < min)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s %f must be >= %.1f",
+						statname, n_distinct, min)));
+}
+
+/*
+ * Check correctness of MCV statistics pair.
+ *
+ * The length of the freqs array must be equal to the length of the values
+ * array. Neither array can contain NULL elements.
+ *
+ * The elements in the freqs array must be monotonically nondecreasing, and the
+ * sum of values in the array theoretically should not exceed 1.0, but we use a
+ * more relaxed limit to allow for compounded rounding errors.
+ */
+static void
+validate_mcv(Datum freqs, Datum values, const char *freqsname,
+			 const char *valuesname)
+{
+	ExpandedArrayHeader *freqsarr = DatumGetExpandedArray(freqs);
+	ExpandedArrayHeader *valsarr = DatumGetExpandedArray(values);
+
+	int			nvals = value_array_len(valsarr, valuesname);
+	int			nfreqs = value_not_null_array_len(freqsarr, freqsname);
+
+	if (nfreqs != nvals)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s has %d elements, but %s has %d elements, "
+						"but they must be equal",
+						freqsname, nfreqs, valuesname, nvals)));
+
+	/*
+	 * check that freqs sum to <= 1.0 or some number slightly higer to allow
+	 * for compounded rounding errors.
+	 */
+	if (nfreqs >= 1)
+	{
+		const float4 freqsummax = 1.1;
+
+		float4		prev = DatumGetFloat4(freqsarr->dvalues[0]);
+		float4		freqsum = prev;
+
+		for (int i = 1; i < nfreqs; i++)
+		{
+			float4		f = DatumGetFloat4(freqsarr->dvalues[i]);
+
+			if (f > prev)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s array values must be in descending "
+								"order, but %f > %f",
+								freqsname, f, prev)));
+
+			freqsum += f;
+			prev = f;
+		}
+
+		if (freqsum > freqsummax)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("The sum of elements in %s must not exceed "
+							"%.2f but is %f",
+							freqsname, freqsummax, freqsum)));
+	}
+}
+
+/*
+ * Check correctness of Histogram Bounds statistics.
+ *
+ * The array represents a histogram, which means that the values must be in
+ * monotonically non-decreasing order.
+ *
+ * If the attribute datatype in question uses collations then this validation
+ * has the chance to turn up any discrepancies in the source and destination
+ * collations if the datatype uses collations.
+ */
+static void
+validate_histogram_bounds(Datum stavalues, Datum srcstrvalue,
+						  Oid ltopr, Oid typcollation,
+						  const char *statname)
+{
+	ExpandedArrayHeader *arr = DatumGetExpandedArray(stavalues);
+	SortSupportData ssupd;
+
+	int			nelems = value_not_null_array_len(arr, statname);
+
+	memset(&ssupd, 0, sizeof(ssupd));
+	ssupd.ssup_cxt = CurrentMemoryContext;
+	ssupd.ssup_collation = typcollation;
+	ssupd.ssup_nulls_first = false;
+	ssupd.abbreviate = false;
+
+	PrepareSortSupportFromOrderingOp(ltopr, &ssupd);
+
+	/*
+	 * Array should be in * monotonically non-decreasing order. If we every
+	 * find a case where [n] > [n+1], raise an error.
+	 */
+	for (int i = 1; i < nelems; i++)
+	{
+		Datum		a = arr->dvalues[i - 1];
+		Datum		b = arr->dvalues[i];
+
+		if (ssupd.comparator(a, b, &ssupd) > 0)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s values must be in ascending order %s",
+							statname, TextDatumGetCString(srcstrvalue))));
+
+	}
+}
+
+/*
+ * Check correctness of Correlation statistics.
+ *
+ * Correlation must be in [-1.0,1,0].
+ */
+static void
+validate_correlation(float4 corr, const char *statname)
+{
+	const float4 min = -1.0;
+	const float4 max = 1.0;
+
+	if ((corr < min) || (corr > max))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s %f is out of range %.1f to %.1f",
+						statname, corr, min, max)));
+}
+
+/*
+ * Check correctness of Most Common Elements statistics.
+ *
+ * Neither array can contain NULL elements.
+ *
+ * The values array must be in monotonically incresing order. Elements may
+ * not repeat.
+ *
+ * Every element in the values array must have a corresponding element in the
+ * freqs array. The freqs array will have two additional elements, representing
+ * the frequency lower bound (LB) and frequency upper bound UB). The freqs
+ * array may have an optoinal extra value representing the null fraction of
+ * values in the column.
+ *
+ * The null fraction, LB and UB must be between [0.0,1.0].
+ *
+ * The LB must be <= the UB.
+ */
+static void
+validate_most_common_elements(Datum freqs, Datum elements, Datum srcstrvalue,
+							  const char *freqsname, const char *elemsname,
+							  Oid ltopr, Oid typcollation)
+{
+	ExpandedArrayHeader *freqsarr = DatumGetExpandedArray(freqs);
+	ExpandedArrayHeader *elemsarr = DatumGetExpandedArray(elements);
+
+	int			nfreqs = value_not_null_array_len(freqsarr, freqsname);
+	int			nelems = value_not_null_array_len(elemsarr, elemsname);
+
+	/*
+	 * The mcelem freqs array has either 2 or 3 additional values: the min
+	 * frequency, the max frequency, the optional null frequency.
+	 */
+	const int	nfreqsmin = nelems + 2;
+	const int	nfreqsmax = nelems + 3;
+
+	/*
+	 * the freqlowbound and freqhighbound must themselves be valid percentages
+	 */
+	const float4 frac_min = 0.0;
+	const float4 frac_max = 1.0;
+
+	float4		freqlowbound;
+	float4		freqhighbound;
+
+	if (nfreqs <= 0)
+		return;
+
+	if ((nfreqs < nfreqsmin) || (nfreqs > nfreqsmax))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s has %d elements, but must have between "
+						"%d and %d because %s has %d elements",
+						freqsname, nfreqs, nfreqsmin, nfreqsmax,
+						elemsname, nelems)));
+
+	/* first freq element past the length of the values is the min */
+	freqlowbound = DatumGetFloat4(freqsarr->dvalues[nelems]);
+	if ((freqlowbound < frac_min) || (freqlowbound > frac_max))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s %s frequency %f is out of "
+						"range %.1f to %.1f",
+						freqsname, "minimum", freqlowbound,
+						frac_min, frac_max)));
+
+	/* second freq element past the length of the values is the max */
+	freqhighbound = DatumGetFloat4(freqsarr->dvalues[nelems + 1]);
+	if ((freqhighbound < frac_min) || (freqhighbound > frac_max))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s %s frequency %f is out of "
+						"range %.1f to %.1f",
+						freqsname, "maximum", freqhighbound,
+						frac_min, frac_max)));
+
+	/* low bound must be < high bound */
+	if (freqlowbound > freqhighbound)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s frequency low bound %f cannot be greater "
+						"than high bound %f",
+						freqsname, freqlowbound, freqhighbound)));
+
+	/*
+	 * third freq element past the length of the values is the null frac
+	 */
+	if (nfreqs == nelems + 3)
+	{
+		float4		freqnullpct;
+
+		freqnullpct = DatumGetFloat4(freqsarr->dvalues[nelems + 2]);
+
+		if ((freqnullpct < frac_min) || (freqnullpct > frac_max))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s %s frequency %f is out of "
+							"range %.1f to %.1f",
+							freqsname, "null", freqnullpct,
+							frac_min, frac_max)));
+	}
+
+	/*
+	 * All the freqs that match up to a val must be between low/high bounds
+	 * (which is never less strict than frac_min/frac_max)
+	 *
+	 * Also, these frequencies do not sum to a number <= 1.0 as is the case
+	 * with MC_FREQS.
+	 */
+	for (int i = 0; i < nelems; i++)
+	{
+		float4		f = DatumGetFloat4(freqsarr->dvalues[i]);
+
+		if ((f < freqlowbound) || (f > freqhighbound))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s frequency %f is out of range %f to %f",
+							freqsname, f, freqlowbound, freqhighbound)));
+	}
+
+	/*
+	 * All the elements must be unique and in ascending order.
+	 */
+	if (nelems > 1)
+	{
+		SortSupportData ssupd;
+
+		memset(&ssupd, 0, sizeof(ssupd));
+		ssupd.ssup_cxt = CurrentMemoryContext;
+		ssupd.ssup_collation = typcollation;
+		ssupd.ssup_nulls_first = false;
+		ssupd.abbreviate = false;
+
+		PrepareSortSupportFromOrderingOp(ltopr, &ssupd);
+
+		for (int i = 1; i < nelems; i++)
+		{
+			Datum		a = elemsarr->dvalues[i - 1];
+			Datum		b = elemsarr->dvalues[i];
+
+			if (ssupd.comparator(a, b, &ssupd) >= 0)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s values must be unqiue and in ascending order %s",
+								elemsname, TextDatumGetCString(srcstrvalue))));
+
+		}
+	}
+}
+
+/*
+ * Check correctness of Distinct Elements Count Histogram statistics.
+ *
+ * All elements must be >= 0.0, and must be in monotonically nonincreasing
+ * order.
+ */
+static void
+validate_distinct_elements_count_histogram(Datum stanumbers,
+										   const char *statname)
+{
+	ExpandedArrayHeader *arr = DatumGetExpandedArray(stanumbers);
+
+	const float4 last_min = 0.0;
+
+	int			nelems = value_not_null_array_len(arr, statname);
+	float4		last;
+
+	/* Last element must be >= 0 */
+	last = DatumGetFloat4(arr->dvalues[nelems - 1]);
+	if (last < last_min)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s has last element %f < %.1f",
+						statname, last, last_min)));
+
+	/* all other elements must be monotonically nondecreasing */
+	if (nelems > 1)
+	{
+		float4		prev = DatumGetFloat4(arr->dvalues[0]);
+
+		for (int i = 1; i < nelems - 1; i++)
+		{
+			float4		f = DatumGetFloat4(arr->dvalues[i]);
+
+			if (f < prev)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s array values must be in ascending "
+								"order, but %f > %f",
+								statname, prev, f)));
+
+			prev = f;
+		}
+	}
+}
+
+/*
+ * Validate Range Bound Histogram statistic.
+ *
+ * The values in this array are ranges of the same scalar type as the attribute
+ * in question and cannot be empty. However those ranges are being used to
+ * represent two parallel arrays with inclusive/exclusive bounds, representing
+ * the histogram of lower bounds and the histogram of upper bounds. Each of
+ * those two arrays must each be monotonically nondecreasing.
+ */
+
+static void
+validate_bounds_histogram(Datum stavalues, Datum strvalue,
+						  TypeCacheEntry *typcache, const char *statname)
+{
+	ExpandedArrayHeader *arr = DatumGetExpandedArray(stavalues);
+
+	int			nelems = value_not_null_array_len(arr, statname);
+	RangeType  *prevrange;
+	RangeBound	prevlower;
+	RangeBound	prevupper;
+	bool		empty;
+
+	if (nelems <= 0)
+		return;
+
+	/* check first element for emptiness */
+	prevrange = DatumGetRangeTypeP(arr->dvalues[0]);
+	range_deserialize(typcache, prevrange, &prevlower,
+					  &prevupper, &empty);
+
+	if (empty)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s array bounds cannot have empty elements",
+						statname)));
+
+	/*
+	 * For element N, verify that: lower_bound(N) <= lower_bound(N+1), and
+	 * upper_bound(N) <= upper_bound(N+1)
+	 */
+	for (int i = 1; i < nelems; i++)
+	{
+		RangeType  *range = DatumGetRangeTypeP(arr->dvalues[i]);
+		RangeBound	lower;
+		RangeBound	upper;
+
+		range_deserialize(typcache, range, &lower, &upper, &empty);
+
+		if (empty)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s array bounds cannot have empty elements",
+							statname)));
+
+		if (range_cmp_bounds(typcache, &prevlower, &lower) == 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s array %s bounds must be in ascending order",
+							statname, "lower")));
+
+		if (range_cmp_bounds(typcache, &prevupper, &upper) == 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s array %s bounds must be in ascending order",
+							statname, "upper")));
+
+		memcpy(&lower, &prevlower, sizeof(RangeBound));
+		memcpy(&upper, &prevupper, sizeof(RangeBound));
+	}
+}
+
+/*
+ * Validate Range Length Histogram statistic pair.
+ *
+ * The empty_frac must be between in [0.0,1.0].
+ *
+ * rlhist is a histogram of float8[], so it must be in monotonically
+ * nondecreasing order.
+ */
+static void
+validate_range_length_histogram(Datum rlhist, float4 empty_frac,
+								const char *histname,
+								const char *fracname)
+{
+	ExpandedArrayHeader *arr = DatumGetExpandedArray(rlhist);
+
+	int			nelems = value_not_null_array_len(arr, histname);
+
+	const float4 min = 0.0;
+	const float4 max = 1.0;
+
+	if ((empty_frac < min) || (empty_frac > max))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s %f is out of range %.1f to %.1f",
+						fracname, empty_frac, min, max)));
+
+	if (nelems > 1)
+	{
+		float8		prev = DatumGetFloat8(arr->dvalues[0]);
+
+		for (int i = 1; i < nelems; i++)
+		{
+			float8		f = DatumGetFloat8(arr->dvalues[i]);
+
+			if (f < prev)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s array values must be in ascending "
+								"order, but %f > %f",
+								histname, prev, f)));
+
+			prev = f;
+		}
+	}
+}
+
+/*
+ * Update the pg_statistic record.
+ */
+static void
+update_pg_statistic(Datum values[], bool nulls[])
+{
+	Relation	sd = table_open(StatisticRelationId, RowExclusiveLock);
+	CatalogIndexState indstate = CatalogOpenIndexes(sd);
+	HeapTuple	oldtup;
+	HeapTuple	stup;
+
+	/* Is there already a pg_statistic tuple for this attribute? */
+	oldtup = SearchSysCache3(STATRELATTINH,
+							 values[Anum_pg_statistic_starelid - 1],
+							 values[Anum_pg_statistic_staattnum - 1],
+							 values[Anum_pg_statistic_stainherit - 1]);
+
+	if (HeapTupleIsValid(oldtup))
+	{
+		/* Yes, replace it */
+		bool		replaces[Natts_pg_statistic];
+
+		for (int i = 0; i < Natts_pg_statistic; i++)
+			replaces[i] = true;
+
+		stup = heap_modify_tuple(oldtup, RelationGetDescr(sd),
+								 values, nulls, replaces);
+		ReleaseSysCache(oldtup);
+		CatalogTupleUpdateWithInfo(sd, &stup->t_self, stup, indstate);
+	}
+	else
+	{
+		/* No, insert new tuple */
+		stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
+		CatalogTupleInsertWithInfo(sd, stup, indstate);
+	}
+
+	heap_freetuple(stup);
+	CatalogCloseIndexes(indstate);
+	table_close(sd, NoLock);
+}
+
+/*
+ * Import statistics for a given relation attribute.
+ *
+ * This will insert/replace a row in pg_statistic for the given relation and
+ * attribute name.
+ *
+ * The function takes input parameters that correspond to columns in the view
+ * pg_stats.
+ *
+ * Of those, the columns attname, inherited, null_frac, avg_width, and
+ * n_distinct all correspond to NOT NULL columns in pg_statistic. These
+ * parameters have no default value and passing NULL to them will result
+ * in an error.
+ *
+ * If there is no attribute with a matching attname in the relation, the
+ * function will raise an error. Likewise for setting inherited statistics
+ * on a table that is not partitioned.
+ *
+ * The remaining parameters all belong to a specific stakind. Some stakinds
+ * have multiple parameters, and in those cases both parameters must be
+ * NOT NULL or both NULL, otherwise an error will be raised.
+ *
+ * Omitting a parameter or explicitly passing NULL means that that particular
+ * stakind is not associated with the attribute.
+ *
+ * Parameters that are NOT NULL will be inspected for consistency checks,
+ * any of which can raise an error.
+ *
+ * Parameters corresponding to ANYARRAY columns are instead passed in as text
+ * values, which is a valid input string for an array of the type or element
+ * type of the attribute. Any error generated by the array_in() function will
+ * in turn fail the function.
+ */
+Datum
+pg_set_attribute_stats(PG_FUNCTION_ARGS)
+{
+	/* Convenience enum to simulate naming the function arguments */
+	enum
+	{
+		P_RELATION = 0,			/* oid */
+		P_ATTNAME,				/* name */
+		P_INHERITED,			/* bool */
+		P_NULL_FRAC,			/* float4 */
+		P_AVG_WIDTH,			/* int32 */
+		P_N_DISTINCT,			/* float4 */
+		P_MC_VALS,				/* text, null */
+		P_MC_FREQS,				/* float4[], null */
+		P_HIST_BOUNDS,			/* text, null */
+		P_CORRELATION,			/* float4, null */
+		P_MC_ELEMS,				/* text, null */
+		P_MC_ELEM_FREQS,		/* float4[], null */
+		P_ELEM_COUNT_HIST,		/* float4[], null */
+		P_RANGE_LENGTH_HIST,	/* text, null */
+		P_RANGE_EMPTY_FRAC,		/* float4, null */
+		P_RANGE_BOUNDS_HIST,	/* text, null */
+		P_NUM_PARAMS
+	};
+
+	/* names of arguments indexed by above enum */
+	const char *param_names[] = {
+		"relation",
+		"attname",
+		"inherited",
+		"null_frac",
+		"avg_width",
+		"n_distinct",
+		"most_common_vals",
+		"most_common_freqs",
+		"histogram_bounds",
+		"correlation",
+		"most_common_elems",
+		"most_common_elem_freqs",
+		"elem_count_histogram",
+		"range_length_histogram",
+		"range_empty_frac",
+		"range_bounds_histogram"
+	};
+
+	Oid			relid;
+	Name		attname;
+	Relation	rel;
+
+	TypeCacheEntry *typcache;
+	TypeCacheEntry *basetypcache;
+
+	int16		attnum;
+	int32		typmod;
+	Oid			typcoll;
+
+	Datum		values[Natts_pg_statistic] = {0};
+	bool		nulls[Natts_pg_statistic] = {false};
+
+	FmgrInfo	finfo;
+
+	bool		has_mcv;
+	bool		has_mc_elems;
+	bool		has_rl_hist;
+	int			stakind_count;
+
+	int			k = 0;
+
+	/*
+	 * A null in a required parameter is an error.
+	 */
+	for (int i = P_RELATION; i <= P_N_DISTINCT; i++)
+		if (PG_ARGISNULL(i))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be NULL", param_names[i])));
+
+	/*
+	 * Check all parameter pairs up front.
+	 */
+	has_mcv = has_arg_pair(fcinfo, param_names,
+						   P_MC_VALS, P_MC_FREQS);
+	has_mc_elems = has_arg_pair(fcinfo, param_names,
+								P_MC_ELEMS, P_MC_ELEM_FREQS);
+	has_rl_hist = has_arg_pair(fcinfo, param_names,
+							   P_RANGE_LENGTH_HIST, P_RANGE_EMPTY_FRAC);
+
+	/*
+	 * If a caller specifies more stakind-stats than we have slots to store
+	 * them, raise an error.
+	 */
+	stakind_count = (int) has_mcv + (int) has_mc_elems + (int) has_rl_hist +
+		(int) !PG_ARGISNULL(P_HIST_BOUNDS) +
+		(int) !PG_ARGISNULL(P_CORRELATION) +
+		(int) !PG_ARGISNULL(P_ELEM_COUNT_HIST) +
+		(int) !PG_ARGISNULL(P_RANGE_BOUNDS_HIST);
+
+	if (stakind_count > STATISTIC_NUM_SLOTS)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("imported statistics must have a maximum of %d slots "
+						"but %d given",
+						STATISTIC_NUM_SLOTS, stakind_count)));
+
+	relid = PG_GETARG_OID(P_RELATION);
+
+	rel = relation_open(relid, ShareUpdateExclusiveLock);
+
+	check_relation_permissions(rel);
+
+	attname = PG_GETARG_NAME(P_ATTNAME);
+	typcache = get_attr_stat_type(rel, attname, &attnum, &typmod, &typcoll);
+
+	/*
+	 * Derive base type if we have stats kinds that need it.
+	 *
+	 * This duplicates some type-specific logic found in various typanalyze
+	 * functions which are called from vacuum's examine_attribute(), but
+	 * using that directly has proven awkward.
+	 */
+	if (has_mc_elems || !PG_ARGISNULL(P_ELEM_COUNT_HIST))
+	{
+		Oid basetypid;
+
+		if (typcache->type_id == TSVECTOROID)
+		{
+			/* tsvectors always have a text oid base type and default collation */
+			basetypid = TEXTOID;
+			typcoll = DEFAULT_COLLATION_OID;
+		}
+		else if (typcache->typtype == TYPTYPE_RANGE)
+			basetypid = get_range_subtype(typcache->type_id);
+		else
+			basetypid = get_base_element_type(typcache->type_id);
+
+		/* not finding a basetype means we already had it */
+		if (basetypid == InvalidOid)
+			basetypid = typcache->type_id;
+
+		/* The stats need the eq_opr, but validation needs the lt_opr */
+		basetypcache = lookup_type_cache(basetypid,
+										 TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR);
+	}
+
+	/* P_HIST_BOUNDS and P_CORRELATION must have a type < operator */
+	if (typcache->lt_opr == InvalidOid)
+		for (int i = P_HIST_BOUNDS; i <= P_CORRELATION; i++)
+			if (!PG_ARGISNULL(i))
+				ereport(ERROR,
+						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+						 errmsg("Relation %s attname %s cannot "
+								"have stats of type %s",
+								RelationGetRelationName(rel),
+								NameStr(*attname),
+								param_names[i])));
+
+	/* Scalar types can't have P_MC_ELEMS, P_MC_ELEM_FREQS, P_ELEM_COUNT_HIST */
+	if (type_is_scalar(typcache->type_id))
+		for (int i = P_MC_ELEMS; i <= P_ELEM_COUNT_HIST; i++)
+			if (!PG_ARGISNULL(i))
+				ereport(ERROR,
+						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+						 errmsg("Relation %s attname %s is a scalar type, "
+								"cannot have stats of type %s",
+								RelationGetRelationName(rel),
+								NameStr(*attname),
+								param_names[i])));
+
+	/* Only range types can have P_RANGE_x */
+	if ((typcache->typtype != TYPTYPE_RANGE) &&
+		(typcache->typtype != TYPTYPE_RANGE))
+		for (int i = P_RANGE_LENGTH_HIST; i <= P_RANGE_BOUNDS_HIST; i++)
+			if (!PG_ARGISNULL(i))
+				ereport(ERROR,
+						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+						 errmsg("Relation %s attname %s is not a range type, "
+								"cannot have stats of type %s",
+								RelationGetRelationName(rel),
+								NameStr(*attname),
+								param_names[i])));
+
+	validate_null_frac(PG_GETARG_FLOAT4(P_NULL_FRAC),
+					   param_names[P_NULL_FRAC]);
+	validate_avg_width(PG_GETARG_INT32(P_AVG_WIDTH),
+					   param_names[P_AVG_WIDTH]);
+	validate_n_distinct(PG_GETARG_FLOAT4(P_N_DISTINCT),
+						param_names[P_N_DISTINCT]);
+	values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(relid);
+	values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(attnum);
+	values[Anum_pg_statistic_stainherit - 1] = PG_GETARG_DATUM(P_INHERITED);
+	values[Anum_pg_statistic_stanullfrac - 1] = PG_GETARG_DATUM(P_NULL_FRAC);
+	values[Anum_pg_statistic_stawidth - 1] = PG_GETARG_DATUM(P_AVG_WIDTH);
+	values[Anum_pg_statistic_stadistinct - 1] = PG_GETARG_DATUM(P_N_DISTINCT);
+
+	fmgr_info(F_ARRAY_IN, &finfo);
+
+	/* MC_VALS && MC_FREQS => STATISTIC_KIND_MCV */
+	if (has_mcv)
+	{
+		Datum		stanumbers = PG_GETARG_DATUM(P_MC_FREQS);
+		Datum		stavalues = cast_stavalues(&finfo, PG_GETARG_DATUM(P_MC_VALS),
+											   typcache->type_id, typmod);
+
+		validate_mcv(stanumbers, stavalues, param_names[P_MC_FREQS],
+					 param_names[P_MC_VALS]);
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_MCV);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(typcache->eq_opr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+		values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+		k++;
+	}
+
+	/* HIST_BOUNDS => STATISTIC_KIND_HISTOGRAM */
+	if (!PG_ARGISNULL(P_HIST_BOUNDS))
+	{
+		Datum		strvalue = PG_GETARG_DATUM(P_HIST_BOUNDS);
+		Datum		stavalues = cast_stavalues(&finfo, strvalue,
+											   typcache->type_id, typmod);
+
+		validate_histogram_bounds(stavalues, strvalue, typcache->lt_opr, typcoll,
+								  param_names[P_HIST_BOUNDS]);
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_HISTOGRAM);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(typcache->lt_opr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+		k++;
+	}
+
+	/* CORRELATION => STATISTIC_KIND_CORRELATION */
+	if (!PG_ARGISNULL(P_CORRELATION))
+	{
+		Datum		elem = PG_GETARG_DATUM(P_CORRELATION);
+		Datum		elems[] = {elem};
+		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+		Datum		stanumbers = PointerGetDatum(arry);
+
+		validate_correlation(DatumGetFloat4(elem), param_names[P_CORRELATION]);
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_CORRELATION);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(typcache->lt_opr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+		k++;
+	}
+
+	/* MC_ELEMS && MC_ELEM_FREQS => STATISTIC_KIND_MCELEM */
+	if (has_mc_elems)
+	{
+		Datum		srcstrvalue = PG_GETARG_DATUM(P_MC_ELEMS);
+		Datum		stanumbers = PG_GETARG_DATUM(P_MC_ELEM_FREQS);
+		Datum		stavalues = cast_stavalues(&finfo, srcstrvalue,
+											   basetypcache->type_id, typmod);
+
+		validate_most_common_elements(stanumbers, stavalues, srcstrvalue,
+									  param_names[P_MC_ELEM_FREQS],
+									  param_names[P_MC_ELEMS],
+									  basetypcache->lt_opr, typcoll);
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_MCELEM);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(basetypcache->eq_opr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+		values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+		k++;
+	}
+
+	/* ELEM_COUNT_HIST => STATISTIC_KIND_DECHIST */
+	if (!PG_ARGISNULL(P_ELEM_COUNT_HIST))
+	{
+		Datum		stanumbers = PG_GETARG_DATUM(P_ELEM_COUNT_HIST);
+
+		validate_distinct_elements_count_histogram(stanumbers,
+												   param_names[P_ELEM_COUNT_HIST]);
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_DECHIST);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(basetypcache->eq_opr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+		k++;
+	}
+
+	/*
+	 * RANGE_BOUNDS_HIST => STATISTIC_KIND_BOUNDS_HISTOGRAM
+	 *
+	 * This stakind appears before STATISTIC_KIND_BOUNDS_HISTOGRAM even though
+	 * it is numerically greater, and all other stakinds appear in numerical
+	 * order. We duplicate this quirk to make before/after tests of
+	 * pg_statistic records easier.
+	 */
+	if (!PG_ARGISNULL(P_RANGE_BOUNDS_HIST))
+	{
+		Datum		strvalue = PG_GETARG_DATUM(P_RANGE_BOUNDS_HIST);
+		Datum		stavalues = cast_stavalues(&finfo, strvalue,
+											   typcache->type_id, typmod);
+
+		validate_bounds_histogram(stavalues, strvalue, typcache,
+								  param_names[P_RANGE_BOUNDS_HIST]);
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_BOUNDS_HISTOGRAM);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+		k++;
+	}
+
+	/*
+	 * P_RANGE_LENGTH_HIST && P_RANGE_EMPTY_FRAC =>
+	 * STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM
+	 */
+	if (has_rl_hist)
+	{
+		Datum		strvalue = PG_GETARG_DATUM(P_RANGE_LENGTH_HIST);
+
+		/* The anyarray is always a float8[] for this stakind */
+		Datum		stavalues = cast_stavalues(&finfo, strvalue, FLOAT8OID, 0);
+		Datum		elem = PG_GETARG_DATUM(P_RANGE_EMPTY_FRAC);
+		Datum		elems[] = {elem};
+		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+
+		validate_range_length_histogram(stavalues,
+										DatumGetFloat4(elem),
+										param_names[P_RANGE_LENGTH_HIST],
+										param_names[P_RANGE_EMPTY_FRAC]);
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(Float8LessOperator);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] = PointerGetDatum(arry);
+		values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+		k++;
+	}
+
+	/* fill in all remaining slots */
+	for (; k < STATISTIC_NUM_SLOTS; k++)
+	{
+		values[Anum_pg_statistic_stakind1 - 1 + k] = Int16GetDatum(0);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+	}
+
+	update_pg_statistic(values, nulls);
+
+	relation_close(rel, NoLock);
+	PG_RETURN_VOID();
+}
diff --git a/src/test/regress/expected/stats_export_import.out b/src/test/regress/expected/stats_export_import.out
new file mode 100644
index 0000000000..482f1eb2a3
--- /dev/null
+++ b/src/test/regress/expected/stats_export_import.out
@@ -0,0 +1,942 @@
+CREATE SCHEMA stats_export_import;
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+        0 |        -1 |             0
+(1 row)
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass, 999, 3.6::real, 15000);
+ pg_set_relation_stats 
+-----------------------
+ 
+(1 row)
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+      999 |       3.6 |         15000
+(1 row)
+
+-- error: relpages null
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass, NULL, 3.6::real, 15000);
+ERROR:  relpages cannot be NULL
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+      999 |       3.6 |         15000
+(1 row)
+
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => '0'::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  could not open relation with OID 0
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => NULL::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  relation cannot be NULL
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => NULL::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  attname cannot be NULL
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => NULL::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  inherited cannot be NULL
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => NULL::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  null_frac cannot be NULL
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => NULL::integer,
+    n_distinct => 0.3::real);
+ERROR:  avg_width cannot be NULL
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => NULL::real);
+ERROR:  n_distinct cannot be NULL
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.1 |         2 |        0.3 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- error: null_frac < 0
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => -0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  null_frac -0.100000 is out of range 0.0 to 1.0
+-- error: null_frac > 1
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 1.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  null_frac 1.100000 is out of range 0.0 to 1.0
+-- error: avg_width < 0
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => -1::integer,
+    n_distinct => 0.3::real);
+ERROR:  avg_width -1 must be >= 0
+-- error: n_distinct < -1
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -1.1::real);
+ERROR:  n_distinct -1.100000 must be >= -1.0
+-- error: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => NULL::text,
+    most_common_freqs => '{0.1,0.2,0.3}'::real[]
+    );
+ERROR:  most_common_vals cannot be NULL when most_common_freqs is NOT NULL
+-- error: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{1,2,3}'::text,
+    most_common_freqs => NULL::real[]
+    );
+ERROR:  most_common_freqs cannot be NULL when most_common_vals is NOT NULL
+-- error: mcv / mcf length mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.2,0.1}'::real[]
+    );
+ERROR:  most_common_freqs has 2 elements, but most_common_vals has 3 elements, but they must be equal
+-- error: mcf sum bad
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.6,0.5,0.3}'::real[]
+    );
+ERROR:  The sum of elements in most_common_freqs must not exceed 1.10 but is 1.400000
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- error: histogram elements null value
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,NULL,3,4}'::text
+    );
+ERROR:  histogram_bounds array cannot contain NULL values
+-- error: histogram elements must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,20,3,4}'::text
+    );
+ERROR:  histogram_bounds values must be in ascending order {1,20,3,4}
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,2,3,4}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- error: correlation low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => -1.1::real);
+ERROR:  correlation -1.100000 is out of range -1.0 to 1.0
+-- error: correlation high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 1.1::real);
+ERROR:  correlation 1.100000 is out of range -1.0 to 1.0
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 0.5::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |         0.5 |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- error: scalars can't have mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{1,3}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+ERROR:  Relation test attname id is a scalar type, cannot have stats of type most_common_elems
+-- error: mcelem / mcelem null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,two}'::text,
+    most_common_elem_freqs => NULL::real[]
+    );
+ERROR:  most_common_elem_freqs cannot be NULL when most_common_elems is NOT NULL
+-- error: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => NULL::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
+    );
+ERROR:  most_common_elems cannot be NULL when most_common_elem_freqs is NOT NULL
+-- error: mcelem / mcelem length mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2}'::real[]
+    );
+ERROR:  most_common_elem_freqs has 2 elements, but must have between 4 and 5 because most_common_elems has 2 elements
+-- error: mcelem freq element out of bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.1,0.2,0.3,0.0}'::real[]
+    );
+ERROR:  most_common_elem_freqs frequency 0.100000 is out of range 0.200000 to 0.300000
+-- error: mcelem freq low-high mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.4,0.3,0.0}'::real[]
+    );
+ERROR:  most_common_elem_freqs frequency low bound 0.400000 cannot be greater than high bound 0.300000
+-- error: mcelem freq null pct invalid
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,-0.0001}'::real[]
+    );
+ERROR:  most_common_elem_freqs null frequency -0.000100 is out of range 0.0 to 1.0
+-- error: mcelem freq bad low bound low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,-0.15,0.3,0.1}'::real[]
+    );
+ERROR:  most_common_elem_freqs minimum frequency -0.150000 is out of range 0.0 to 1.0
+-- error: mcelem freq bad low bound high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,1.5,0.3,0.1}'::real[]
+    );
+ERROR:  most_common_elem_freqs minimum frequency 1.500000 is out of range 0.0 to 1.0
+-- error: mcelem freq bad high bound low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.4,-0.3,0.1}'::real[]
+    );
+ERROR:  most_common_elem_freqs maximum frequency -0.300000 is out of range 0.0 to 1.0
+-- error: mcelem freq bad high bound high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.4,3.0,0.1}'::real[]
+    );
+ERROR:  most_common_elem_freqs maximum frequency 3.000000 is out of range 0.0 to 1.0
+-- error mcelem values must be monotonically increasing
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.3,0.2,0.3,0.0}'::real[]
+    );
+ERROR:  most_common_elems values must be unqiue and in ascending order {three,one}
+-- error mcelem values must be unique
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three,three}'::text,
+    most_common_elem_freqs => '{0.3,0.4,0.4,0.2,0.5,0.0}'::real[]
+    );
+ERROR:  most_common_elems values must be unqiue and in ascending order {one,three,three}
+-- ok: mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             | {one,three}       | {0.3,0.2,0.2,0.3,0}    |                      |                        |                  | 
+(1 row)
+
+-- error: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ERROR:  Relation test attname id is a scalar type, cannot have stats of type elem_count_histogram
+-- error: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ERROR:  elem_count_histogram array cannot contain NULL values
+-- error: elem_count_histogram must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ERROR:  elem_count_histogram array values must be in ascending order, but 1.000000 > 0.000000
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+(1 row)
+
+-- error: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ERROR:  Relation test attname id is not a range type, cannot have stats of type range_length_histogram
+-- error: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ERROR:  range_empty_frac cannot be NULL when range_length_histogram is NOT NULL
+-- error: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real
+    );
+ERROR:  range_length_histogram cannot be NULL when range_empty_frac is NOT NULL
+-- error: range_empty_frac low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => -0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ERROR:  range_empty_frac -0.500000 is out of range 0.0 to 1.0
+-- error: range_empty_frac high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 1.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ERROR:  range_empty_frac 1.500000 is out of range 0.0 to 1.0
+-- error: range_length_histogram not ascending
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,Infinity,499}'::text
+    );
+ERROR:  range_length_histogram array values must be in ascending order, but Infinity > 499.000000
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
+(1 row)
+
+-- error: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ERROR:  Relation test attname id is not a range type, cannot have stats of type range_bounds_histogram
+-- error: range_bound_hist low bounds must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[-2,4)","[1,4)","[1,100)"}'::text
+    );
+ERROR:  range_bounds_histogram array lower bounds must be in ascending order
+-- error: range_bound_hist high bounds must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,11)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ERROR:  range_bounds_histogram array upper bounds must be in ascending order
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
+ stats_export_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      |                        |                  | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+(1 row)
+
+-- error: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[],
+    histogram_bounds => '{1,2,3,4}'::text,
+    correlation => 1.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    range_empty_frac => -0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ERROR:  imported statistics must have a maximum of 5 slots but 6 given
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+   relname    | num_stats 
+--------------+-----------
+ is_odd       |         1
+ is_odd_clone |         1
+ test         |         5
+ test_clone   |         5
+(4 rows)
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 5ac6e871f5..a7a4dfd411 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,8 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc stats_export_import
+
 
 # ----------
 # Load huge amounts of data
diff --git a/src/test/regress/sql/stats_export_import.sql b/src/test/regress/sql/stats_export_import.sql
new file mode 100644
index 0000000000..1a7d02a2c7
--- /dev/null
+++ b/src/test/regress/sql/stats_export_import.sql
@@ -0,0 +1,836 @@
+CREATE SCHEMA stats_export_import;
+
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass, 999, 3.6::real, 15000);
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+-- error: relpages null
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass, NULL, 3.6::real, 15000);
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => '0'::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => NULL::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => NULL::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => NULL::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => NULL::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => NULL::integer,
+    n_distinct => 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => NULL::real);
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- error: null_frac < 0
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => -0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: null_frac > 1
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 1.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: avg_width < 0
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => -1::integer,
+    n_distinct => 0.3::real);
+
+-- error: n_distinct < -1
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -1.1::real);
+
+-- error: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => NULL::text,
+    most_common_freqs => '{0.1,0.2,0.3}'::real[]
+    );
+
+-- error: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{1,2,3}'::text,
+    most_common_freqs => NULL::real[]
+    );
+
+-- error: mcv / mcf length mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.2,0.1}'::real[]
+    );
+
+-- error: mcf sum bad
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.6,0.5,0.3}'::real[]
+    );
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- error: histogram elements null value
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,NULL,3,4}'::text
+    );
+-- error: histogram elements must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,20,3,4}'::text
+    );
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,2,3,4}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- error: correlation low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => -1.1::real);
+
+-- error: correlation high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 1.1::real);
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 0.5::real);
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- error: scalars can't have mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{1,3}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+-- error: mcelem / mcelem null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,two}'::text,
+    most_common_elem_freqs => NULL::real[]
+    );
+
+-- error: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => NULL::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
+    );
+
+-- error: mcelem / mcelem length mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2}'::real[]
+    );
+
+-- error: mcelem freq element out of bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.1,0.2,0.3,0.0}'::real[]
+    );
+
+-- error: mcelem freq low-high mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.4,0.3,0.0}'::real[]
+    );
+
+-- error: mcelem freq null pct invalid
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,-0.0001}'::real[]
+    );
+
+-- error: mcelem freq bad low bound low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,-0.15,0.3,0.1}'::real[]
+    );
+
+-- error: mcelem freq bad low bound high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,1.5,0.3,0.1}'::real[]
+    );
+
+-- error: mcelem freq bad high bound low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.4,-0.3,0.1}'::real[]
+    );
+
+-- error: mcelem freq bad high bound high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.4,3.0,0.1}'::real[]
+    );
+
+-- error mcelem values must be monotonically increasing
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.3,0.2,0.3,0.0}'::real[]
+    );
+-- error mcelem values must be unique
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three,three}'::text,
+    most_common_elem_freqs => '{0.3,0.4,0.4,0.2,0.5,0.0}'::real[]
+    );
+
+-- ok: mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- error: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- error: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- error: elem_count_histogram must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- error: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+-- error: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+-- error: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real
+    );
+-- error: range_empty_frac low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => -0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+-- error: range_empty_frac high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 1.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+-- error: range_length_histogram not ascending
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,Infinity,499}'::text
+    );
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- error: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+-- error: range_bound_hist low bounds must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[-2,4)","[1,4)","[1,100)"}'::text
+    );
+-- error: range_bound_hist high bounds must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,11)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- error: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[],
+    histogram_bounds => '{1,2,3,4}'::text,
+    correlation => 1.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    range_empty_frac => -0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+
+SELECT
+    format('SELECT pg_catalog.pg_set_attribute_stats( '
+            || 'relation => %L::regclass::oid, attname => %L::name, '
+            || 'inherited => %L::boolean, null_frac => %L::real, '
+            || 'avg_width => %L::integer, n_distinct => %L::real, '
+            || 'most_common_vals => %L::text, '
+            || 'most_common_freqs => %L::real[], '
+            || 'histogram_bounds => %L::text, '
+            || 'correlation => %L::real, '
+            || 'most_common_elems => %L::text, '
+            || 'most_common_elem_freqs => %L::real[], '
+            || 'elem_count_histogram => %L::real[], '
+            || 'range_length_histogram => %L::text, '
+            || 'range_empty_frac => %L::real, '
+            || 'range_bounds_histogram => %L::text) ',
+        'stats_export_import.' || s.tablename || '_clone', s.attname,
+        s.inherited, s.null_frac,
+        s.avg_width, s.n_distinct,
+        s.most_common_vals, s.most_common_freqs, s.histogram_bounds,
+        s.correlation, s.most_common_elems, s.most_common_elem_freqs,
+        s.elem_count_histogram, s.range_length_histogram,
+        s.range_empty_frac, s.range_bounds_histogram)
+FROM pg_catalog.pg_stats AS s
+WHERE s.schemaname = 'stats_export_import'
+AND s.tablename IN ('test', 'is_odd')
+\gexec
+
+-- restore ECHO to original value
+\set ECHO :orig_ECHO
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 93b0bc2bc6..153d0dc6ac 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29123,6 +29123,128 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
     in identifying the specific disk files associated with database objects.
    </para>
 
+   <table id="functions-admin-statsimport">
+    <title>Database Object Statistics Import Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_relation_stats</primary>
+        </indexterm>
+        <function>pg_set_relation_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>relpages</parameter> <type>integer</type>,
+         <parameter>reltuples</parameter> <type>real</type>,
+         <parameter>relallvisible</parameter> <type>integer</type> )
+        <returnvalue>void</returnvalue>
+       </para>
+       <para>
+        Updates the <structname>pg_class</structname> row for the specified
+        <parameter>relation</parameter>, setting the values for the columns
+        <structfield>reltuples</structfield>,
+        <structfield>relpages</structfield>, and
+        <structfield>relallvisible</structfield>.
+        To avoid table bloat in <structname>pg_class</structname>, this change
+        is made with an in-place update, and therefore cannot be rolled back
+        through normal transaction processing.
+       </para>
+       <para>
+        This function mimics the behavior of <command>ANALYZE</command> in its
+        effects on the values in <structname>pg_class</structname>, except that
+        the values are supplied as parameters rather than derived from table
+        sampling.
+       </para>
+       <para>
+        The caller must either be the owner of the relation, or have superuser
+        privileges.
+       </para>
+       <para>
+        The parameters supplied must all be NOT NULL. The value of
+        <structfield>relpages</structfield> must not be less than 0.  The
+        value of <structfield>reltuples</structfield> must not be less than
+        -1.0.  The value of <structfield>relallvisible</structfield> must not
+        be less than 0.
+       </para>
+       </entry>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_attribute_stats</primary>
+        </indexterm>
+        <function>pg_set_attribute_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>
+         , <parameter>attname</parameter> <type>name</type>
+         , <parameter>inherited</parameter> <type>boolean</type>
+         , <parameter>null_frac</parameter> <type>real</type>
+         , <parameter>avg_width</parameter> <type>integer</type>
+         , <parameter>n_distinct</parameter> <type>real</type>
+         [, <parameter>most_common_vals</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_freqs</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>histogram_bounds</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>correlation</parameter> <type>real</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_elems</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_elem_freqs</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>elem_count_histogram</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_length_histogram</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_empty_frac</parameter> <type>real</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_bounds_histogram</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ] )
+        <returnvalue>void</returnvalue>
+       </para>
+       <para>
+        Replaces the <structname>pg_statistic</structname> row for the
+        <structname>pg_attribute</structname> row specified by
+        <parameter>relation</parameter>, <parameter>attname</parameter>
+        and <parameter>inherited</parameter>. The remaining parameters
+        all correspond to attributes of the same name found in
+        <link linkend="view-pg-stats"><structname>pg_stats</structname></link>,
+        and the values supplied in the parameter must meet the requirements of
+        the corresponding attribute.
+       </para>
+       <para>
+        This function mimics the behavior of <command>ANALYZE</command> in its
+        effects on the values in <structname>pg_statistic</structname>, except
+        that the values are supplied as parameters rather than derived from
+        table sampling.
+       </para>
+       <para>
+        The purpose of this function is to apply statistics values in an
+        upgrade situation that are "good enough" for system operation until
+        they are replaced by the next <command>ANALYZE</command>, usually via
+        <command>autovacuum</command> This function is used by
+        <command>pg_upgrade</command> and <command>pg_restore</command> to
+        convey the statistics from the old system version into the new one.
+       </para>
+       <para>
+        The caller must either be the owner of the relation, or have superuser
+        privileges.
+       </para>
+       </entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
    <table id="functions-admin-dblocation">
     <title>Database Object Location Functions</title>
     <tgroup cols="1">
-- 
2.44.0

v15-0002-Enable-dumping-of-table-index-stats-in-pg_dump.patchtext/x-patch; charset=US-ASCII; name=v15-0002-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From b86fa7d7c3c031c7dd5f7b42e34ff4689b7ac62b Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH v15 2/2] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will generate a statement that
calls pg_set_relation_stats(), and it will generate a series of
statements that call pg_set_attribute_stats(), one per attribute. These
statements will restore the statistics of the current system onto the
destination system.

As is the pattern with pg_dump options, this can be disabled with
--no-statistics.
---
 src/bin/pg_dump/pg_backup.h            |   2 +
 src/bin/pg_dump/pg_backup_archiver.c   |   5 +
 src/bin/pg_dump/pg_dump.c              | 289 ++++++++++++++++++++++++-
 src/bin/pg_dump/pg_dump.h              |   1 +
 src/bin/pg_dump/pg_dumpall.c           |   5 +
 src/bin/pg_dump/pg_restore.c           |   3 +
 src/bin/pg_upgrade/t/002_pg_upgrade.pl |   4 +-
 7 files changed, 305 insertions(+), 4 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 9ef2f2017e..1db5cf52eb 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -112,6 +112,7 @@ typedef struct _restoreOptions
 	int			no_publications;	/* Skip publication entries */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
@@ -179,6 +180,7 @@ typedef struct _dumpOptions
 	int			no_security_labels;
 	int			no_publications;
 	int			no_subscriptions;
+	int			no_statistics;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index d97ebaff5b..d5f61399d9 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2833,6 +2833,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's a stats dump, maybe ignore it */
+	if (ropt->no_statistics && strcmp(te->desc, "STATISTICS") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2862,6 +2866,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index b1c4c3ec7f..f7ba76217c 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -428,6 +428,7 @@ main(int argc, char **argv)
 		{"no-comments", no_argument, &dopt.no_comments, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -1144,6 +1145,7 @@ help(const char *progname)
 	printf(_("  --no-comments                do not dump comments\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -7001,6 +7003,7 @@ getTables(Archive *fout, int *numTables)
 
 		/* Tables have data */
 		tblinfo[i].dobj.components |= DUMP_COMPONENT_DATA;
+		tblinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
 
 		/* Mark whether table has an ACL */
 		if (!PQgetisnull(res, i, i_relacl))
@@ -7498,6 +7501,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -10247,6 +10251,272 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * Tabular description of the parameters to pg_set_relation_stats()
+ * required, param_name, param_type
+ */
+static const char *rel_stats_arginfo[][3] = {
+	{"t", "relation", "regclass"},
+	{"t", "relpages", "integer"},
+	{"t", "reltuples", "real"},
+	{"t", "relallvisible", "integer"},
+};
+
+/*
+ * Tabular description of the parameters to pg_set_attribute_stats()
+ * required, param_name, param_type
+ */
+static const char *att_stats_arginfo[][3] = {
+	{"t", "relation", "regclass"},
+	{"t", "attname", "name"},
+	{"t", "inherited", "boolean"},
+	{"t", "null_frac", "float4"},
+	{"t", "avg_width", "integer"},
+	{"t", "n_distinct", "float4"},
+	{"f", "most_common_vals", "text"},
+	{"f", "most_common_freqs", "float4[]"},
+	{"f", "histogram_bounds", "text"},
+	{"f", "correlation", "float4"},
+	{"f", "most_common_elems", "text"},
+	{"f", "most_common_elem_freqs", "float4[]"},
+	{"f", "elem_count_histogram", "float4[]"},
+	{"f", "range_length_histogram", "text"},
+	{"f", "range_empty_frac", "float4"},
+	{"f", "range_bounds_histogram", "text"},
+};
+
+/*
+ * getRelStatsExportQuery --
+ *
+ * Generate a query that will fetch all relation (e.g. pg_class)
+ * stats for a given relation.
+ */
+static void
+getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, c.relpages, "
+						 "c.reltuples, c.relallvisible "
+						 "FROM pg_class c "
+						 "JOIN pg_namespace n "
+						 "ON n.oid = c.relnamespace "
+						 "WHERE n.nspname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND c.relname = ");
+	appendStringLiteralAH(query, relname, fout);
+}
+
+/*
+ * getAttStatsExportQuery --
+ *
+ * Generate a query that will fetch all attribute (e.g. pg_statistic)
+ * stats for a given relation.
+ */
+static void
+getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "s.attname,"
+						 "s.inherited,"
+						 "s.null_frac,"
+						 "s.avg_width,"
+						 "s.n_distinct,"
+						 "s.most_common_vals,"
+						 "s.most_common_freqs,"
+						 "s.histogram_bounds,"
+						 "s.correlation,"
+						 "s.most_common_elems,"
+						 "s.most_common_elem_freqs,"
+						 "s.elem_count_histogram,");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 "s.range_length_histogram,"
+							 "s.range_empty_frac,"
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(query,
+							 "NULL AS range_length_histogram,"
+							 "NULL AS range_empty_frac,"
+							 "NULL AS range_bounds_histogram ");
+
+	appendPQExpBufferStr(query,
+						 "FROM pg_stats s "
+						 "JOIN pg_namespace n "
+						 "ON n.nspname = s.schemaname "
+						 "JOIN pg_class c "
+						 "ON c.relname = s.tablename "
+						 "AND c.relnamespace = n.oid "
+						 "WHERE s.schemaname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND s.tablename = ");
+	appendStringLiteralAH(query, relname, fout);
+	appendPQExpBufferStr(query, " ORDER BY s.attname, s.inherited");
+}
+
+
+/*
+ * appendNamedArgument --
+ *
+ * Convenience routine for constructing parameters of the form:
+ *  paraname => 'value'::type
+ */
+static void
+appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
+					const char *argval, const char *argtype)
+{
+	appendPQExpBuffer(out, "\t%s => ", argname);
+	appendStringLiteralAH(out, argval, fout);
+	appendPQExpBuffer(out, "::%s", argtype);
+}
+
+/*
+ * appendRelStatsImport --
+ *
+ * Append a formatted pg_set_relation_stats statement.
+ */
+static void
+appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	const char *sep = "";
+
+	if (PQntuples(res) == 0)
+		return;
+
+	appendPQExpBufferStr(out, "SELECT pg_catalog.pg_set_relation_stats(\n");
+
+	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
+	{
+		bool			required = (rel_stats_arginfo[argno][0][0] == 't');
+		const char	   *argname = rel_stats_arginfo[argno][1];
+		const char	   *argtype = rel_stats_arginfo[argno][2];
+		int				fieldno = PQfnumber(res, argname);
+
+		if (fieldno < 0)
+			pg_fatal("relation stats export query missing field '%s'",
+					 argname);
+
+		if (PQgetisnull(res, 0, fieldno))
+		{
+			if (required)
+				pg_fatal("relation stats export query unexpected NULL in '%s'",
+						 argname);
+			else
+				continue;
+		}
+
+		appendPQExpBufferStr(out, sep);
+		appendNamedArgument(out, fout, argname, 
+							PQgetvalue(res, 0, fieldno), argtype);
+
+		sep = ",\n";
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+/*
+ * appendAttStatsImport --
+ *
+ * Append a series of formatted pg_set_attribute_stats statements.
+ */
+static void
+appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	{
+		const char *sep = "";
+		appendPQExpBufferStr(out, "SELECT pg_catalog.pg_set_attribute_stats(\n");
+		for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
+		{
+			bool		required = (att_stats_arginfo[argno][0][0] == 't');
+			const char	*argname = att_stats_arginfo[argno][1];
+			const char	*argtype = att_stats_arginfo[argno][2];
+			int			 fieldno = PQfnumber(res, argname);
+
+			if (fieldno < 0)
+				pg_fatal("attribute stats export query missing field '%s'",
+						 argname);
+
+			if (PQgetisnull(res, rownum, fieldno))
+			{
+				if (required)
+					pg_fatal("attribute stats export query unexpected NULL in '%s'",
+							 argname);
+				else
+					continue;
+			}
+
+			appendPQExpBufferStr(out, sep);
+			appendNamedArgument(out, fout, argname,
+								PQgetvalue(res, rownum, fieldno), argtype);
+			sep = ",\n";
+		}
+		appendPQExpBufferStr(out, "\n);\n");
+	}
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const DumpableObject *dobj,
+				  const char *reltypename, DumpId dumpid)
+{
+	PGresult   *res;
+	PQExpBuffer query;
+	PQExpBuffer out;
+	PQExpBuffer tag;
+
+	/* do nothing, if --no-statistics is supplied */
+	if (fout->dopt->no_statistics)
+		return;
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", reltypename, fmtId(dobj->name));
+
+
+	query = createPQExpBuffer();
+	out = createPQExpBuffer();
+
+	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendRelStatsImport(out, fout, res);
+	PQclear(res);
+
+	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendRelStatsImport(out, fout, res);
+	PQclear(res);
+
+	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+							  dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendAttStatsImport(out, fout, res);
+	PQclear(res);
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATISTICS DATA",
+							  .section = SECTION_NONE,
+							  .createStmt = out->data,
+							  .deps = &dumpid,
+							  .nDeps = 1));
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(out);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -16681,6 +16951,13 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 	if (tbinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
 		dumpTableSecLabel(fout, tbinfo, reltypename);
 
+	/* Statistics are dependent on the definition, not the data */
+	/* Views don't have stats */
+	if ((tbinfo->dobj.dump & DUMP_COMPONENT_STATISTICS) &&
+		(tbinfo->relkind != RELKIND_VIEW))
+		dumpRelationStats(fout, &tbinfo->dobj, reltypename,
+						  tbinfo->dobj.dumpId);
+
 	/* Dump comments on inlined table constraints */
 	for (j = 0; j < tbinfo->ncheck; j++)
 	{
@@ -16882,6 +17159,7 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 	PQExpBuffer delq;
 	char	   *qindxname;
 	char	   *qqindxname;
+	DumpId		dumpid;
 
 	/* Do nothing in data-only dump */
 	if (dopt->dataOnly)
@@ -16994,14 +17272,21 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 		free(indstatvalsarray);
 	}
 
+	/* Comments and stats share same .dep */
+	dumpid = is_constraint ? indxinfo->indexconstraint :
+		indxinfo->dobj.dumpId;
+
 	/* Dump Index Comments */
 	if (indxinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
 		dumpComment(fout, "INDEX", qindxname,
 					tbinfo->dobj.namespace->dobj.name,
 					tbinfo->rolname,
 					indxinfo->dobj.catId, 0,
-					is_constraint ? indxinfo->indexconstraint :
-					indxinfo->dobj.dumpId);
+					dumpid);
+
+	/* Dump Index Stats */
+	if (indxinfo->dobj.dump & DUMP_COMPONENT_STATISTICS)
+		dumpRelationStats(fout, &indxinfo->dobj, "INDEX", dumpid);
 
 	destroyPQExpBuffer(q);
 	destroyPQExpBuffer(delq);
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 9bc93520b4..d6a071ec28 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -101,6 +101,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 046c0dc3b3..69652aa205 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -105,6 +105,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -174,6 +175,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -453,6 +455,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -668,6 +672,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index c3beacdec1..2d326dec72 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -75,6 +75,7 @@ main(int argc, char **argv)
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
 	static int	no_subscriptions = 0;
+	static int  no_statistics = 0;
 	static int	strict_names = 0;
 
 	struct option cmdopts[] = {
@@ -126,6 +127,7 @@ main(int argc, char **argv)
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
+		{"no-statistics", no_argument, &no_statistics, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -358,6 +360,7 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
diff --git a/src/bin/pg_upgrade/t/002_pg_upgrade.pl b/src/bin/pg_upgrade/t/002_pg_upgrade.pl
index 3e67121a8d..5006223fbe 100644
--- a/src/bin/pg_upgrade/t/002_pg_upgrade.pl
+++ b/src/bin/pg_upgrade/t/002_pg_upgrade.pl
@@ -315,7 +315,7 @@ if (defined($ENV{oldinstall}))
 # that we need to use pg_dumpall from the new node here.
 my @dump_command = (
 	'pg_dumpall', '--no-sync', '-d', $oldnode->connstr('postgres'),
-	'-f', $dump1_file);
+	'-f', $dump1_file, '--no-statistics');
 # --extra-float-digits is needed when upgrading from a version older than 11.
 push(@dump_command, '--extra-float-digits', '0')
   if ($oldnode->pg_version < 12);
@@ -483,7 +483,7 @@ is( $result,
 # Second dump from the upgraded instance.
 @dump_command = (
 	'pg_dumpall', '--no-sync', '-d', $newnode->connstr('postgres'),
-	'-f', $dump2_file);
+	'-f', $dump2_file, '--no-statistics');
 # --extra-float-digits is needed when upgrading from a version older than 11.
 push(@dump_command, '--extra-float-digits', '0')
   if ($oldnode->pg_version < 12);
-- 
2.44.0

#93Magnus Hagander
magnus@hagander.net
In reply to: Corey Huinker (#88)
Re: Statistics Import and Export

On Sat, Mar 30, 2024 at 1:26 AM Corey Huinker <corey.huinker@gmail.com>
wrote:

On Fri, Mar 29, 2024 at 7:34 PM Jeff Davis <pgsql@j-davis.com> wrote:

On Fri, 2024-03-29 at 18:02 -0400, Stephen Frost wrote:

I’d certainly think “with stats” would be the preferred default of
our users.

I'm concerned there could still be paths that lead to an error. For
pg_restore, or when loading a SQL file, a single error isn't fatal
(unless -e is specified), but it still could be somewhat scary to see
errors during a reload.

To that end, I'm going to be modifying the "Optimizer statistics are not
transferred by pg_upgrade..." message when stats _were_ transferred,
width additional instructions that the user should treat any stats-ish
error messages encountered as a reason to manually analyze that table. We
should probably say something about extended stats as well.

I'm getting late into this discussion and I apologize if I've missed this
being discussed before. But.

Please don't.

That will make it *really* hard for any form of automation or drivers of
this. The information needs to go somewhere where such tools can easily
consume it, and an informational message during runtime (which is also
likely to be translated in many environments) is the exact opposite of that.

Surely we can come up with something better. Otherwise, I think all those
tools are just going ot have to end up assuming that it always failed and
proceed based on that, and that would be a shame.

--
Magnus Hagander
Me: https://www.hagander.net/ <http://www.hagander.net/&gt;
Work: https://www.redpill-linpro.com/ <http://www.redpill-linpro.com/&gt;

#94Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#92)
Re: Statistics Import and Export

On Sat, 2024-03-30 at 01:34 -0400, Corey Huinker wrote:

- 002pg_upgrade.pl now dumps before/after databases with --no-
statistics. I tried to find out why some tables were getting their
relstats either not set, or set and reset, never affecting the
attribute stats. I even tried turning autovacuum off for both
instances, but nothing seemed to change the fact that the same tables
were having their relstats reset.

I think I found out why this is happening: a schema-only dump first
creates the table, then sets the relstats, then creates indexes. The
index creation updates the relstats, but because the dump was schema-
only, it overwrites the relstats with zeros.

That exposes an interesting dependency, which is that relstats must be
set after index creation, otherwise they will be lost -- at least in
the case of pg_upgrade.

This re-raises the question of whether stats are part of a schema-only
dump or not. Did we settle conclusively that they are?

Regards,
Jeff Davis

#95Tom Lane
tgl@sss.pgh.pa.us
In reply to: Jeff Davis (#94)
Re: Statistics Import and Export

Jeff Davis <pgsql@j-davis.com> writes:

This re-raises the question of whether stats are part of a schema-only
dump or not. Did we settle conclusively that they are?

Surely they are data, not schema. It would make zero sense to restore
them if you aren't restoring the data they describe.

Hence, it'll be a bit messy if we can't put them in the dump's DATA
section. Maybe we need to revisit CREATE INDEX's behavior rather
than assuming it's graven in stone?

regards, tom lane

#96Jeff Davis
pgsql@j-davis.com
In reply to: Tom Lane (#95)
Re: Statistics Import and Export

On Sat, 2024-03-30 at 13:18 -0400, Tom Lane wrote:

Surely they are data, not schema.  It would make zero sense to
restore
them if you aren't restoring the data they describe.

The complexity is that pg_upgrade does create the data, but relies on a
schema-only dump. So we'd need to at least account for that somehow,
either with a separate stats-only dump, or make a special case in
binary upgrade mode that dumps schema+stats (and resolves the CREATE
INDEX issue).

Maybe we need to revisit CREATE INDEX's behavior rather
than assuming it's graven in stone?

Would there be a significant cost to just not doing that? Or are you
suggesting that we special-case the behavior, or turn it off during
restore with a GUC?

Regards,
Jeff Davis

#97Tom Lane
tgl@sss.pgh.pa.us
In reply to: Jeff Davis (#96)
Re: Statistics Import and Export

Jeff Davis <pgsql@j-davis.com> writes:

On Sat, 2024-03-30 at 13:18 -0400, Tom Lane wrote:

Surely they are data, not schema. It would make zero sense to
restore them if you aren't restoring the data they describe.

The complexity is that pg_upgrade does create the data, but relies on a
schema-only dump. So we'd need to at least account for that somehow,
either with a separate stats-only dump, or make a special case in
binary upgrade mode that dumps schema+stats (and resolves the CREATE
INDEX issue).

Ah, good point. But binary-upgrade mode is special in tons of ways
already. I don't see a big problem with allowing it to dump stats
even though --schema-only would normally imply not doing that.

(You could also imagine an explicit positive --stats switch that would
override --schema-only, but I don't see that it's worth the trouble.)

Maybe we need to revisit CREATE INDEX's behavior rather
than assuming it's graven in stone?

Would there be a significant cost to just not doing that? Or are you
suggesting that we special-case the behavior, or turn it off during
restore with a GUC?

I didn't have any specific proposal in mind, was just trying to think
outside the box.

regards, tom lane

#98Jeff Davis
pgsql@j-davis.com
In reply to: Tom Lane (#97)
Re: Statistics Import and Export

On Sat, 2024-03-30 at 13:39 -0400, Tom Lane wrote:

(You could also imagine an explicit positive --stats switch that
would
override --schema-only, but I don't see that it's worth the trouble.)

That would have its own utility for reproducing planner problems
outside of production systems.

(That could be a separate feature, though, and doesn't need to be a
part of this patch set.)

Regards,
Jeff Davis

#99Corey Huinker
corey.huinker@gmail.com
In reply to: Magnus Hagander (#93)
Re: Statistics Import and Export

I'm getting late into this discussion and I apologize if I've missed this
being discussed before. But.

Please don't.

That will make it *really* hard for any form of automation or drivers of
this. The information needs to go somewhere where such tools can easily
consume it, and an informational message during runtime (which is also
likely to be translated in many environments) is the exact opposite of that.

That makes a lot of sense. I'm not sure what form it would take (file,
pseudo-table, something else?). Open to suggestions.

#100Corey Huinker
corey.huinker@gmail.com
In reply to: Tom Lane (#97)
Re: Statistics Import and Export

I didn't have any specific proposal in mind, was just trying to think
outside the box.

What if we added a separate resection SECTION_STATISTICS which is run
following post-data?

#101Tom Lane
tgl@sss.pgh.pa.us
In reply to: Corey Huinker (#100)
Re: Statistics Import and Export

Corey Huinker <corey.huinker@gmail.com> writes:

I didn't have any specific proposal in mind, was just trying to think
outside the box.

What if we added a separate resection SECTION_STATISTICS which is run
following post-data?

Maybe, but that would have a lot of side-effects on pg_dump's API
and probably on some end-user scripts. I'd rather not.

I haven't looked at the details, but I'm really a bit surprised
by Jeff's assertion that CREATE INDEX destroys statistics on the
base table. That seems wrong from here, and maybe something we
could have it not do. (I do realize that it recalculates reltuples
and relpages, but so what? If it updates those, the results should
be perfectly accurate.)

regards, tom lane

#102Corey Huinker
corey.huinker@gmail.com
In reply to: Magnus Hagander (#93)
Re: Statistics Import and Export

That will make it *really* hard for any form of automation or drivers of
this. The information needs to go somewhere where such tools can easily
consume it, and an informational message during runtime (which is also
likely to be translated in many environments) is the exact opposite of that.

Having given this some thought, I'd be inclined to create a view,
pg_stats_missing, with the same security barrier as pg_stats, but looking
for tables that lack stats on at least one column, or lack stats on an
extended statistics object.

Table structure would be

schemaname name
tablename name
attnames text[]
ext_stats text[]

The informational message, if it changes at all, could reference this new
view as the place to learn about how well the stats import went.

vacuumdb might get a --missing-only option in addition to
--analyze-in-stages.

For that matter, we could add --analyze-missing options to pg_restore and
pg_upgrade to do the mopping up themselves.

#103Tom Lane
tgl@sss.pgh.pa.us
In reply to: Corey Huinker (#102)
Re: Statistics Import and Export

Corey Huinker <corey.huinker@gmail.com> writes:

Having given this some thought, I'd be inclined to create a view,
pg_stats_missing, with the same security barrier as pg_stats, but looking
for tables that lack stats on at least one column, or lack stats on an
extended statistics object.

The week before feature freeze is no time to be designing something
like that, unless you've abandoned all hope of getting this into v17.

There's a bigger issue though: AFAICS this patch set does nothing
about dumping extended statistics. I surely don't want to hold up
the patch insisting that that has to happen before we can commit the
functionality proposed here. But we cannot rip out pg_upgrade's
support for post-upgrade ANALYZE processing before we do something
about extended statistics, and that means it's premature to be
designing any changes to how that works. So I'd set that whole
topic on the back burner.

It's possible that we could drop the analyze-in-stages recommendation,
figuring that this functionality will get people to the
able-to-limp-along level immediately and that all that is needed is a
single mop-up ANALYZE pass. But I think we should leave that till we
have a bit more than zero field experience with this feature.

regards, tom lane

#104Tom Lane
tgl@sss.pgh.pa.us
In reply to: Corey Huinker (#102)
Re: Statistics Import and Export

My apologies for having paid so little attention to this thread for
months. I got around to reading the v15 patches today, and while
I think they're going in more or less the right direction, there's
a long way to go IMO.

I concur with the plan of extracting data from pg_stats not
pg_statistics, and with emitting a single "set statistics"
call per attribute. (I think at one point I'd suggested a call
per stakind slot, but that would lead to a bunch of UPDATEs on
existing pg_attribute tuples and hence a bunch of dead tuples
at the end of an import, so it's not the way to go. A series
of UPDATEs would likely also play poorly with a background
auto-ANALYZE happening concurrently.)

I do not like the current design for pg_set_attribute_stats' API
though: I don't think it's at all future-proof. What happens when
somebody adds a new stakind (and hence new pg_stats column)?
You could try to add an overloaded pg_set_attribute_stats
version with more parameters, but I'm pretty sure that would
lead to "ambiguous function call" failures when trying to load
old dump files containing only the original parameters. The
present design is also fragile in that an unrecognized parameter
will lead to a parse-time failure and no function call happening,
which is less robust than I'd like. As lesser points,
the relation argument ought to be declared regclass not oid for
convenience of use, and I really think that we need to provide
the source server's major version number --- maybe we will never
need that, but if we do and we don't have it we will be sad.

So this leads me to suggest that we'd be best off with a VARIADIC
ANY signature, where the variadic part consists of alternating
parameter labels and values:

pg_set_attribute_stats(table regclass, attribute name,
inherited bool, source_version int,
variadic "any") returns void

where a call might look like

SELECT pg_set_attribute_stats('public.mytable', 'mycolumn',
false, -- not inherited
16, -- source server major version
-- pairs of labels and values follow
'null_frac', 0.4,
'avg_width', 42,
'histogram_bounds',
array['a', 'b', 'c']::text[],
...);

Note a couple of useful things here:

* AFAICS we could label the function strict and remove all those ad-hoc
null checks. If you don't have a value for a particular stat, you
just leave that pair of arguments out (exactly as the existing code
in 0002 does, just using a different notation). This also means that
we don't need any default arguments and so no need for hackery in
system_functions.sql.

* If we don't recognize a parameter label at runtime, we can treat
that as a warning rather than a hard error, and press on. This case
would mostly be useful in major version downgrades I suppose, but
that will be something people will want eventually.

* We can require the calling statement to cast arguments, particularly
arrays, to the proper type, removing the need for conversions within
the stats-setting function. (But instead, it'd need to check that the
next "any" argument is the type it ought to be based on the type of
the target column.)

If we write the labels as undecorated string literals as I show
above, I think they will arrive at the function as "unknown"-type
constants, which is a little weird but doesn't seem like it's
really a big problem. The alternative is to cast them all to text
explicitly, but that's adding notation to no great benefit IMO.

pg_set_relation_stats is simpler in that the set of stats values
to be set will probably remain fairly static, and there seems little
reason to allow only part of them to be supplied (so personally I'd
drop the business about accepting nulls there too). If we do grow
another value or values for it to set there shouldn't be much problem
with overloading it with another version with more arguments.
Still needs to take regclass not oid though ...

I've not read the patches in great detail, but I did note a
few low-level issues:

* why is check_relation_permissions looking up the pg_class row?
There's already a copy of that in the Relation struct. Likewise
for the other caller of can_modify_relation (but why is that
caller not using check_relation_permissions?) That all looks
overly complicated and duplicative. I think you don't need two
layers of function there.

* I find the stuff with enums and "const char *param_names" to
be way too cute and unlike anything we do elsewhere. Please
don't invent your own notations for coding patterns that have
hundreds of existing instances. pg_set_relation_stats, for
example, has absolutely no reason not to look like the usual

Oid relid = PG_GETARG_OID(0);
float4 relpages = PG_GETARG_FLOAT4(1);
... etc ...

* The array manipulations seem to me to be mostly not well chosen.
There's no reason to use expanded arrays here, since you won't be
modifying the arrays in-place; all that's doing is wasting memory.
I'm also noting a lack of defenses against nulls in the arrays.
I'd suggest using deconstruct_array to disassemble the arrays,
if indeed they need disassembled at all. (Maybe they don't, see
next item.)

* I'm dubious that we can fully vet the contents of these arrays,
and even a little dubious that we need to try. As an example,
what's the worst that's going to happen if a histogram array isn't
sorted precisely? You might get bogus selectivity estimates
from the planner, but that's no worse than you would've got with
no stats at all. (It used to be that selfuncs.c would use a
histogram even if its contents didn't match the query's collation.
The comments justifying that seem to be gone, but I think it's
still the case that the code isn't *really* dependent on the sort
order being exactly so.) The amount of hastily-written code in the
patch for checking this seems a bit scary, and it's well within the
realm of possibility that it introduces more bugs than it prevents.
We do need to verify data types, lack of nulls, and maybe
1-dimensional-ness, which could break the accessing code at a fairly
low level; but I'm not sure that we need more than that.

* There's a lot of ERROR cases that maybe we ought to downgrade
to WARN-and-press-on, in the service of not breaking the restore
completely in case of trouble.

* 0002 is confused about whether the tag for these new TOC
entries is "STATISTICS" or "STATISTICS DATA". I also think
they need to be in SECTION_DATA not SECTION_NONE, and I'd be
inclined to make them dependent on the table data objects
not the table declarations. We don't really want a parallel
restore to load them before the data is loaded: that just
increases the risk of bad interactions with concurrent
auto-analyze.

* It'd definitely not be OK to put BEGIN/COMMIT into the commands
in these TOC entries. But I don't think we need to.

* dumpRelationStats seems to be dumping the relation-level
stats twice.

* Why exactly are you suppressing testing of statistics upgrade
in 002_pg_upgrade??

regards, tom lane

#105Corey Huinker
corey.huinker@gmail.com
In reply to: Tom Lane (#103)
Re: Statistics Import and Export

On Sun, Mar 31, 2024 at 2:41 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:

Corey Huinker <corey.huinker@gmail.com> writes:

Having given this some thought, I'd be inclined to create a view,
pg_stats_missing, with the same security barrier as pg_stats, but looking
for tables that lack stats on at least one column, or lack stats on an
extended statistics object.

The week before feature freeze is no time to be designing something
like that, unless you've abandoned all hope of getting this into v17.

It was a response to the suggestion that there be some way for
tools/automation to read the status of stats. I would view it as a separate
patch, as such a view would be useful now for knowing which tables to
ANALYZE, regardless of whether this patch goes in or not.

There's a bigger issue though: AFAICS this patch set does nothing
about dumping extended statistics. I surely don't want to hold up
the patch insisting that that has to happen before we can commit the
functionality proposed here. But we cannot rip out pg_upgrade's
support for post-upgrade ANALYZE processing before we do something
about extended statistics, and that means it's premature to be
designing any changes to how that works. So I'd set that whole
topic on the back burner.

So Extended Stats _were_ supported by earlier versions where the medium of
communication was JSON. However, there were several problems with adapting
that to the current model where we match params to stat types:

* Several of the column types do not have functional input functions, so we
must construct the data structure internally and pass them to
statext_store().
* The output functions for some of those column types have lists of
attnums, with negative values representing positional expressions in the
stat definition. This information is not translatable to another system
without also passing along the attnum/attname mapping of the source system.

At least three people told me "nobody uses extended stats" and to just drop
that from the initial version. Unhappy with this assessment, I inquired as
to whether my employer (AWS) had some internal databases that used extended
stats so that I could get good test data, and came up with nothing, nor did
anyone know of customers who used the feature. So when the fourth person
told me that nobody uses extended stats, and not to let a rarely-used
feature get in the way of a feature that would benefit nearly 100% of
users, I dropped it.

It's possible that we could drop the analyze-in-stages recommendation,
figuring that this functionality will get people to the
able-to-limp-along level immediately and that all that is needed is a
single mop-up ANALYZE pass. But I think we should leave that till we
have a bit more than zero field experience with this feature.

It may be that we leave the recommendation exactly as it is.

Perhaps we enhance the error messages in pg_set_*_stats() to indicate what
command would remediate the issue.

#106Tom Lane
tgl@sss.pgh.pa.us
In reply to: Corey Huinker (#105)
Re: Statistics Import and Export

Corey Huinker <corey.huinker@gmail.com> writes:

On Sun, Mar 31, 2024 at 2:41 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:

There's a bigger issue though: AFAICS this patch set does nothing
about dumping extended statistics. I surely don't want to hold up
the patch insisting that that has to happen before we can commit the
functionality proposed here. But we cannot rip out pg_upgrade's
support for post-upgrade ANALYZE processing before we do something
about extended statistics, and that means it's premature to be
designing any changes to how that works. So I'd set that whole
topic on the back burner.

So Extended Stats _were_ supported by earlier versions where the medium of
communication was JSON. However, there were several problems with adapting
that to the current model where we match params to stat types:

* Several of the column types do not have functional input functions, so we
must construct the data structure internally and pass them to
statext_store().
* The output functions for some of those column types have lists of
attnums, with negative values representing positional expressions in the
stat definition. This information is not translatable to another system
without also passing along the attnum/attname mapping of the source system.

I wonder if the right answer to that is "let's enhance the I/O
functions for those types". But whether that helps or not, it's
v18-or-later material for sure.

At least three people told me "nobody uses extended stats" and to just drop
that from the initial version.

I can't quibble with that view of what has priority. I'm just
suggesting that redesigning what pg_upgrade does in this area
should come later than doing something about extended stats.

regards, tom lane

#107Corey Huinker
corey.huinker@gmail.com
In reply to: Tom Lane (#106)
Re: Statistics Import and Export

I wonder if the right answer to that is "let's enhance the I/O
functions for those types". But whether that helps or not, it's
v18-or-later material for sure.

That was Stephen's take as well, and I agreed given that I had to throw the
kitchen-sink of source-side oid mappings (attname, types, collatons,
operators) into the JSON to work around the limitation.

I can't quibble with that view of what has priority. I'm just
suggesting that redesigning what pg_upgrade does in this area
should come later than doing something about extended stats.

I mostly agree, with the caveat that pg_upgrade's existing message saying
that optimizer stats were not carried over wouldn't be 100% true anymore.

#108Tom Lane
tgl@sss.pgh.pa.us
In reply to: Corey Huinker (#107)
Re: Statistics Import and Export

Corey Huinker <corey.huinker@gmail.com> writes:

I can't quibble with that view of what has priority. I'm just
suggesting that redesigning what pg_upgrade does in this area
should come later than doing something about extended stats.

I mostly agree, with the caveat that pg_upgrade's existing message saying
that optimizer stats were not carried over wouldn't be 100% true anymore.

I think we can tweak the message wording. I just don't want to be
doing major redesign of the behavior, nor adding fundamentally new
monitoring capabilities.

regards, tom lane

#109Corey Huinker
corey.huinker@gmail.com
In reply to: Tom Lane (#104)
Re: Statistics Import and Export

I concur with the plan of extracting data from pg_stats not
pg_statistics, and with emitting a single "set statistics"
call per attribute. (I think at one point I'd suggested a call
per stakind slot, but that would lead to a bunch of UPDATEs on
existing pg_attribute tuples and hence a bunch of dead tuples
at the end of an import, so it's not the way to go. A series
of UPDATEs would likely also play poorly with a background
auto-ANALYZE happening concurrently.)

That was my reasoning as well.

I do not like the current design for pg_set_attribute_stats' API
though: I don't think it's at all future-proof. What happens when
somebody adds a new stakind (and hence new pg_stats column)?
You could try to add an overloaded pg_set_attribute_stats
version with more parameters, but I'm pretty sure that would
lead to "ambiguous function call" failures when trying to load
old dump files containing only the original parameters.

I don't think we'd overload, we'd just add new parameters to the function
signature.

The
present design is also fragile in that an unrecognized parameter
will lead to a parse-time failure and no function call happening,
which is less robust than I'd like.

There was a lot of back-and-forth about what sorts of failures were
error-worthy, and which were warn-worthy. I'll discuss further below.

As lesser points,
the relation argument ought to be declared regclass not oid for
convenience of use,

+1

and I really think that we need to provide
the source server's major version number --- maybe we will never
need that, but if we do and we don't have it we will be sad.

The JSON had it, and I never did use it. Not against having it again.

So this leads me to suggest that we'd be best off with a VARIADIC
ANY signature, where the variadic part consists of alternating
parameter labels and values:

pg_set_attribute_stats(table regclass, attribute name,
inherited bool, source_version int,
variadic "any") returns void

where a call might look like

SELECT pg_set_attribute_stats('public.mytable', 'mycolumn',
false, -- not inherited
16, -- source server major version
-- pairs of labels and values follow
'null_frac', 0.4,
'avg_width', 42,
'histogram_bounds',
array['a', 'b', 'c']::text[],
...);

Note a couple of useful things here:

* AFAICS we could label the function strict and remove all those ad-hoc
null checks. If you don't have a value for a particular stat, you
just leave that pair of arguments out (exactly as the existing code
in 0002 does, just using a different notation). This also means that
we don't need any default arguments and so no need for hackery in
system_functions.sql.

I'm not aware of how strict works with variadics. Would the lack of any
variadic parameters trigger it?

Also going with strict means that an inadvertent explicit NULL in one
parameter would cause the entire attribute import to fail silently. I'd
rather fail loudly.

* If we don't recognize a parameter label at runtime, we can treat
that as a warning rather than a hard error, and press on. This case
would mostly be useful in major version downgrades I suppose, but
that will be something people will want eventually.

Interesting.

* We can require the calling statement to cast arguments, particularly

arrays, to the proper type, removing the need for conversions within
the stats-setting function. (But instead, it'd need to check that the
next "any" argument is the type it ought to be based on the type of
the target column.)

So, that's tricky. The type of the values is not always the attribute type,
for expression indexes, we do call exprType() and exprCollation(), in which
case we always use the expression type over the attribute type, but only
use the collation type if the attribute had no collation. This mimics the
behavior of ANALYZE.

Then, for the MCELEM and DECHIST stakinds we have to find the type's
element type, and that has special logic for tsvectors, ranges, and other
non-scalars, borrowing from the various *_typanalyze() functions. For that
matter, the existing typanalyze functions don't grab the < operator, which
I need for later data validations, so using examine_attribute() was
simultaneously overkill and insufficient.

None of this functionality is accessible from a client program, so we'd
have to pull in a lot of backend stuff to pg_dump to make it resolve the
typecasts correctly. Text and array_in() was just easier.

pg_set_relation_stats is simpler in that the set of stats values
to be set will probably remain fairly static, and there seems little
reason to allow only part of them to be supplied (so personally I'd
drop the business about accepting nulls there too). If we do grow
another value or values for it to set there shouldn't be much problem
with overloading it with another version with more arguments.
Still needs to take regclass not oid though ...

I'm still iffy about the silent failures of strict.

I looked it up, and the only change needed for changing oid to regclass is
in the pg_proc.dat. (and the docs, of course). So I'm already on board.

* why is check_relation_permissions looking up the pg_class row?
There's already a copy of that in the Relation struct. Likewise
for the other caller of can_modify_relation (but why is that
caller not using check_relation_permissions?) That all looks
overly complicated and duplicative. I think you don't need two
layers of function there.

To prove that the caller is the owner (or better) of the table.

* The array manipulations seem to me to be mostly not well chosen.
There's no reason to use expanded arrays here, since you won't be
modifying the arrays in-place; all that's doing is wasting memory.
I'm also noting a lack of defenses against nulls in the arrays.

Easily remedied in light of the deconstruct_array() suggestion below, but I
do want to add that value_not_null_array_len() does check for nulls, and
that function is used to generate all but one of the arrays (and that one
we're just verifying that it's length matches the length of the other
array).There's even a regression test that checks it (search for:
"elem_count_histogram null element").

I'd suggest using deconstruct_array to disassemble the arrays,
if indeed they need disassembled at all. (Maybe they don't, see
next item.)

+1

* I'm dubious that we can fully vet the contents of these arrays,
and even a little dubious that we need to try. As an example,
what's the worst that's going to happen if a histogram array isn't
sorted precisely? You might get bogus selectivity estimates
from the planner, but that's no worse than you would've got with
no stats at all. (It used to be that selfuncs.c would use a
histogram even if its contents didn't match the query's collation.
The comments justifying that seem to be gone, but I think it's
still the case that the code isn't *really* dependent on the sort
order being exactly so.) The amount of hastily-written code in the
patch for checking this seems a bit scary, and it's well within the
realm of possibility that it introduces more bugs than it prevents.
We do need to verify data types, lack of nulls, and maybe
1-dimensional-ness, which could break the accessing code at a fairly
low level; but I'm not sure that we need more than that.

A lot of the feedback I got on this patch over the months concerned giving
inaccurate, nonsensical, or malicious data to the planner. Surely the
planner does do *some* defensive programming when fetching these values,
but this is the first time those values were potentially set by a user, not
by our own internal code. We can try to match types, collations, etc from
source to dest, but even that would fall victim to another glibc-level
collation change. Verifying that the list the source system said was sorted
is actually sorted when put on the destination system is the truest test
we're ever going to get, albeit for sampled elements.

* There's a lot of ERROR cases that maybe we ought to downgrade
to WARN-and-press-on, in the service of not breaking the restore
completely in case of trouble.

All cases were made error precisely to spark debate about which cases we'd
want to continue from and which we'd want to error from. Also, I was under
the impression it was bad form to follow up NOTICE/WARN with an ERROR in
the same function call.

* 0002 is confused about whether the tag for these new TOC
entries is "STATISTICS" or "STATISTICS DATA". I also think
they need to be in SECTION_DATA not SECTION_NONE, and I'd be
inclined to make them dependent on the table data objects
not the table declarations. We don't really want a parallel
restore to load them before the data is loaded: that just
increases the risk of bad interactions with concurrent
auto-analyze.

SECTION_NONE works the best, but we're getting some situations where the
relpages/reltuples/relallvisible gets reset to 0s in pg_class. Hence the
temporary --no-statistics in the pg_upgrade TAP test.

SECTION_POST_DATA (a previous suggestion) causes something weird to happen
where certain GRANT/REVOKEs happen outside of their expected section.

In work I've done since v15, I tried giving the table stats archive entry a
dependency on every index (and index constraint) as well as the table
itself, thinking that would get us past all resets of pg_class, but it
hasn't worked.

* It'd definitely not be OK to put BEGIN/COMMIT into the commands
in these TOC entries. But I don't think we need to.

Agreed. Don't need to, each function call now sinks or swims on its own.

* dumpRelationStats seems to be dumping the relation-level
stats twice.

+1

* Why exactly are you suppressing testing of statistics upgrade

in 002_pg_upgrade??

Temporary. Related to the pg_class overwrite issue above.

#110Tom Lane
tgl@sss.pgh.pa.us
In reply to: Corey Huinker (#109)
Re: Statistics Import and Export

Corey Huinker <corey.huinker@gmail.com> writes:

and I really think that we need to provide
the source server's major version number --- maybe we will never
need that, but if we do and we don't have it we will be sad.

The JSON had it, and I never did use it. Not against having it again.

Well, you don't need it now seeing that the definition of pg_stats
columns hasn't changed in the past ... but there's no guarantee we
won't want to change them in the future.

So this leads me to suggest that we'd be best off with a VARIADIC
ANY signature, where the variadic part consists of alternating
parameter labels and values:
pg_set_attribute_stats(table regclass, attribute name,
inherited bool, source_version int,
variadic "any") returns void

I'm not aware of how strict works with variadics. Would the lack of any
variadic parameters trigger it?

IIRC, "variadic any" requires having at least one variadic parameter.
But that seems fine --- what would be the point, or even the
semantics, of calling pg_set_attribute_stats with no data fields?

Also going with strict means that an inadvertent explicit NULL in one
parameter would cause the entire attribute import to fail silently. I'd
rather fail loudly.

Not really convinced that that is worth any trouble...

* We can require the calling statement to cast arguments, particularly

arrays, to the proper type, removing the need for conversions within
the stats-setting function. (But instead, it'd need to check that the
next "any" argument is the type it ought to be based on the type of
the target column.)

So, that's tricky. The type of the values is not always the attribute type,

Hmm. You would need to have enough smarts in pg_set_attribute_stats
to identify the appropriate array type in any case: as coded, it needs
that for coercion, whereas what I'm suggesting would only require it
for checking, but either way you need it. I do concede that pg_dump
(or other logic generating the calls) needs to know more under my
proposal than before. I had been thinking that it would not need to
hard-code that because it could look to see what the actual type is
of the array it's dumping. However, I see that pg_typeof() doesn't
work for that because it just returns anyarray. Perhaps we could
invent a new backend function that extracts the actual element type
of a non-null anyarray argument.

Another way we could get to no-coercions is to stick with your
signature but declare the relevant parameters as anyarray instead of
text. I still think though that we'd be better off to leave the
parameter matching to runtime, so that we-don't-recognize-that-field
can be a warning not an error.

* why is check_relation_permissions looking up the pg_class row?
There's already a copy of that in the Relation struct.

To prove that the caller is the owner (or better) of the table.

I think you missed my point: you're doing that inefficiently,
and maybe even with race conditions. Use the relcache's copy
of the pg_class row.

* I'm dubious that we can fully vet the contents of these arrays,
and even a little dubious that we need to try.

A lot of the feedback I got on this patch over the months concerned giving
inaccurate, nonsensical, or malicious data to the planner. Surely the
planner does do *some* defensive programming when fetching these values,
but this is the first time those values were potentially set by a user, not
by our own internal code. We can try to match types, collations, etc from
source to dest, but even that would fall victim to another glibc-level
collation change.

That sort of concern is exactly why I think the planner has to, and
does, defend itself. Even if you fully vet the data at the instant
of loading, we might have the collation change under us later.

It could be argued that feeding bogus data to the planner for testing
purposes is a valid use-case for this feature. (Of course, as
superuser we could inject bogus data into pg_statistic manually,
so it's not necessary to have this feature for that purpose.)
I guess I'm a great deal more sanguine than other people about the
planner's ability to tolerate inconsistent data; but in any case
I don't have a lot of faith in relying on checks in
pg_set_attribute_stats to substitute for that ability. That idea
mainly leads to having a whole lot of code that has to be kept in
sync with other code that's far away from it and probably isn't
coded in a parallel fashion either.

* There's a lot of ERROR cases that maybe we ought to downgrade
to WARN-and-press-on, in the service of not breaking the restore
completely in case of trouble.

All cases were made error precisely to spark debate about which cases we'd
want to continue from and which we'd want to error from.

Well, I'm here to debate it if you want, but I'll just note that *one*
error will be enough to abort a pg_upgrade entirely, and most users
these days get scared by errors during manual dump/restore too. So we
had better not be throwing errors except for cases that we don't think
pg_dump could ever emit.

Also, I was under
the impression it was bad form to follow up NOTICE/WARN with an ERROR in
the same function call.

Seems like nonsense to me. WARN then ERROR about the same condition
would be annoying, but that's not what we are talking about here.

regards, tom lane

#111Corey Huinker
corey.huinker@gmail.com
In reply to: Tom Lane (#110)
Re: Statistics Import and Export

IIRC, "variadic any" requires having at least one variadic parameter.
But that seems fine --- what would be the point, or even the
semantics, of calling pg_set_attribute_stats with no data fields?

If my pg_dump run emitted a bunch of stats that could never be imported,
I'd want to know. With silent failures, I don't.

Perhaps we could
invent a new backend function that extracts the actual element type
of a non-null anyarray argument.

A backend function that we can't guarantee exists on the source system. :(

Another way we could get to no-coercions is to stick with your
signature but declare the relevant parameters as anyarray instead of
text. I still think though that we'd be better off to leave the
parameter matching to runtime, so that we-don't-recognize-that-field
can be a warning not an error.

I'm a bit confused here. AFAIK we can't construct an anyarray in SQL:

# select '{1,2,3}'::anyarray;
ERROR: cannot accept a value of type anyarray

I think you missed my point: you're doing that inefficiently,
and maybe even with race conditions. Use the relcache's copy
of the pg_class row.

Roger Wilco.

Well, I'm here to debate it if you want, but I'll just note that *one*
error will be enough to abort a pg_upgrade entirely, and most users
these days get scared by errors during manual dump/restore too. So we
had better not be throwing errors except for cases that we don't think
pg_dump could ever emit.

That's pretty persuasive. It also means that we need to trap for error in
the array_in() calls, as that function does not yet have a _safe() mode.

#112Ashutosh Bapat
ashutosh.bapat.oss@gmail.com
In reply to: Ashutosh Bapat (#76)
Re: Statistics Import and Export

Hi Corey,

On Mon, Mar 25, 2024 at 3:38 PM Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>
wrote:

Hi Corey,

On Sat, Mar 23, 2024 at 7:21 AM Corey Huinker <corey.huinker@gmail.com>
wrote:

v12 attached.

0001 -

Some random comments

+SELECT
+    format('SELECT pg_catalog.pg_set_attribute_stats( '
+            || 'relation => %L::regclass::oid, attname => %L::name, '
+            || 'inherited => %L::boolean, null_frac => %L::real, '
+            || 'avg_width => %L::integer, n_distinct => %L::real, '
+            || 'most_common_vals => %L::text, '
+            || 'most_common_freqs => %L::real[], '
+            || 'histogram_bounds => %L::text, '
+            || 'correlation => %L::real, '
+            || 'most_common_elems => %L::text, '
+            || 'most_common_elem_freqs => %L::real[], '
+            || 'elem_count_histogram => %L::real[], '
+            || 'range_length_histogram => %L::text, '
+            || 'range_empty_frac => %L::real, '
+            || 'range_bounds_histogram => %L::text) ',
+        'stats_export_import.' || s.tablename || '_clone', s.attname,
+        s.inherited, s.null_frac,
+        s.avg_width, s.n_distinct,
+        s.most_common_vals, s.most_common_freqs, s.histogram_bounds,
+        s.correlation, s.most_common_elems, s.most_common_elem_freqs,
+        s.elem_count_histogram, s.range_length_histogram,
+        s.range_empty_frac, s.range_bounds_histogram)
+FROM pg_catalog.pg_stats AS s
+WHERE s.schemaname = 'stats_export_import'
+AND s.tablename IN ('test', 'is_odd')
+\gexec

Why do we need to construct the command and execute? Can we instead
execute the function directly? That would also avoid ECHO magic.

Addressed in v15

+   <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>

COMMENT: The functions throw many validation errors. Do we want to list
the acceptable/unacceptable input values in the documentation corresponding
to those? I don't expect one line per argument validation. Something like
"these, these and these arguments can not be NULL" or "both arguments in
each of the pairs x and y, a and b, and c and d should be non-NULL or NULL
respectively".

Addressed in v15.

+ /* Statistics are dependent on the definition, not the data */
+ /* Views don't have stats */
+ if ((tbinfo->dobj.dump & DUMP_COMPONENT_STATISTICS) &&
+ (tbinfo->relkind == RELKIND_VIEW))
+ dumpRelationStats(fout, &tbinfo->dobj, reltypename,
+  tbinfo->dobj.dumpId);
+

Statistics are about data. Whenever pg_dump dumps some filtered data, the
statistics collected for the whole table are uselss. We should avoide
dumping
statistics in such a case. E.g. when only schema is dumped what good is
statistics? Similarly the statistics on a partitioned table may not be
useful
if some its partitions are not dumped. Said that dumping statistics on
foreign
table makes sense since they do not contain data but the statistics still
makes sense.

Dumping statistics without data is required for pg_upgrade. This is being
discussed in the same thread. But I don't see some of the suggestions e.g.
using binary-mode switch being used in v15.

Also, should we handle sequences, composite types the same way? THe latter
is probably not dumped, but in case.

Whether or not I pass --no-statistics, there is no difference in the dump
output. Am I missing something?
$ pg_dump -d postgres > /tmp/dump_no_arguments.out
$ pg_dump -d postgres --no-statistics > /tmp/dump_no_statistics.out
$ diff /tmp/dump_no_arguments.out /tmp/dump_no_statistics.out
$

IIUC, pg_dump includes statistics by default. That means all our pg_dump
related tests will have statistics output by default. That's good since the
functionality will always be tested. 1. We need additional tests to ensure
that the statistics is installed after restore. 2. Some of those tests
compare dumps before and after restore. In case the statistics is changed
because of auto-analyze happening post-restore, these tests will fail.

Fixed.

Thanks for addressing those comments.

--
Best Wishes,
Ashutosh Bapat

#113Ashutosh Bapat
ashutosh.bapat.oss@gmail.com
In reply to: Corey Huinker (#111)
1 attachment(s)
Re: Statistics Import and Export

Hi Corey,

Some more comments on v15.

+/*
+ * A more encapsulated version of can_modify_relation for when the the
+ * HeapTuple and Form_pg_class are not needed later.
+ */
+static void
+check_relation_permissions(Relation rel)

This function is used exactly at one place, so usually won't make much
sense to write a separate function. But given that the caller is so long,
this seems ok. If this function returns the cached tuple when permission
checks succeed, it can be used at the other place as well. The caller will
be responsible to release the tuple Or update it.

Attached patch contains a test to invoke this function on a view. ANALYZE
throws a WARNING when a view is passed to it. Similarly this function
should refuse to update the statistics on relations for which ANALYZE
throws a warning. A warning instead of an error seems fine.

+
+ const float4 min = 0.0;
+ const float4 max = 1.0;

When reading the validation condition, I have to look up variable values.
That can be avoided by directly using the values in the condition itself?
If there's some dependency elsewhere in the code, we can use macros. But I
have not seen using constant variables in such a way elsewhere in the code.

+ values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(relid);
+ values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(attnum);
+ values[Anum_pg_statistic_stainherit - 1] = PG_GETARG_DATUM(P_INHERITED);

For a partitioned table this value has to be true. For a normal table when
setting this value to true, it should at least make sure that the table has
at least one child. Otherwise it should throw an error. Blindly accepting
the given value may render the statistics unusable. Prologue of the
function needs to be updated accordingly.

I have fixed a documentation error in the patch as well. Please incorporate
it in your next patchset.
--
Best Wishes,
Ashutosh Bapat

Attachments:

stats_import_export_review.patchapplication/x-patch; name=stats_import_export_review.patchDownload
commit 353a4077d07cf097c5cb90c732b7dde2f16f04a6
Author: Ashutosh Bapat <ashutosh.bapat@enterprisedb.com>
Date:   Mon Apr 1 13:04:40 2024 +0530

    Review changes
    
    Ashutosh Bapat

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 153d0dc6ac..6018e81486 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29178,6 +29178,8 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
         be less than 0.
        </para>
        </entry>
+       </row>
+       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
          <primary>pg_set_attribute_stats</primary>
diff --git a/src/test/regress/sql/stats_export_import.sql b/src/test/regress/sql/stats_export_import.sql
index 1a7d02a2c7..e3f42f85f0 100644
--- a/src/test/regress/sql/stats_export_import.sql
+++ b/src/test/regress/sql/stats_export_import.sql
@@ -15,6 +15,8 @@ CREATE TABLE stats_export_import.test(
     tags text[]
 );
 
+CREATE VIEW stats_export_import.test_view AS SELECT id, length(name), (comp).e FROM stats_export_import.test;
+
 SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
 
 SELECT pg_set_relation_stats('stats_export_import.test'::regclass, 999, 3.6::real, 15000);
@@ -26,6 +28,16 @@ SELECT pg_set_relation_stats('stats_export_import.test'::regclass, NULL, 3.6::re
 
 SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
 
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test_view'::regclass;
+
+ANALYZE stats_export_import.test_view;
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test_view'::regclass;
+
+SELECT pg_set_relation_stats('stats_export_import.test_view'::regclass, 999, 3.6::real, 15000);
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test_view'::regclass;
+
 -- error: object doesn't exist
 SELECT pg_catalog.pg_set_attribute_stats(
     relation => '0'::oid,
@@ -62,6 +74,25 @@ SELECT pg_catalog.pg_set_attribute_stats(
     avg_width => 2::integer,
     n_distinct => 0.3::real);
 
+-- error: inherited true for a table which does not have any child
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => true,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+CREATE TABLE stats_export_import.child() INHERITS(stats_export_import.test);
+
+ANALYZE VERBOSE stats_export_import.test;
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND attname = 'id';
+
 -- error: null_frac null
 SELECT pg_catalog.pg_set_attribute_stats(
     relation => 'stats_export_import.test'::regclass,
#114Tom Lane
tgl@sss.pgh.pa.us
In reply to: Corey Huinker (#111)
Re: Statistics Import and Export

Corey Huinker <corey.huinker@gmail.com> writes:

IIRC, "variadic any" requires having at least one variadic parameter.
But that seems fine --- what would be the point, or even the
semantics, of calling pg_set_attribute_stats with no data fields?

If my pg_dump run emitted a bunch of stats that could never be imported,
I'd want to know. With silent failures, I don't.

What do you think would be silent about that? If there's a complaint
to be made, it's that it'd be a hard failure ("no such function").

To be clear, I'm ok with emitting ERROR for something that pg_dump
clearly did wrong, which in this case would be emitting a
set_statistics call for an attribute it had exactly no stats values
for. What I think needs to be WARN is conditions that the originating
pg_dump couldn't have foreseen, for example cross-version differences.
If we do try to check things like sort order, that complaint obviously
has to be WARN, since it's checking something potentially different
from what was correct at the source server.

Perhaps we could
invent a new backend function that extracts the actual element type
of a non-null anyarray argument.

A backend function that we can't guarantee exists on the source system. :(

[ shrug... ] If this doesn't work for source servers below v17, that
would be a little sad, but it wouldn't be the end of the world.
I see your point that that is an argument for finding another way,
though.

Another way we could get to no-coercions is to stick with your
signature but declare the relevant parameters as anyarray instead of
text.

I'm a bit confused here. AFAIK we can't construct an anyarray in SQL:

# select '{1,2,3}'::anyarray;
ERROR: cannot accept a value of type anyarray

That's not what I suggested at all. The function parameters would
be declared anyarray, but the values passed to them would be coerced
to the correct concrete array types. So as far as the coercion rules
are concerned this'd be equivalent to the variadic-any approach.

That's pretty persuasive. It also means that we need to trap for error in
the array_in() calls, as that function does not yet have a _safe() mode.

Well, the approach I'm advocating for would have the array input and
coercion done by the calling query before control ever reaches
pg_set_attribute_stats, so that any incorrect-for-the-data-type values
would result in hard errors. I think that's okay for the same reason
you probably figured you didn't have to trap array_in: it's the fault
of the originating pg_dump if it offers a value that doesn't coerce to
the datatype it claims the value is of. My formulation is a bit safer
though in that it's the originating pg_dump, not the receiving server,
that is in charge of saying which type that is. (If that type doesn't
agree with what the receiving server thinks it should be, that's a
condition that pg_set_attribute_stats itself will detect, and then it
can WARN and move on to the next thing.)

regards, tom lane

#115Jeff Davis
pgsql@j-davis.com
In reply to: Tom Lane (#101)
Re: Statistics Import and Export

On Sat, 2024-03-30 at 20:08 -0400, Tom Lane wrote:

I haven't looked at the details, but I'm really a bit surprised
by Jeff's assertion that CREATE INDEX destroys statistics on the
base table.  That seems wrong from here, and maybe something we
could have it not do.  (I do realize that it recalculates reltuples
and relpages, but so what?  If it updates those, the results should
be perfectly accurate.)

In the v15 of the patch I was looking at, "pg_dump -s" included the
statistics. The stats appeared first in the dump, followed by the
CREATE INDEX commands. The latter overwrote the relpages/reltuples set
by the former.

While zeros are the right answers for a schema-only dump, it defeated
the purpose of including relpages/reltuples stats in the dump, and
caused the pg_upgrade TAP test to fail.

You're right that there are a number of ways this could be resolved --
I don't think it's an inherent problem.

Regards,
Jeff Davis

#116Bruce Momjian
bruce@momjian.us
In reply to: Tom Lane (#114)
Re: Statistics Import and Export

Reality check --- are we still targeting this feature for PG 17?

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

Only you can decide what is important to you.

#117Tom Lane
tgl@sss.pgh.pa.us
In reply to: Jeff Davis (#115)
Re: Statistics Import and Export

Jeff Davis <pgsql@j-davis.com> writes:

On Sat, 2024-03-30 at 20:08 -0400, Tom Lane wrote:

I haven't looked at the details, but I'm really a bit surprised
by Jeff's assertion that CREATE INDEX destroys statistics on the
base table.  That seems wrong from here, and maybe something we
could have it not do.  (I do realize that it recalculates reltuples
and relpages, but so what?  If it updates those, the results should
be perfectly accurate.)

In the v15 of the patch I was looking at, "pg_dump -s" included the
statistics. The stats appeared first in the dump, followed by the
CREATE INDEX commands. The latter overwrote the relpages/reltuples set
by the former.

While zeros are the right answers for a schema-only dump, it defeated
the purpose of including relpages/reltuples stats in the dump, and
caused the pg_upgrade TAP test to fail.

You're right that there are a number of ways this could be resolved --
I don't think it's an inherent problem.

I'm inclined to call it not a problem at all. While I do agree there
are use-cases for injecting false statistics with these functions,
I do not think that pg_dump has to cater to such use-cases.

In any case, I remain of the opinion that stats are data and should
not be included in a -s dump (with some sort of exception for
pg_upgrade). If the data has been loaded, then a subsequent
overwrite by CREATE INDEX should not be a problem.

regards, tom lane

#118Tom Lane
tgl@sss.pgh.pa.us
In reply to: Bruce Momjian (#116)
Re: Statistics Import and Export

Bruce Momjian <bruce@momjian.us> writes:

Reality check --- are we still targeting this feature for PG 17?

I'm not sure. I think if we put our heads down we could finish
the changes I'm suggesting and resolve the other issues this week.
However, it is starting to feel like the sort of large, barely-ready
patch that we often regret cramming in at the last minute. Maybe
we should agree that the first v18 CF would be a better time to
commit it.

regards, tom lane

#119Jeff Davis
pgsql@j-davis.com
In reply to: Bruce Momjian (#116)
Re: Statistics Import and Export

On Mon, 2024-04-01 at 13:11 -0400, Bruce Momjian wrote:

Reality check --- are we still targeting this feature for PG 17?

I see a few useful pieces here:

1. Support import of statistics (i.e.
pg_set_{relation|attribute}_stats()).

2. Support pg_dump of stats

3. Support pg_upgrade with stats

It's possible that not all of them make it, but let's not dismiss the
entire feature yet.

Regards,
Jeff Davis

#120Bruce Momjian
bruce@momjian.us
In reply to: Tom Lane (#108)
Re: Statistics Import and Export

On Sun, Mar 31, 2024 at 07:04:47PM -0400, Tom Lane wrote:

Corey Huinker <corey.huinker@gmail.com> writes:

I can't quibble with that view of what has priority. I'm just
suggesting that redesigning what pg_upgrade does in this area
should come later than doing something about extended stats.

I mostly agree, with the caveat that pg_upgrade's existing message saying
that optimizer stats were not carried over wouldn't be 100% true anymore.

I think we can tweak the message wording. I just don't want to be
doing major redesign of the behavior, nor adding fundamentally new
monitoring capabilities.

I think pg_upgrade could check for the existence of extended statistics
in any database and adjust the analyze recommdnation wording
accordingly.

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

Only you can decide what is important to you.

#121Jeff Davis
pgsql@j-davis.com
In reply to: Tom Lane (#104)
Re: Statistics Import and Export

On Sun, 2024-03-31 at 14:48 -0400, Tom Lane wrote:

What happens when
somebody adds a new stakind (and hence new pg_stats column)?
You could try to add an overloaded pg_set_attribute_stats
version with more parameters, but I'm pretty sure that would
lead to "ambiguous function call" failures when trying to load
old dump files containing only the original parameters.

Why would you need to overload in this case? Wouldn't we just define a
new function with more optional named parameters?

  The
present design is also fragile in that an unrecognized parameter
will lead to a parse-time failure and no function call happening,
which is less robust than I'd like.

I agree on this point; I found this annoying when testing the feature.

So this leads me to suggest that we'd be best off with a VARIADIC
ANY signature, where the variadic part consists of alternating
parameter labels and values:

I didn't consider this and I think it has a lot of advantages. It's
slightly unfortunate that we can't make them explicitly name/value
pairs, but pg_dump can use whitespace or even SQL comments to make it
more readable.

Regards,
Jeff Davis

#122Tom Lane
tgl@sss.pgh.pa.us
In reply to: Jeff Davis (#119)
Re: Statistics Import and Export

Jeff Davis <pgsql@j-davis.com> writes:

On Mon, 2024-04-01 at 13:11 -0400, Bruce Momjian wrote:

Reality check --- are we still targeting this feature for PG 17?

I see a few useful pieces here:

1. Support import of statistics (i.e.
pg_set_{relation|attribute}_stats()).

2. Support pg_dump of stats

3. Support pg_upgrade with stats

It's possible that not all of them make it, but let's not dismiss the
entire feature yet.

The unresolved questions largely have to do with the interactions
between these pieces. I think we would seriously regret setting
any one of them in stone before all three are ready to go.

regards, tom lane

#123Tom Lane
tgl@sss.pgh.pa.us
In reply to: Jeff Davis (#121)
Re: Statistics Import and Export

Jeff Davis <pgsql@j-davis.com> writes:

On Sun, 2024-03-31 at 14:48 -0400, Tom Lane wrote:

What happens when
somebody adds a new stakind (and hence new pg_stats column)?

Why would you need to overload in this case? Wouldn't we just define a
new function with more optional named parameters?

Ah, yeah, you could change the function to have more parameters,
given the assumption that all calls will be named-parameter style.
I still suggest that my proposal is more robust for the case where
the dump lists parameters that the receiving system doesn't have.

regards, tom lane

#124Corey Huinker
corey.huinker@gmail.com
In reply to: Tom Lane (#114)
Re: Statistics Import and Export

That's not what I suggested at all. The function parameters would
be declared anyarray, but the values passed to them would be coerced
to the correct concrete array types. So as far as the coercion rules
are concerned this'd be equivalent to the variadic-any approach.

+1

That's pretty persuasive. It also means that we need to trap for error in
the array_in() calls, as that function does not yet have a _safe() mode.

Well, the approach I'm advocating for would have the array input and
coercion done by the calling query before control ever reaches
pg_set_attribute_stats, so that any incorrect-for-the-data-type values
would result in hard errors. I think that's okay for the same reason
you probably figured you didn't have to trap array_in: it's the fault
of the originating pg_dump if it offers a value that doesn't coerce to
the datatype it claims the value is of.

+1

#125Corey Huinker
corey.huinker@gmail.com
In reply to: Bruce Momjian (#120)
Re: Statistics Import and Export

I think pg_upgrade could check for the existence of extended statistics
in any database and adjust the analyze recommdnation wording
accordingly.

+1

#126Corey Huinker
corey.huinker@gmail.com
In reply to: Tom Lane (#123)
Re: Statistics Import and Export

Ah, yeah, you could change the function to have more parameters,
given the assumption that all calls will be named-parameter style.
I still suggest that my proposal is more robust for the case where
the dump lists parameters that the receiving system doesn't have.

So what's the behavior when the user fails to supply a parameter that is
currently NOT NULL checked (example: avg_witdth)? Is that a WARN-and-exit?

#127Tom Lane
tgl@sss.pgh.pa.us
In reply to: Corey Huinker (#126)
Re: Statistics Import and Export

Corey Huinker <corey.huinker@gmail.com> writes:

So what's the behavior when the user fails to supply a parameter that is
currently NOT NULL checked (example: avg_witdth)? Is that a WARN-and-exit?

I still think that we could just declare the function strict, if we
use the variadic-any approach. Passing a null in any position is
indisputable caller error. However, if you're allergic to silently
doing nothing in such a case, we could have pg_set_attribute_stats
check each argument and throw an error. (Or warn and keep going;
but according to the design principle I posited earlier, this'd be
the sort of thing we don't need to tolerate.)

regards, tom lane

#128Corey Huinker
corey.huinker@gmail.com
In reply to: Tom Lane (#127)
Re: Statistics Import and Export

I still think that we could just declare the function strict, if we
use the variadic-any approach. Passing a null in any position is
indisputable caller error. However, if you're allergic to silently
doing nothing in such a case, we could have pg_set_attribute_stats
check each argument and throw an error. (Or warn and keep going;
but according to the design principle I posited earlier, this'd be
the sort of thing we don't need to tolerate.)

Any thoughts about going back to having a return value, a caller could then
see that the function returned NULL rather than whatever the expected value
was (example: TRUE)?

#129Tom Lane
tgl@sss.pgh.pa.us
In reply to: Corey Huinker (#128)
Re: Statistics Import and Export

Corey Huinker <corey.huinker@gmail.com> writes:

Any thoughts about going back to having a return value, a caller could then
see that the function returned NULL rather than whatever the expected value
was (example: TRUE)?

If we are envisioning that the function might emit multiple warnings
per call, a useful definition could be to return the number of
warnings (so zero is good, not-zero is bad). But I'm not sure that's
really better than a boolean result. pg_dump/pg_restore won't notice
anyway, but perhaps other programs using these functions would care.

regards, tom lane

#130Corey Huinker
corey.huinker@gmail.com
In reply to: Tom Lane (#129)
Re: Statistics Import and Export

If we are envisioning that the function might emit multiple warnings
per call, a useful definition could be to return the number of
warnings (so zero is good, not-zero is bad). But I'm not sure that's
really better than a boolean result. pg_dump/pg_restore won't notice
anyway, but perhaps other programs using these functions would care.

A boolean is what we had before, I'm quite comfortable with that, and it
addresses my silent-failure concerns.

#131Tom Lane
tgl@sss.pgh.pa.us
In reply to: Corey Huinker (#130)
Re: Statistics Import and Export

Corey Huinker <corey.huinker@gmail.com> writes:

A boolean is what we had before, I'm quite comfortable with that, and it
addresses my silent-failure concerns.

WFM.

regards, tom lane

#132Corey Huinker
corey.huinker@gmail.com
In reply to: Tom Lane (#131)
1 attachment(s)
Re: Statistics Import and Export

Here's a one-liner patch for disabling update of pg_class
relpages/reltuples/relallviible during a binary upgrade.

This was causting pg_upgrade tests to fail in the existing stats import
work.

Attachments:

v1-0001-Disable-updating-pg_class-for-CREATE-INDEX-during.patchtext/x-patch; charset=US-ASCII; name=v1-0001-Disable-updating-pg_class-for-CREATE-INDEX-during.patchDownload
From 322db8c9e8796ce673f7d7b63bd64e4c9403a144 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Mon, 1 Apr 2024 18:25:18 -0400
Subject: [PATCH v1] Disable updating pg_class for CREATE INDEX during binary
 upgrade.

There is no point in setting these values because the table will always
be empty.
---
 src/backend/catalog/index.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index b6a7c60e23..f83b35add5 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2874,7 +2874,7 @@ index_update_stats(Relation rel,
 		dirty = true;
 	}
 
-	if (reltuples >= 0)
+	if ((reltuples >= 0) && (!IsBinaryUpgrade))
 	{
 		BlockNumber relpages = RelationGetNumberOfBlocks(rel);
 		BlockNumber relallvisible;
-- 
2.44.0

#133Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#132)
Re: Statistics Import and Export

On Tue, 2024-04-02 at 05:38 -0400, Corey Huinker wrote:

Here's a one-liner patch for disabling update of pg_class
relpages/reltuples/relallviible during a binary upgrade.

This change makes sense to me regardless of the rest of the work.
Updating the relpages/reltuples/relallvisible during pg_upgrade before
the data is there will store the wrong stats.

It could use a brief comment, though.

Regards,
Jeff Davis

#134Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#133)
Re: Statistics Import and Export

I have refactored pg_set_relation_stats to be variadic, and I'm working on
pg_set_attribute_sttats, but I'm encountering an issue with the anyarray
values.

Jeff suggested looking at anyarray_send as a way of extracting the type,
and with some extra twiddling we can get and cast the type. However, some
of the ANYARRAYs have element types that are themselves arrays, and near as
I can tell, such a construct is not expressible in SQL. So, rather than
getting an anyarray of an array type, you instead get an array of one
higher dimension. Like so:

# select schemaname, tablename, attname,

substring(substring(anyarray_send(histogram_bounds) from 9 for
4)::text,2)::bit(32)::integer::regtype,

substring(substring(anyarray_send(histogram_bounds::text::text[][]) from 9
for 4)::text,2)::bit(32)::integer::regtype
from pg_stats where histogram_bounds is not null

and tablename = 'pg_proc' and attname = 'proargnames'

;

schemaname | tablename | attname | substring | substring

------------+-----------+-------------+-----------+-----------

pg_catalog | pg_proc | proargnames | text[] | text

Luckily, passing in such a value would have done all of the element
typechecking for us, so we would just move the data to an array of one less
dimension typed elem[]. If there's an easy way to do that, I don't know of
it.

What remains is just checking the input types against the expected type of
the array, stepping down the dimension if need be, and skipping if the type
doesn't meet expectations.

#135Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#134)
Re: Statistics Import and Export

On Tue, 2024-04-02 at 12:59 -0400, Corey Huinker wrote:

However, some of the ANYARRAYs have element types that are
themselves arrays, and near as I can tell, such a construct is not
expressible in SQL. So, rather than getting an anyarray of an array
type, you instead get an array of one higher dimension.

Fundamentally, you want to recreate the exact same anyarray values on
the destination system as they existed on the source. There's some
complexity to that on both the export side as well as the import side,
but I believe the problems are solvable.

On the export side, the problem is that the element type (and
dimensionality and maybe hasnull) is an important part of the anyarray
value, but it's not part of the output of anyarray_out(). For new
versions, we can add a scalar function that simply outputs the
information we need. For old versions, we can hack it by parsing the
output of anyarray_send(), which contains the information we need
(binary outputs are under-specified, but I believe they are specified
enough in this case). There may be other hacks to get the information
from the older systems; that's just an idea. To get the actual data,
doing histogram_bounds::text::text[] seems to be enough: that seems to
always give a one-dimensional array with element type "text", even if
the element type is an array. (Note: this means we need the function's
API to also include this extra information about the anyarray values,
so it might be slightly more complex than name/value pairs).

On the import side, the problem is that there may not be an input
function to go from a 1-D array of text to a 1-D array of any element
type we want. For example, there's no input function that will create a
1-D array with element type float4[] (that's because Postgres doesn't
really have arrays-of-arrays, it has multi-dimensional arrays).
Instead, don't use the input function, pass each element of the 1-D
text array to the element type's input function (which may be scalar or
not) and then construct a 1-D array out of that with the appropriate
element type (which may be scalar or not).

Regards,
Jeff Davis

#136Tom Lane
tgl@sss.pgh.pa.us
In reply to: Jeff Davis (#135)
Re: Statistics Import and Export

Jeff Davis <pgsql@j-davis.com> writes:

On the export side, the problem is that the element type (and
dimensionality and maybe hasnull) is an important part of the anyarray
value, but it's not part of the output of anyarray_out(). For new
versions, we can add a scalar function that simply outputs the
information we need. For old versions, we can hack it by parsing the
output of anyarray_send(), which contains the information we need
(binary outputs are under-specified, but I believe they are specified
enough in this case).

Yeah, I was thinking yesterday about pulling the anyarray columns in
binary and looking at the header fields. However, I fear there is a
showstopper problem: anyarray_send will fail if the element type
doesn't have a typsend function, which is entirely possible for
user-defined types (and I'm not even sure we've provided them for
every type in the core distro). I haven't thought of a good answer
to that other than a new backend function. However ...

On the import side, the problem is that there may not be an input
function to go from a 1-D array of text to a 1-D array of any element
type we want. For example, there's no input function that will create a
1-D array with element type float4[] (that's because Postgres doesn't
really have arrays-of-arrays, it has multi-dimensional arrays).
Instead, don't use the input function, pass each element of the 1-D
text array to the element type's input function (which may be scalar or
not) and then construct a 1-D array out of that with the appropriate
element type (which may be scalar or not).

Yup. I had hoped that we could avoid doing any array-munging inside
pg_set_attribute_stats, but this array-of-arrays problem seems to
mean we have to. In turn, that means that the whole idea of
declaring the function inputs as anyarray rather than text[] is
probably pointless. And that means that we don't need the sending
side to know the element type anyway. So, I apologize for sending
us down a useless side path. We may as well stick to the function
signature as shown in the v15 patch --- although maybe variadic
any is still worthwhile so that an unrecognized field name doesn't
need to be a hard error?

regards, tom lane

#137Corey Huinker
corey.huinker@gmail.com
In reply to: Tom Lane (#136)
Re: Statistics Import and Export

side to know the element type anyway. So, I apologize for sending
us down a useless side path. We may as well stick to the function
signature as shown in the v15 patch --- although maybe variadic
any is still worthwhile so that an unrecognized field name doesn't
need to be a hard error?

Variadic is nearly done. This issue was the main blocking point. I can go
back to array_in() as we know that code works.

#138Jeff Davis
pgsql@j-davis.com
In reply to: Tom Lane (#136)
Re: Statistics Import and Export

On Tue, 2024-04-02 at 17:31 -0400, Tom Lane wrote:

And that means that we don't need the sending
side to know the element type anyway.

We need to get the original element type on the import side somehow,
right? Otherwise it will be hard to tell whether '{1, 2, 3, 4}' has
element type "int4" or "text", which affects the binary representation
of the anyarray value in pg_statistic.

Either we need to get it at export time (which seems the most reliable
in principle, but problematic for older versions) and pass it as an
argument to pg_set_attribute_stats(); or we need to derive it reliably
from the table schema on the destination side, right?

Regards,
Jeff Davis

#139Tom Lane
tgl@sss.pgh.pa.us
In reply to: Jeff Davis (#138)
Re: Statistics Import and Export

Jeff Davis <pgsql@j-davis.com> writes:

We need to get the original element type on the import side somehow,
right? Otherwise it will be hard to tell whether '{1, 2, 3, 4}' has
element type "int4" or "text", which affects the binary representation
of the anyarray value in pg_statistic.

Yeah, but that problem exists no matter what. I haven't read enough
of the patch to find where it's determining that, but I assume there's
code in there to intuit the statistics storage type depending on the
table column's data type and the statistics kind.

Either we need to get it at export time (which seems the most reliable
in principle, but problematic for older versions) and pass it as an
argument to pg_set_attribute_stats(); or we need to derive it reliably
from the table schema on the destination side, right?

We could not trust the exporting side to tell us the correct answer;
for one reason, it might be different across different releases.
So "derive it reliably on the destination" is really the only option.

I think that it's impossible to do this in the general case, since
type-specific typanalyze functions can store pretty nearly whatever
they like. However, the pg_stats view isn't going to show nonstandard
statistics kinds anyway, so we are going to be lossy for custom
statistics kinds.

regards, tom lane

#140Corey Huinker
corey.huinker@gmail.com
In reply to: Tom Lane (#139)
Re: Statistics Import and Export

Yeah, but that problem exists no matter what. I haven't read enough
of the patch to find where it's determining that, but I assume there's
code in there to intuit the statistics storage type depending on the
table column's data type and the statistics kind.

Correct. It borrows a lot from examine_attribute() and the *_typanalyze()
functions. Actually using VacAttrStats proved problematic, but that can be
revisited at some point.

We could not trust the exporting side to tell us the correct answer;
for one reason, it might be different across different releases.
So "derive it reliably on the destination" is really the only option.

+1

I think that it's impossible to do this in the general case, since
type-specific typanalyze functions can store pretty nearly whatever
they like. However, the pg_stats view isn't going to show nonstandard
statistics kinds anyway, so we are going to be lossy for custom
statistics kinds.

Sadly true.

#141Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#140)
3 attachment(s)
Re: Statistics Import and Export

v16 attached.

- both functions now use variadics for anything that can be considered a
stat.
- most consistency checks removed, null element tests remain
- functions strive to not ERROR unless absolutely necessary. The biggest
exposure is the call to array_in().
- docs have not yet been updated, pending general acceptance of the
variadic over the named arg version.

Having variant arguments is definitely a little bit more work to manage,
and the shift from ERROR to WARN removes a lot of the easy exits that it
previously had, as well as having to do some extra type checking that we
got for free with fixed arguments. Still, I don't think the readability
suffers too much, and we are now able to work for downgrades as well as
upgrades.

Attachments:

v16-0001-Disable-updating-pg_class-for-CREATE-INDEX-durin.patchtext/x-patch; charset=US-ASCII; name=v16-0001-Disable-updating-pg_class-for-CREATE-INDEX-durin.patchDownload
From d98a95243fd3114af96fc69126581ca591df9556 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 3 Apr 2024 00:48:03 -0400
Subject: [PATCH v16 1/3] Disable updating pg_class for CREATE INDEX during
 binary upgrade.

There is no point in setting these values because the table will always
be empty.
---
 src/backend/catalog/index.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index b6a7c60e23..67d6bf0342 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2874,7 +2874,8 @@ index_update_stats(Relation rel,
 		dirty = true;
 	}
 
-	if (reltuples >= 0)
+	/* During binary upgrade, tables are always empty. */
+	if ((reltuples >= 0) && (!IsBinaryUpgrade))
 	{
 		BlockNumber relpages = RelationGetNumberOfBlocks(rel);
 		BlockNumber relallvisible;
-- 
2.44.0

v16-0002-Create-pg_set_relation_stats-pg_set_attribute_st.patchtext/x-patch; charset=US-ASCII; name=v16-0002-Create-pg_set_relation_stats-pg_set_attribute_st.patchDownload
From d548bb929e8d1ceef3edc965f89ce3454592f57f Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 21 Mar 2024 18:04:54 -0400
Subject: [PATCH v16 2/3] Create pg_set_relation_stats, pg_set_attribute_stats.

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

Both functions take an oid to identify the target relation that will
receive the statistics. There is nothing requiring that relation to be
the same one as the one exported, though the statistics have to make
sense in the context of the new relation.

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

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

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

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

These functions also allows for tweaking of table statistics in-place,
allowing the user to inflate rowcounts, skew histograms, etc, to see
what those changes will evoke from the query planner.
---
 src/include/catalog/pg_proc.dat               |   22 +-
 src/include/statistics/statistics.h           |    2 +
 src/backend/statistics/Makefile               |    3 +-
 src/backend/statistics/meson.build            |    1 +
 src/backend/statistics/statistics.c           | 1183 +++++++++++++++++
 .../regress/expected/stats_export_import.out  | 1181 ++++++++++++++++
 src/test/regress/parallel_schedule            |    3 +-
 src/test/regress/sql/stats_export_import.sql  |  894 +++++++++++++
 doc/src/sgml/func.sgml                        |  122 ++
 9 files changed, 3406 insertions(+), 5 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 153d816a05..c7212cbb7a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12183,9 +12183,6 @@
   proallargtypes => '{int8,pg_lsn,pg_lsn,oid,oid,oid,int2,int8,bool}',
   proargmodes => '{i,i,i,o,o,o,o,o,o}',
   proargnames => '{tli,start_lsn,end_lsn,relfilenode,reltablespace,reldatabase,relforknumber,relblocknumber,is_limit_block}',
-  prosrc => 'pg_wal_summary_contents' },
-{ oid => '8438',
-  descr => 'WAL summarizer state',
   proname => 'pg_get_wal_summarizer_state',
   provolatile => 'v', proparallel => 's',
   prorettype => 'record', proargtypes => '',
@@ -12200,4 +12197,23 @@
   proargtypes => 'int2',
   prosrc => 'gist_stratnum_identity' },
 
+# Statistics Import
+{ oid => '8048',
+  descr => 'set statistics on relation',
+  proname => 'pg_set_relation_stats', provariadic => 'any',
+  proisstrict => 't', provolatile => 'v',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'regclass any',
+  proargnames => '{relation,stats}',
+  proargmodes => '{i,v}',
+  prosrc => 'pg_set_relation_stats' },
+{ oid => '8049',
+  descr => 'set statistics on attribute',
+  proname => 'pg_set_attribute_stats', provariadic => 'any',
+  proisstrict => 't', provolatile => 'v',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'regclass name bool any',
+  proargnames => '{relation,attname,inherited,stats}',
+  proargmodes => '{i,i,i,v}',
+  prosrc => 'pg_set_attribute_stats' },
 ]
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 7f2bf18716..1dddf96576 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -127,4 +127,6 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												int nclauses);
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
+extern Datum pg_set_relation_stats(PG_FUNCTION_ARGS);
+extern Datum pg_set_attribute_stats(PG_FUNCTION_ARGS);
 #endif							/* STATISTICS_H */
diff --git a/src/backend/statistics/Makefile b/src/backend/statistics/Makefile
index 89cf8c2797..e4f8ab7c4f 100644
--- a/src/backend/statistics/Makefile
+++ b/src/backend/statistics/Makefile
@@ -16,6 +16,7 @@ OBJS = \
 	dependencies.o \
 	extended_stats.o \
 	mcv.o \
-	mvdistinct.o
+	mvdistinct.o \
+	statistics.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/statistics/meson.build b/src/backend/statistics/meson.build
index 73b29a3d50..331e82c776 100644
--- a/src/backend/statistics/meson.build
+++ b/src/backend/statistics/meson.build
@@ -5,4 +5,5 @@ backend_sources += files(
   'extended_stats.c',
   'mcv.c',
   'mvdistinct.c',
+  'statistics.c',
 )
diff --git a/src/backend/statistics/statistics.c b/src/backend/statistics/statistics.c
new file mode 100644
index 0000000000..bbe747a7e6
--- /dev/null
+++ b/src/backend/statistics/statistics.c
@@ -0,0 +1,1183 @@
+/*-------------------------------------------------------------------------
+ * statistics.c
+ *
+ *	  POSTGRES statistics import
+ *
+ * Code supporting the direct importation of relation statistics, similar to
+ * what is done by the ANALYZE command.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *       src/backend/statistics/statistics.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "catalog/pg_collation.h"
+#include "catalog/pg_database.h"
+#include "catalog/pg_operator.h"
+#include "catalog/pg_type.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_type.h"
+#include "fmgr.h"
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "nodes/nodeFuncs.h"
+#include "statistics/statistics.h"
+#include "utils/acl.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/float.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/rangetypes.h"
+#include "utils/syscache.h"
+#include "utils/typcache.h"
+
+/*
+ * A role has privileges to set statistics on the relation if any of the
+ * following are true:
+ *   - the role owns the current database and the relation is not shared
+ *   - the role has the MAINTAIN privilege on the relation
+ *
+ */
+static bool
+can_modify_relation(Relation rel)
+{
+	Oid			relid = RelationGetRelid(rel);
+	Form_pg_class reltuple = rel->rd_rel;
+
+	return ((object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId())
+			 && !reltuple->relisshared) ||
+			pg_class_aclcheck(relid, GetUserId(), ACL_MAINTAIN) == ACLCHECK_OK);
+}
+
+/*
+ * Set statistics for a given pg_class entry.
+ *
+ * This does an in-place (i.e. non-transactional) update of pg_class, just as
+ * is done in ANALYZE.
+ *
+ */
+Datum
+pg_set_relation_stats(PG_FUNCTION_ARGS)
+{
+	Oid			relid = PG_GETARG_OID(0);
+
+	/* build argument values to build the object */
+	Datum	   *args;
+	bool	   *nulls;
+	Oid		   *types;
+	int			nargs = extract_variadic_args(fcinfo, 1, true,
+											  &args, &types, &nulls);
+
+	Relation	rel;
+
+	/* indexes of where we found required stats */
+	int			i_relpages = 0;
+	int			i_reltuples = 0;
+	int			i_relallvisible = 0;
+
+	/* if the pairs aren't pairs, something is malformed */
+	if (nargs % 2 == 1)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("stats parameters must be in name-value pairs")));
+		PG_RETURN_BOOL(false);
+	}
+
+	for (int i = 0; i < nargs; i++)
+		if (nulls[i])
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot contain NULL elements", "stats")));
+			PG_RETURN_BOOL(false);
+		}
+
+	/*
+	 * Open the relation, getting ShareUpdateExclusiveLock to ensure that no
+	 * other stat-setting operation can run on it concurrently.
+	 */
+	rel = table_open(RelationRelationId, ShareUpdateExclusiveLock);
+
+	if (!can_modify_relation(rel))
+	{
+		table_close(rel, NoLock);
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("must be owner to modify relation stats")));
+		PG_RETURN_BOOL(false);
+	}
+
+	for (int i = 0; i < nargs; i += 2)
+	{
+		char	   *statname;
+
+		if (types[i] != TEXTOID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("stat names must be of type text")));
+			continue;
+		}
+		statname = TextDatumGetCString(args[i]);
+
+		if (strcmp(statname, "relpages") == 0)
+			i_relpages = (types[i + 1] == INT4OID) ? i + 1 : -1;
+		else if (strcmp(statname, "reltuples") == 0)
+			i_reltuples = (types[i + 1] == FLOAT4OID) ? i + 1 : -1;
+		else if (strcmp(statname, "relallvisible") == 0)
+			i_relallvisible = (types[i + 1] == INT4OID) ? i + 1 : -1;
+		else
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("unknown stat naame '%s', skipping", statname)));
+
+		pfree(statname);
+	}
+
+	/* only modify if all required values were present */
+	if ((i_relpages > 0) && (i_reltuples > 0) && (i_relallvisible > 0))
+	{
+		HeapTuple	ctup;
+		Form_pg_class pgcform;
+		int			relpages = DatumGetInt32(args[i_relpages]);
+		float4		reltuples = DatumGetFloat4(args[i_reltuples]);
+		int			relallvisible = DatumGetInt32(args[i_relallvisible]);
+
+		ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+		if (!HeapTupleIsValid(ctup))
+			ereport(ERROR,
+					(errcode(ERRCODE_OBJECT_IN_USE),
+					 errmsg("pg_class entry for relid %u not found", relid)));
+
+		pgcform = (Form_pg_class) GETSTRUCT(ctup);
+		/* Only update pg_class if there is a meaningful change */
+		if ((pgcform->reltuples != reltuples)
+			|| (pgcform->relpages != relpages)
+			|| (pgcform->relallvisible != relallvisible))
+		{
+			pgcform->relpages = relpages;
+			pgcform->reltuples = reltuples;
+			pgcform->relallvisible = relallvisible;
+
+			heap_inplace_update(rel, ctup);
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("wrote")));
+		}
+
+		table_close(rel, NoLock);
+		PG_RETURN_BOOL(true);
+	}
+	else
+	{
+		/* don't flood the user will all errors, just one will do */
+		if (i_relpages == 0)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("required parameter %s not set", "relpages")));
+		else if (i_relpages == -1)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("relpages must be of type integer")));
+		else if (i_reltuples == 0)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("required parameter %s not set", "reltuples")));
+		else if (i_reltuples == -1)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("reltuples must be of type real")));
+		else if (i_relallvisible == 0)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("required parameter %s not set", "relallvisible")));
+		else if (i_relallvisible == -1)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("relallvisible must be of type integer")));
+	}
+
+	table_close(rel, NoLock);
+	PG_RETURN_BOOL(false);
+}
+
+/*
+ * Test if the type is a scalar for MCELEM purposes
+ */
+static bool
+type_is_scalar(Oid typid)
+{
+	HeapTuple	tp;
+	bool		result = false;
+
+	tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+	if (HeapTupleIsValid(tp))
+	{
+		Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+
+		result = (!OidIsValid(typtup->typanalyze));
+		ReleaseSysCache(tp);
+	}
+	return result;
+}
+
+/*
+ * Fetch datatype information, this is needed to derive the proper staopN
+ * and stacollN values.
+ *
+ * If this relation is an index and that index has expressions in it, and
+ * the attnum specified is known to be an expression, then we must walk
+ * the list attributes up to the specified attnum to get the right
+ * expression.
+ *
+ */
+static TypeCacheEntry *
+get_attr_stat_type(Relation rel, Name attname,
+				   int16 *attnum, int32 *typmod, Oid *typcoll)
+{
+	Oid			relid = RelationGetRelid(rel);
+	Form_pg_attribute attr;
+	HeapTuple	atup;
+	Oid			typid;
+
+	atup = SearchSysCache2(ATTNAME, ObjectIdGetDatum(relid),
+						   NameGetDatum(attname));
+
+	/* Attribute not found */
+	if (!HeapTupleIsValid(atup))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s has no attname %s",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+		return NULL;
+	}
+
+	attr = (Form_pg_attribute) GETSTRUCT(atup);
+	if (attr->attisdropped)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s attname %s is dropped",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+		return NULL;
+	}
+	*attnum = attr->attnum;
+
+	if ((rel->rd_rel->relkind == RELKIND_INDEX
+		 || (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+		&& (rel->rd_indexprs != NIL)
+		&& (rel->rd_index->indkey.values[attr->attnum - 1] == 0))
+	{
+		ListCell   *indexpr_item = list_head(rel->rd_indexprs);
+		Node	   *expr;
+
+		for (int i = 0; i < attr->attnum - 1; i++)
+			if (rel->rd_index->indkey.values[i] == 0)
+				indexpr_item = lnext(rel->rd_indexprs, indexpr_item);
+
+		if (indexpr_item == NULL)	/* shouldn't happen */
+			elog(ERROR, "too few entries in indexprs list");
+
+		expr = (Node *) lfirst(indexpr_item);
+
+		typid = exprType(expr);
+		*typmod = exprTypmod(expr);
+
+		/*
+		 * If a collation has been specified for the index column, use that in
+		 * preference to anything else; but if not, fall back to whatever we
+		 * can get from the expression.
+		 */
+		if (OidIsValid(attr->attcollation))
+			*typcoll = attr->attcollation;
+		else
+			*typcoll = exprCollation(expr);
+	}
+	else
+	{
+		typid = attr->atttypid;
+		*typmod = attr->atttypmod;
+		*typcoll = attr->attcollation;
+	}
+	ReleaseSysCache(atup);
+
+	/* if it's a multirange, step down to the range type */
+	if (type_is_multirange(typid))
+		typid = get_multirange_range(typid);
+
+	return lookup_type_cache(typid,
+							 TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR);
+}
+
+/*
+ * Perform the cast of text to some array type
+ */
+static Datum
+cast_stavalues(FmgrInfo *finfo, Datum d, Oid typid, int32 typmod)
+{
+	char	   *s = TextDatumGetCString(d);
+	Datum		out = FunctionCall3(finfo, CStringGetDatum(s),
+									ObjectIdGetDatum(typid),
+									Int32GetDatum(typmod));
+
+	pfree(s);
+
+	return out;
+}
+
+
+
+/*
+ * Check array for any NULLs, and optionally for one-dimensionality
+ */
+static bool
+array_check(Datum datum, int one_dim, const char *statname)
+{
+	ArrayType  *arr = DatumGetArrayTypeP(datum);
+	int16		elmlen;
+	char		elmalign;
+	bool		elembyval;
+	Datum	   *values;
+	bool	   *nulls;
+	int			nelems;
+
+	if (one_dim && (arr->ndim != 1))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be a multidimensional array", statname)));
+		return false;
+	}
+
+	get_typlenbyvalalign(ARR_ELEMTYPE(arr), &elmlen, &elembyval, &elmalign);
+
+	deconstruct_array(arr, ARR_ELEMTYPE(arr), elmlen, elembyval, elmalign,
+					  &values, &nulls, &nelems);
+
+	for (int i = 0; i < nelems; i++)
+		if (nulls[i])
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s array cannot contain NULL values", statname)));
+			return false;
+		}
+	return true;
+}
+
+/*
+ * Update the pg_statistic record.
+ */
+static void
+update_pg_statistic(Datum values[], bool nulls[])
+{
+	Relation	sd = table_open(StatisticRelationId, RowExclusiveLock);
+	CatalogIndexState indstate = CatalogOpenIndexes(sd);
+	HeapTuple	oldtup;
+	HeapTuple	stup;
+
+	/* Is there already a pg_statistic tuple for this attribute? */
+	oldtup = SearchSysCache3(STATRELATTINH,
+							 values[Anum_pg_statistic_starelid - 1],
+							 values[Anum_pg_statistic_staattnum - 1],
+							 values[Anum_pg_statistic_stainherit - 1]);
+
+	if (HeapTupleIsValid(oldtup))
+	{
+		/* Yes, replace it */
+		bool		replaces[Natts_pg_statistic];
+
+		for (int i = 0; i < Natts_pg_statistic; i++)
+			replaces[i] = true;
+
+		stup = heap_modify_tuple(oldtup, RelationGetDescr(sd),
+								 values, nulls, replaces);
+		ReleaseSysCache(oldtup);
+		CatalogTupleUpdateWithInfo(sd, &stup->t_self, stup, indstate);
+	}
+	else
+	{
+		/* No, insert new tuple */
+		stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
+		CatalogTupleInsertWithInfo(sd, stup, indstate);
+	}
+
+	heap_freetuple(stup);
+	CatalogCloseIndexes(indstate);
+	table_close(sd, NoLock);
+}
+
+/*
+ * Import statistics for a given relation attribute.
+ *
+ * This will insert/replace a row in pg_statistic for the given relation and
+ * attribute name.
+ *
+ * The function takes input parameters that correspond to columns in the view
+ * pg_stats.
+ *
+ * Of those, the columns relation, attname, inherited, null_frac, avg_width,
+ * and n_distinct all correspond to NOT NULL columns in pg_statistic. These
+ * parameters have no default value and passing NULL to them will result
+ * in an error.
+ *
+ * If there is no attribute with a matching attname in the relation, the
+ * function will raise an error. Likewise for setting inherited statistics
+ * on a table that is not partitioned.
+ *
+ * The remaining parameters all belong to a specific stakind. Some stakinds
+ * have multiple parameters, and in those cases both parameters must be
+ * NOT NULL or both NULL, otherwise an error will be raised.
+ *
+ * Omitting a parameter or explicitly passing NULL means that that particular
+ * stakind is not associated with the attribute.
+ *
+ * Parameters that are NOT NULL will be inspected for consistency checks,
+ * any of which can raise an error.
+ *
+ * Parameters corresponding to ANYARRAY columns are instead passed in as text
+ * values, which is a valid input string for an array of the type or element
+ * type of the attribute. Any error generated by the array_in() function will
+ * in turn fail the function.
+ */
+Datum
+pg_set_attribute_stats(PG_FUNCTION_ARGS)
+{
+	Oid			relid = PG_GETARG_OID(0);
+	Name		attname = PG_GETARG_NAME(1);
+	bool		inherited = PG_GETARG_BOOL(2);
+
+	/* build argument values to build the object */
+	Datum	   *args;
+	bool	   *pnulls;
+	Oid		   *types;
+	int			nargs = extract_variadic_args(fcinfo, 3, true,
+											  &args, &types, &pnulls);
+
+	Datum		values[Natts_pg_statistic] = {0};
+	bool		nulls[Natts_pg_statistic] = {false};
+
+	/* 0 = not found, -1 = error, n > 0 = found */
+	/* parameters that are required get indexes */
+	int			i_null_frac = 0;
+	int			i_avg_width = 0;
+	int			i_n_distinct = 0;
+
+	/* stakind stats do not */
+	int			i_mc_vals = 0;
+	int			i_mc_freqs = 0;
+	int			i_hist_bounds = 0;
+	int			i_correlation = 0;
+	int			i_mc_elems = 0;
+	int			i_mc_elem_freqs = 0;
+	int			i_elem_count_hist = 0;
+	int			i_range_length_hist = 0;
+	int			i_range_empty_frac = 0;
+	int			i_range_bounds_hist = 0;
+
+	Relation	rel;
+
+	TypeCacheEntry *typcache;
+	TypeCacheEntry *elemtypcache;
+
+	int16		attnum;
+	int32		typmod;
+	Oid			typcoll;
+
+	FmgrInfo	finfo;
+
+	int			stakind_count;
+
+	int			k = 0;
+
+	/* if the pairs aren't pairs, something is malformed */
+	if (nargs % 2 == 1)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("stats parameters must be in name-value pairs")));
+		PG_RETURN_BOOL(false);
+	}
+
+	for (int i = 0; i < nargs; i++)
+		if (pnulls[i])
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot contain NULL elements", "stats")));
+			PG_RETURN_BOOL(false);
+		}
+
+	for (int i = 0; i < nargs; i += 2)
+	{
+		char	   *statname;
+		int			argidx = i + 1;
+		Oid			argtype = types[argidx];
+		Datum		argval = args[argidx];
+
+		if (types[i] != TEXTOID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("stat names must be of type text")));
+			break;
+		}
+		statname = TextDatumGetCString(args[i]);
+
+		if (strcmp(statname, "null_frac") == 0)
+		{
+			if (argtype == FLOAT4OID)
+			{
+				i_null_frac = argidx;	/* mark found */
+				values[Anum_pg_statistic_stanullfrac - 1] = argval;
+			}
+			else
+			{
+				/* required param, not recoverable */
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be of type %s", "null_frac",
+								"real")));
+				PG_RETURN_BOOL(false);
+			}
+		}
+
+		else if (strcmp(statname, "avg_width") == 0)
+		{
+			if (argtype == INT4OID)
+			{
+				i_avg_width = argidx;	/* mark found */
+				values[Anum_pg_statistic_stawidth - 1] = argval;
+			}
+			else
+			{
+				/* required param, not recoverable */
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be of type %s", "avg_width",
+								"integer")));
+				PG_RETURN_BOOL(false);
+			}
+		}
+		else if (strcmp(statname, "n_distinct") == 0)
+		{
+			if (argtype == FLOAT4OID)
+			{
+				i_n_distinct = argidx;	/* mark found */
+				values[Anum_pg_statistic_stadistinct - 1] = argval;
+			}
+			else
+			{
+				/* required param, not recoverable */
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be of type %s", "n_distinct",
+								"real")));
+				PG_RETURN_BOOL(false);
+			}
+		}
+
+		/*
+		 * For the stakind parameters, we have a specific order that we want
+		 * them in, so just scan to get indexes
+		 */
+		else if (strcmp(statname, "most_common_vals") == 0)
+			i_mc_vals = argidx;
+		else if (strcmp(statname, "most_common_freqs") == 0)
+			i_mc_freqs = argidx;
+		else if (strcmp(statname, "histogram_bounds") == 0)
+			i_hist_bounds = argidx;
+		else if (strcmp(statname, "correlation") == 0)
+			i_correlation = argidx;
+		else if (strcmp(statname, "most_common_elems") == 0)
+			i_mc_elems = argidx;
+		else if (strcmp(statname, "most_common_elem_freqs") == 0)
+			i_mc_elem_freqs = argidx;
+		else if (strcmp(statname, "elem_count_histogram") == 0)
+			i_elem_count_hist = argidx;
+		else if (strcmp(statname, "range_length_histogram") == 0)
+			i_range_length_hist = argidx;
+		else if (strcmp(statname, "range_empty_frac") == 0)
+			i_range_empty_frac = argidx;
+		else if (strcmp(statname, "range_bounds_histogram") == 0)
+			i_range_bounds_hist = argidx;
+		else
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("unknown stat naame '%s', skipping", statname)));
+
+		pfree(statname);
+	}
+
+	/* check all required parameters */
+	if (i_null_frac == 0)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "null_frac")));
+		PG_RETURN_BOOL(false);
+	}
+	if (i_avg_width == 0)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "avg_width")));
+		PG_RETURN_BOOL(false);
+	}
+	if (i_n_distinct == 0)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "n_distinct")));
+		PG_RETURN_BOOL(false);
+	}
+
+	/* look for pair mismatches */
+	if ((i_mc_vals == 0) != (i_mc_freqs == 0))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be present if %s is missing",
+						(i_mc_vals == 0) ? "most_common_freqs" :
+						"most_common_vals",
+						(i_mc_vals == 0) ? "most_common_vals" :
+						"most_common_freqs")));
+		PG_RETURN_BOOL(false);
+	}
+	if ((i_mc_elems == 0) != (i_mc_elem_freqs == 0))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be present if %s is missing",
+						(i_mc_elems == 0) ?
+						"most_common_elem_freqs" :
+						"most_common_elems",
+						(i_mc_elems == 0) ?
+						"most_common_elems" :
+						"most_common_elem_freqs")));
+		PG_RETURN_BOOL(false);
+	}
+	if ((i_range_length_hist == 0) != (i_range_empty_frac == 0))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be present if %s is missing",
+						(i_range_length_hist == 0) ?
+						"range_empty_frac" :
+						"range_length_histogram",
+						(i_range_length_hist == 0) ?
+						"range_length_histogram" :
+						"range_empty_frac")));
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * count the number of stakinds we want to set, paired params count as
+	 * one. The count cannot exceed STATISTIC_NUM_SLOTS.
+	 */
+	stakind_count = (int) (i_mc_vals > 0) +
+		(int) (i_mc_elems > 0) +
+		(int) (i_range_length_hist > 0) +
+		(int) (i_hist_bounds > 0) +
+		(int) (i_correlation > 0) +
+		(int) (i_elem_count_hist > 0) +
+		(int) (i_range_bounds_hist > 0);
+
+	if (stakind_count > STATISTIC_NUM_SLOTS)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("imported statistics must have a maximum of %d slots "
+						"but %d given",
+						STATISTIC_NUM_SLOTS, stakind_count)));
+		PG_RETURN_BOOL(false);
+	}
+
+	rel = relation_open(relid, ShareUpdateExclusiveLock);
+
+	if (!can_modify_relation(rel))
+	{
+		relation_close(rel, NoLock);
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						RelationGetRelationName(rel))));
+		PG_RETURN_BOOL(false);
+	}
+
+	typcache = get_attr_stat_type(rel, attname, &attnum, &typmod, &typcoll);
+	if (typcache == NULL)
+		PG_RETURN_BOOL(false);
+
+
+	/*
+	 * Derive element type if we have stats kinds that need it.
+	 *
+	 * This duplicates some type-specific logic found in various typanalyze
+	 * functions which are called from vacuum's examine_attribute(), but using
+	 * that directly has proven awkward, as it leaves out other other
+	 * information about the element type that we need for data validation
+	 * purposes.
+	 */
+	if ((i_mc_elems > 0) || (i_elem_count_hist > 0))
+	{
+		Oid			elemtypid;
+
+		if (typcache->type_id == TSVECTOROID)
+		{
+			/*
+			 * tsvector elems always have a text oid type and default
+			 * collation
+			 */
+			elemtypid = TEXTOID;
+			typcoll = DEFAULT_COLLATION_OID;
+		}
+		else if (typcache->typtype == TYPTYPE_RANGE)
+			elemtypid = get_range_subtype(typcache->type_id);
+		else
+			elemtypid = get_base_element_type(typcache->type_id);
+
+		/* not finding a basetype means we already had it */
+		if (elemtypid == InvalidOid)
+			elemtypid = typcache->type_id;
+
+		/* The stats need the eq_opr, but validation needs the lt_opr */
+		elemtypcache = lookup_type_cache(elemtypid,
+										 TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR);
+	}
+
+	/*
+	 * histogram_bounds and correlation must have a type < operator. WARN and
+	 * skip.
+	 */
+	if (typcache->lt_opr == InvalidOid)
+	{
+		if (i_hist_bounds > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"histogram_bounds")));
+			i_hist_bounds = -1;
+		}
+		if (i_correlation > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"correlation")));
+			i_correlation = -1;
+		}
+	}
+
+	/*
+	 * Scalar types can't have most_common_elems, most_common_elem_freqs,
+	 * elem_count_histogram. WARN and skip.
+	 */
+	if (type_is_scalar(typcache->type_id))
+	{
+		if (i_mc_elems > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"most_common_elems")));
+			i_mc_elems = -1;
+		}
+		if (i_mc_elem_freqs > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"most_common_elem_freqs")));
+			i_mc_elem_freqs = -1;
+		}
+		if (i_elem_count_hist > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"elem_count_histogram")));
+			i_elem_count_hist = -1;
+		}
+	}
+
+	/*
+	 * Only range types can have range_length_histogram, range_empty_frac, and
+	 * range_bounds_histogram. WARN and skip
+	 */
+	if ((typcache->typtype != TYPTYPE_RANGE) &&
+		(typcache->typtype != TYPTYPE_RANGE))
+	{
+		if (i_range_length_hist > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"range_length_histogram")));
+			i_range_length_hist = -1;
+		}
+		if (i_range_empty_frac > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"range_empty_frac")));
+			i_range_empty_frac = -1;
+		}
+		if (i_range_bounds_hist > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"range_bounds_histogram")));
+			i_range_bounds_hist = -1;
+		}
+	}
+
+	values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(relid);
+	values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(attnum);
+	values[Anum_pg_statistic_stainherit - 1] = BoolGetDatum(inherited);
+
+	fmgr_info(F_ARRAY_IN, &finfo);
+
+	/*
+	 * STATISTIC_KIND_MCV
+	 *
+	 * most_common_freqs:	real[] most_common_vals:	ANYARRAY::text
+	 */
+	if (i_mc_vals > 0)
+	{
+		bool		ok = true;
+		Oid			numberstype = types[i_mc_freqs];
+		Oid			valuestype = types[i_mc_vals];
+		Datum		stanumbers = args[i_mc_freqs];
+		Datum		strvalue = args[i_mc_vals];
+
+		if (get_element_type(numberstype) != FLOAT4OID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"most_common_freqs", "real[]")));
+			ok = false;
+		}
+		else if (valuestype != TEXTOID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"most_common_vals", "text")));
+			ok = false;
+		}
+
+		if (ok)
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_MCV);
+			Datum		staop = ObjectIdGetDatum(typcache->eq_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		stavalues = cast_stavalues(&finfo, strvalue,
+												   typcache->type_id, typmod);
+
+			if (array_check(stavalues, false, "most_common_vals") &&
+				array_check(stanumbers, true, "most_common_freqs"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_HISTOGRAM
+	 *
+	 * histogram_bounds: ANYARRAY::text
+	 */
+	if (i_hist_bounds > 0)
+	{
+		Oid			valuestype = types[i_hist_bounds];
+		Datum		strvalue = args[i_hist_bounds];
+
+		if (valuestype == TEXTOID)
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_HISTOGRAM);
+			Datum		staop = ObjectIdGetDatum(typcache->lt_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		stavalues = cast_stavalues(&finfo, strvalue,
+												   typcache->type_id, typmod);
+
+			if (array_check(stavalues, false, "histogram_bounds"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+		else
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"histogram_bounds", "text")));
+	}
+
+	/*
+	 * STATISTIC_KIND_CORRELATION
+	 *
+	 * correlation: real
+	 */
+	if (i_correlation > 0)
+	{
+		if (types[i_correlation] == FLOAT4OID)
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_CORRELATION);
+			Datum		staop = ObjectIdGetDatum(typcache->lt_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		elems[] = {args[i_correlation]};
+			ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+			Datum		stanumbers = PointerGetDatum(arry);
+
+			values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+			values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+			values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+			values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+			nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+			k++;
+		}
+		else
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be of type %s, ignored",
+							"correlation", "real")));
+	}
+
+	/*
+	 * STATISTIC_KIND_MCELEM
+	 *
+	 * most_common_elem_freqs:	real[] most_common_elems:		ANYARRAY::text
+	 */
+	if (i_mc_elems > 0)
+	{
+		bool		ok = true;
+		Oid			numberstype = types[i_mc_elem_freqs];
+		Oid			valuestype = types[i_mc_elems];
+		Datum		stanumbers = args[i_mc_elem_freqs];
+
+		if (get_element_type(numberstype) != FLOAT4OID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"most_common_elem_freqs", "real[]")));
+			ok = false;
+		}
+		else if (valuestype != TEXTOID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"most_common_elems", "text")));
+			ok = false;
+		}
+
+		if (ok)
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_MCELEM);
+			Datum		staop = ObjectIdGetDatum(elemtypcache->eq_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		strvalue = args[i_mc_elems];
+			Datum		stavalues = cast_stavalues(&finfo, strvalue,
+												   elemtypcache->type_id,
+												   typmod);
+
+			if (array_check(stavalues, false, "most_common_elems") &&
+				array_check(stanumbers, true, "most_common_elem_freqs"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_DECHIST
+	 *
+	 * elem_count_histogram:	real[]
+	 */
+	if (i_elem_count_hist > 0)
+	{
+		Oid			numberstype = types[i_elem_count_hist];
+
+		if (get_element_type(numberstype) == FLOAT4OID)
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_DECHIST);
+			Datum		staop = ObjectIdGetDatum(elemtypcache->eq_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		stanumbers = args[i_elem_count_hist];
+
+			if (array_check(stanumbers, true, "elem_count_histogram"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+				nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+				k++;
+			}
+		}
+		else
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"elem_count_histogram", "real[]")));
+	}
+
+	/*
+	 * STATISTIC_KIND_BOUNDS_HISTOGRAM
+	 *
+	 * range_bounds_histogram:	ANYARRAY::text
+	 *
+	 * This stakind appears before STATISTIC_KIND_BOUNDS_HISTOGRAM even though
+	 * it is numerically greater, and all other stakinds appear in numerical
+	 * order. We duplicate this quirk to make before/after tests of
+	 * pg_statistic records easier.
+	 */
+	if (i_range_bounds_hist > 0)
+	{
+		Oid			valuestype = types[i_range_bounds_hist];
+
+		if (valuestype == TEXTOID)
+		{
+
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_BOUNDS_HISTOGRAM);
+			Datum		staop = ObjectIdGetDatum(InvalidOid);
+			Datum		stacoll = ObjectIdGetDatum(InvalidOid);
+			Datum		strvalue = args[i_range_bounds_hist];
+			Datum		stavalues = cast_stavalues(&finfo, strvalue,
+												   typcache->type_id, typmod);
+
+			if (array_check(stavalues, false, "range_bounds_histogram"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+		else
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"range_bounds_histogram", "text")));
+	}
+
+	/*
+	 * STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM
+	 *
+	 * range_empty_frac:        real range_length_histogram:  double
+	 * precision[]::text
+	 */
+	if (i_range_length_hist > 0)
+	{
+		bool		ok = true;
+		Oid			numberstype = types[i_range_empty_frac];
+		Oid			valuestype = types[i_range_length_hist];
+
+		if (numberstype != FLOAT4OID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"range_empty_frac", "real")));
+			ok = false;
+		}
+		else if (valuestype != TEXTOID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"range_length_histogram", "text")));
+			ok = false;
+		}
+
+		if (ok)
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM);
+			Datum		staop = ObjectIdGetDatum(Float8LessOperator);
+			Datum		stacoll = ObjectIdGetDatum(InvalidOid);
+
+			/* The anyarray is always a float8[] for this stakind */
+			Datum		elem = args[i_range_empty_frac];
+			Datum		elems[] = {elem};
+			ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+			Datum		stanumbers = PointerGetDatum(arry);
+			Datum		strvalue = args[i_range_length_hist];
+			Datum		stavalues = cast_stavalues(&finfo, strvalue, FLOAT8OID, 0);
+
+			if (array_check(stavalues, false, "range_length_histogram"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+	}
+
+	/* fill in all remaining slots */
+	for (; k < STATISTIC_NUM_SLOTS; k++)
+	{
+		values[Anum_pg_statistic_stakind1 - 1 + k] = Int16GetDatum(0);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+	}
+
+	update_pg_statistic(values, nulls);
+
+	relation_close(rel, NoLock);
+	PG_RETURN_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..2e75b4d5e9
--- /dev/null
+++ b/src/test/regress/expected/stats_export_import.out
@@ -0,0 +1,1181 @@
+CREATE SCHEMA stats_export_import;
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+        0 |        -1 |             0
+(1 row)
+
+-- error: regclass not found
+SELECT pg_set_relation_stats('stats_export_import.nope'::regclass);
+ERROR:  relation "stats_export_import.nope" does not exist
+LINE 1: SELECT pg_set_relation_stats('stats_export_import.nope'::reg...
+                                     ^
+-- error: all three params missing
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass);
+ERROR:  function pg_set_relation_stats(regclass) does not exist
+LINE 1: SELECT pg_set_relation_stats('stats_export_import.test'::reg...
+               ^
+HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+-- error: reltuples, relallvisible missing
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             'relpages', 4::integer);
+WARNING:  required parameter reltuples not set
+ pg_set_relation_stats 
+-----------------------
+ f
+(1 row)
+
+-- error: null value
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             'relpages', 'nope'::text,
+                             'reltuples', NULL::real,
+                             'relallvisible', 4::integer);
+ pg_set_relation_stats 
+-----------------------
+ 
+(1 row)
+
+-- error: bad relpages type
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             'relpages', 'nope'::text,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+WARNING:  relpages must be of type integer
+ pg_set_relation_stats 
+-----------------------
+ f
+(1 row)
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             'relpages', 17::integer,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+WARNING:  wrote
+ pg_set_relation_stats 
+-----------------------
+ t
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       17 |       400 |             4
+(1 row)
+
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    '0'::oid,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ERROR:  could not open relation with OID 0
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    NULL::oid,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    NULL::name,
+    false::boolean,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    NULL::boolean,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', NULL::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.1::real,
+    'avg_width', NULL::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', NULL::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT stanullfrac, stawidth, stadistinct
+FROM pg_statistic
+WHERE starelid = 'stats_export_import.test'::regclass;
+ stanullfrac | stawidth | stadistinct 
+-------------+----------+-------------
+         0.1 |        2 |         0.3
+(1 row)
+
+-- ignore: null_frac < 0
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', -0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ignore: null_frac > 1
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 1.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ignore: avg_width < 0
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', -1::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ignore: n_distinct < -1
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -1.1::real);
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_freqs', '{0.1,0.2,0.3}'::real[]
+    );
+WARNING:  most_common_freqs cannot be present if most_common_vals is missing
+ pg_set_attribute_stats 
+------------------------
+ f
+(1 row)
+
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{1,2,3}'::text
+    );
+WARNING:  most_common_vals cannot be present if most_common_freqs is missing
+ pg_set_attribute_stats 
+------------------------
+ f
+(1 row)
+
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.2,0.1}'::double precision[]
+    );
+WARNING:  most_common_freqs must be type real[], ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ignore: mcv / mcf length mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.2,0.1}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ignore: mcf sum bad
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.6,0.5,0.3}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: histogram elements null value
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,NULL,3,4}'::text
+    );
+WARNING:  histogram_bounds array cannot contain NULL values
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ignore: histogram elements must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,20,3,4}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,2,3,4}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- ignore: correlation low
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'correlation', -1.1::real);
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ignore: correlation high
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'correlation', 1.1::real);
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'correlation', 0.5::real);
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |         0.5 |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{1,3}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+WARNING:  id cannot accept most_common_elems stats, ignored
+WARNING:  id cannot accept most_common_elem_freqs stats, ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,two}'::text
+    );
+WARNING:  most_common_elems cannot be present if most_common_elem_freqs is missing
+ pg_set_attribute_stats 
+------------------------
+ f
+(1 row)
+
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
+    );
+WARNING:  most_common_elem_freqs cannot be present if most_common_elems is missing
+ pg_set_attribute_stats 
+------------------------
+ f
+(1 row)
+
+-- ignore: mcelem / mcelem length mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.2}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ignore: mcelem freq element out of bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.1,0.2,0.3,0.0}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ignore: mcelem freq low-high mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.4,0.3,0.0}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ignore: mcelem freq null pct invalid
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,-0.0001}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ignore: mcelem freq bad low bound low
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,-0.15,0.3,0.1}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ignore: mcelem freq bad low bound high
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,1.5,0.3,0.1}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ignore: mcelem freq bad high bound low
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.4,-0.3,0.1}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ignore: mcelem freq bad high bound high
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.4,3.0,0.1}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ignore: mcelem values must be monotonically increasing
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{three,one}'::text,
+    'most_common_elem_freqs', '{0.3,0.3,0.2,0.3,0.0}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ignore: mcelem values must be unique
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.4,0.4,0.2,0.5,0.0}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             | {one,three}       | {0.3,0.2,0.2,0.3,0}    |                      |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+WARNING:  id cannot accept elem_count_histogram stats, ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+WARNING:  elem_count_histogram array cannot contain NULL values
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ignore: elem_count_histogram must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+WARNING:  id cannot accept range_length_histogram stats, ignored
+WARNING:  id cannot accept range_empty_frac stats, ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+WARNING:  range_length_histogram cannot be present if range_empty_frac is missing
+ pg_set_attribute_stats 
+------------------------
+ f
+(1 row)
+
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real
+    );
+WARNING:  range_empty_frac cannot be present if range_length_histogram is missing
+ pg_set_attribute_stats 
+------------------------
+ f
+(1 row)
+
+-- ignore: range_empty_frac low
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', -0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ignore: range_empty_frac high
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 1.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ignore: range_length_histogram not ascending
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,Infinity,499}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
+(1 row)
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+WARNING:  id cannot accept range_bounds_histogram stats, ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ignore: range_bound_hist low bounds must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[-2,4)","[1,4)","[1,100)"}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ignore: range_bound_hist high bounds must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,11)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
+ stats_export_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      |                        |                  | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+(1 row)
+
+-- warn: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
+    'histogram_bounds', '{1,2,3,4}'::text,
+    'correlation', 1.1::real,
+    'most_common_elems', '{3,1}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    'range_empty_frac', -0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+WARNING:  imported statistics must have a maximum of 5 slots but 6 given
+ pg_set_attribute_stats 
+------------------------
+ f
+(1 row)
+
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+   relname    | num_stats 
+--------------+-----------
+ is_odd       |         1
+ is_odd_clone |         1
+ test         |         5
+ test_clone   |         5
+(4 rows)
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 5ac6e871f5..a7a4dfd411 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,8 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc stats_export_import
+
 
 # ----------
 # Load huge amounts of data
diff --git a/src/test/regress/sql/stats_export_import.sql b/src/test/regress/sql/stats_export_import.sql
new file mode 100644
index 0000000000..8d9e3439a4
--- /dev/null
+++ b/src/test/regress/sql/stats_export_import.sql
@@ -0,0 +1,894 @@
+CREATE SCHEMA stats_export_import;
+
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+-- error: regclass not found
+SELECT pg_set_relation_stats('stats_export_import.nope'::regclass);
+
+-- error: all three params missing
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass);
+
+-- error: reltuples, relallvisible missing
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             'relpages', 4::integer);
+
+-- error: null value
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             'relpages', 'nope'::text,
+                             'reltuples', NULL::real,
+                             'relallvisible', 4::integer);
+
+-- error: bad relpages type
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             'relpages', 'nope'::text,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             'relpages', 17::integer,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_export_import.test'::regclass;
+
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    '0'::oid,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    NULL::oid,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    NULL::name,
+    false::boolean,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    NULL::boolean,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', NULL::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.1::real,
+    'avg_width', NULL::integer,
+    'n_distinct', 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', NULL::real);
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+SELECT stanullfrac, stawidth, stadistinct
+FROM pg_statistic
+WHERE starelid = 'stats_export_import.test'::regclass;
+
+-- ignore: null_frac < 0
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', -0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- ignore: null_frac > 1
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 1.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- ignore: avg_width < 0
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', -1::integer,
+    'n_distinct', 0.3::real);
+
+-- ignore: n_distinct < -1
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -1.1::real);
+
+-- warn: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_freqs', '{0.1,0.2,0.3}'::real[]
+    );
+
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{1,2,3}'::text
+    );
+
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.2,0.1}'::double precision[]
+    );
+
+-- ignore: mcv / mcf length mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.2,0.1}'::real[]
+    );
+
+-- ignore: mcf sum bad
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.6,0.5,0.3}'::real[]
+    );
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: histogram elements null value
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,NULL,3,4}'::text
+    );
+
+-- ignore: histogram elements must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,20,3,4}'::text
+    );
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,2,3,4}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- ignore: correlation low
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'correlation', -1.1::real);
+
+-- ignore: correlation high
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'correlation', 1.1::real);
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'correlation', 0.5::real);
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: scalars can't have mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{1,3}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,two}'::text
+    );
+
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
+    );
+
+-- ignore: mcelem / mcelem length mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.2}'::real[]
+    );
+
+-- ignore: mcelem freq element out of bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.1,0.2,0.3,0.0}'::real[]
+    );
+
+-- ignore: mcelem freq low-high mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.4,0.3,0.0}'::real[]
+    );
+
+-- ignore: mcelem freq null pct invalid
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,-0.0001}'::real[]
+    );
+
+-- ignore: mcelem freq bad low bound low
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,-0.15,0.3,0.1}'::real[]
+    );
+
+-- ignore: mcelem freq bad low bound high
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,1.5,0.3,0.1}'::real[]
+    );
+
+-- ignore: mcelem freq bad high bound low
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.4,-0.3,0.1}'::real[]
+    );
+
+-- ignore: mcelem freq bad high bound high
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.4,3.0,0.1}'::real[]
+    );
+
+-- ignore: mcelem values must be monotonically increasing
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{three,one}'::text,
+    'most_common_elem_freqs', '{0.3,0.3,0.2,0.3,0.0}'::real[]
+    );
+-- ignore: mcelem values must be unique
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.4,0.4,0.2,0.5,0.0}'::real[]
+    );
+
+-- ok: mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- warn: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- ignore: elem_count_histogram must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real
+    );
+-- ignore: range_empty_frac low
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', -0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+-- ignore: range_empty_frac high
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 1.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+-- ignore: range_length_histogram not ascending
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,Infinity,499}'::text
+    );
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+-- ignore: range_bound_hist low bounds must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[-2,4)","[1,4)","[1,100)"}'::text
+    );
+-- ignore: range_bound_hist high bounds must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,11)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
+    'histogram_bounds', '{1,2,3,4}'::text,
+    'correlation', 1.1::real,
+    'most_common_elems', '{3,1}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    'range_empty_frac', -0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+
+SELECT
+    format('SELECT pg_catalog.pg_set_attribute_stats( '
+            || '%L::regclass::oid, '
+            || '%L::name, '
+            || '%L::boolean, '
+            || '%L, %L::real, '
+            || '%L, %L::integer, '
+            || '%L, %L::real %s)',
+        'stats_export_import.' || s.tablename || '_clone',
+        s.attname,
+        s.inherited,
+        'null_frac', s.null_frac,
+        'avg_width', s.avg_width,
+        'n_distinct', s.n_distinct,
+        CASE
+            WHEN s.most_common_vals IS NULL THEN ''
+            ELSE format(', %L, %L::text, %L, %L::real[]',
+                        'most_common_vals', s.most_common_vals,
+                        'most_common_freqs', s.most_common_freqs)
+        END ||
+        CASE
+            WHEN s.histogram_bounds IS NULL THEN ''
+            ELSE format(', %L, %L::text',
+                        'histogram_bounds', s.histogram_bounds)
+        END ||
+        CASE
+            WHEN s.correlation IS NULL THEN ''
+            ELSE format(', %L, %L::real',
+                        'correlation', s.correlation)
+        END ||
+        CASE
+            WHEN s.most_common_elems IS NULL THEN ''
+            ELSE format(', %L, %L::text, %L, %L::real[]',
+                        'most_common_elems', s.most_common_elems,
+                        'most_common_elem_freqs', s.most_common_elem_freqs)
+        END ||
+        CASE
+            WHEN s.elem_count_histogram IS NULL THEN ''
+            ELSE format(', %L, %L::real[]',
+                        'elem_count_histogram', s.elem_count_histogram)
+        END ||
+        CASE
+            WHEN s.range_bounds_histogram IS NULL THEN ''
+            ELSE format(', %L, %L::text',
+                        'range_bounds_histogram', s.range_bounds_histogram)
+        END ||
+        CASE
+            WHEN s.range_empty_frac IS NULL THEN ''
+            ELSE format(', %L, %L::real, %L, %L::text',
+                        'range_empty_frac', s.range_empty_frac,
+                        'range_length_histogram', s.range_length_histogram)
+        END)
+FROM pg_catalog.pg_stats AS s
+WHERE s.schemaname = 'stats_export_import'
+AND s.tablename IN ('test', 'is_odd')
+\gexec
+
+-- restore ECHO to original value
+\set ECHO :orig_ECHO
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 192959ebc1..9dfa0c5a24 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29260,6 +29260,128 @@ DETAIL:  Make sure pg_wal_replay_wait() isn't called within a transaction, anoth
     in identifying the specific disk files associated with database objects.
    </para>
 
+   <table id="functions-admin-statsimport">
+    <title>Database Object Statistics Import Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_relation_stats</primary>
+        </indexterm>
+        <function>pg_set_relation_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>relpages</parameter> <type>integer</type>,
+         <parameter>reltuples</parameter> <type>real</type>,
+         <parameter>relallvisible</parameter> <type>integer</type> )
+        <returnvalue>void</returnvalue>
+       </para>
+       <para>
+        Updates the <structname>pg_class</structname> row for the specified
+        <parameter>relation</parameter>, setting the values for the columns
+        <structfield>reltuples</structfield>,
+        <structfield>relpages</structfield>, and
+        <structfield>relallvisible</structfield>.
+        To avoid table bloat in <structname>pg_class</structname>, this change
+        is made with an in-place update, and therefore cannot be rolled back
+        through normal transaction processing.
+       </para>
+       <para>
+        This function mimics the behavior of <command>ANALYZE</command> in its
+        effects on the values in <structname>pg_class</structname>, except that
+        the values are supplied as parameters rather than derived from table
+        sampling.
+       </para>
+       <para>
+        The caller must either be the owner of the relation, or have superuser
+        privileges.
+       </para>
+       <para>
+        The parameters supplied must all be NOT NULL. The value of
+        <structfield>relpages</structfield> must not be less than 0.  The
+        value of <structfield>reltuples</structfield> must not be less than
+        -1.0.  The value of <structfield>relallvisible</structfield> must not
+        be less than 0.
+       </para>
+       </entry>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_attribute_stats</primary>
+        </indexterm>
+        <function>pg_set_attribute_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>
+         , <parameter>attname</parameter> <type>name</type>
+         , <parameter>inherited</parameter> <type>boolean</type>
+         , <parameter>null_frac</parameter> <type>real</type>
+         , <parameter>avg_width</parameter> <type>integer</type>
+         , <parameter>n_distinct</parameter> <type>real</type>
+         [, <parameter>most_common_vals</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_freqs</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>histogram_bounds</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>correlation</parameter> <type>real</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_elems</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_elem_freqs</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>elem_count_histogram</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_length_histogram</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_empty_frac</parameter> <type>real</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_bounds_histogram</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ] )
+        <returnvalue>void</returnvalue>
+       </para>
+       <para>
+        Replaces the <structname>pg_statistic</structname> row for the
+        <structname>pg_attribute</structname> row specified by
+        <parameter>relation</parameter>, <parameter>attname</parameter>
+        and <parameter>inherited</parameter>. The remaining parameters
+        all correspond to attributes of the same name found in
+        <link linkend="view-pg-stats"><structname>pg_stats</structname></link>,
+        and the values supplied in the parameter must meet the requirements of
+        the corresponding attribute.
+       </para>
+       <para>
+        This function mimics the behavior of <command>ANALYZE</command> in its
+        effects on the values in <structname>pg_statistic</structname>, except
+        that the values are supplied as parameters rather than derived from
+        table sampling.
+       </para>
+       <para>
+        The purpose of this function is to apply statistics values in an
+        upgrade situation that are "good enough" for system operation until
+        they are replaced by the next <command>ANALYZE</command>, usually via
+        <command>autovacuum</command> This function is used by
+        <command>pg_upgrade</command> and <command>pg_restore</command> to
+        convey the statistics from the old system version into the new one.
+       </para>
+       <para>
+        The caller must either be the owner of the relation, or have superuser
+        privileges.
+       </para>
+       </entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
    <table id="functions-admin-dblocation">
     <title>Database Object Location Functions</title>
     <tgroup cols="1">
-- 
2.44.0

v16-0003-Enable-dumping-of-table-index-stats-in-pg_dump.patchtext/x-patch; charset=US-ASCII; name=v16-0003-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From 110c9c15c88d676ce966a06b9d88040323aefbfa Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH v16 3/3] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will generate a statement that
calls pg_set_relation_stats(), and it will generate a series of
statements that call pg_set_attribute_stats(), one per attribute. These
statements will restore the statistics of the current system onto the
destination system.

As is the pattern with pg_dump options, this can be disabled with
--no-statistics.
---
 src/bin/pg_dump/pg_backup.h          |   2 +
 src/bin/pg_dump/pg_backup_archiver.c |   5 +
 src/bin/pg_dump/pg_dump.c            | 298 ++++++++++++++++++++++++++-
 src/bin/pg_dump/pg_dump.h            |   1 +
 src/bin/pg_dump/pg_dumpall.c         |   5 +
 src/bin/pg_dump/pg_restore.c         |   3 +
 6 files changed, 312 insertions(+), 2 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index fbf5f1c515..9f9b9f8724 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -112,6 +112,7 @@ typedef struct _restoreOptions
 	int			no_publications;	/* Skip publication entries */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
@@ -181,6 +182,7 @@ typedef struct _dumpOptions
 	int			no_security_labels;
 	int			no_publications;
 	int			no_subscriptions;
+	int			no_statistics;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 465e9ce777..b7bd896414 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2947,6 +2947,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's a stats dump, maybe ignore it */
+	if (ropt->no_statistics && strcmp(te->desc, "STATISTICS") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2976,6 +2980,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index c52e961b30..aed1ca3514 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -435,6 +435,7 @@ main(int argc, char **argv)
 		{"no-comments", no_argument, &dopt.no_comments, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -1151,6 +1152,7 @@ help(const char *progname)
 	printf(_("  --no-comments                do not dump comments\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -7067,6 +7069,7 @@ getTables(Archive *fout, int *numTables)
 
 		/* Tables have data */
 		tblinfo[i].dobj.components |= DUMP_COMPONENT_DATA;
+		tblinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
 
 		/* Mark whether table has an ACL */
 		if (!PQgetisnull(res, i, i_relacl))
@@ -7564,6 +7567,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -10313,6 +10317,281 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * Tabular description of the parameters to pg_set_relation_stats()
+ * required, param_name, param_type
+ */
+static const char *rel_stats_arginfo[][3] = {
+	{"t", "relation", "regclass"},
+	{"f", "relpages", "integer"},
+	{"f", "reltuples", "real"},
+	{"f", "relallvisible", "integer"},
+};
+
+/*
+ * Tabular description of the parameters to pg_set_attribute_stats()
+ * required, param_name, param_type
+ */
+static const char *att_stats_arginfo[][3] = {
+	{"t", "relation", "regclass"},
+	{"t", "attname", "name"},
+	{"t", "inherited", "boolean"},
+	{"f", "null_frac", "float4"},
+	{"f", "avg_width", "integer"},
+	{"f", "n_distinct", "float4"},
+	{"f", "most_common_vals", "text"},
+	{"f", "most_common_freqs", "float4[]"},
+	{"f", "histogram_bounds", "text"},
+	{"f", "correlation", "float4"},
+	{"f", "most_common_elems", "text"},
+	{"f", "most_common_elem_freqs", "float4[]"},
+	{"f", "elem_count_histogram", "float4[]"},
+	{"f", "range_length_histogram", "text"},
+	{"f", "range_empty_frac", "float4"},
+	{"f", "range_bounds_histogram", "text"},
+};
+
+/*
+ * getRelStatsExportQuery --
+ *
+ * Generate a query that will fetch all relation (e.g. pg_class)
+ * stats for a given relation.
+ */
+static void
+getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, c.relpages, "
+						 "c.reltuples, c.relallvisible "
+						 "FROM pg_class c "
+						 "JOIN pg_namespace n "
+						 "ON n.oid = c.relnamespace "
+						 "WHERE n.nspname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND c.relname = ");
+	appendStringLiteralAH(query, relname, fout);
+}
+
+/*
+ * getAttStatsExportQuery --
+ *
+ * Generate a query that will fetch all attribute (e.g. pg_statistic)
+ * stats for a given relation.
+ */
+static void
+getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "s.attname,"
+						 "s.inherited,"
+						 "s.null_frac,"
+						 "s.avg_width,"
+						 "s.n_distinct,"
+						 "s.most_common_vals,"
+						 "s.most_common_freqs,"
+						 "s.histogram_bounds,"
+						 "s.correlation,"
+						 "s.most_common_elems,"
+						 "s.most_common_elem_freqs,"
+						 "s.elem_count_histogram,");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 "s.range_length_histogram,"
+							 "s.range_empty_frac,"
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(query,
+							 "NULL AS range_length_histogram,"
+							 "NULL AS range_empty_frac,"
+							 "NULL AS range_bounds_histogram ");
+
+	appendPQExpBufferStr(query,
+						 "FROM pg_stats s "
+						 "JOIN pg_namespace n "
+						 "ON n.nspname = s.schemaname "
+						 "JOIN pg_class c "
+						 "ON c.relname = s.tablename "
+						 "AND c.relnamespace = n.oid "
+						 "WHERE s.schemaname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND s.tablename = ");
+	appendStringLiteralAH(query, relname, fout);
+	appendPQExpBufferStr(query, " ORDER BY s.attname, s.inherited");
+}
+
+
+/*
+ * appendNamedArgument --
+ *
+ * Convenience routine for constructing parameters of the form:
+ * 'paraname', 'value'::type
+ */
+static void
+appendNamedArgument(PQExpBuffer out, Archive *fout,
+					const char *argname, bool positional,
+					const char *argval, const char *argtype)
+{
+	appendPQExpBufferStr(out, "\t");
+
+	if (!positional)
+	{
+		appendStringLiteralAH(out, argname, fout);
+		appendPQExpBufferStr(out, ", ");
+	}
+
+	appendStringLiteralAH(out, argval, fout);
+	appendPQExpBuffer(out, "::%s", argtype);
+}
+
+/*
+ * appendRelStatsImport --
+ *
+ * Append a formatted pg_set_relation_stats statement.
+ */
+static void
+appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	const char *sep = "";
+
+	if (PQntuples(res) == 0)
+		return;
+
+	appendPQExpBufferStr(out, "SELECT pg_catalog.pg_set_relation_stats(\n");
+
+	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
+	{
+		bool		positional = (rel_stats_arginfo[argno][0][0] == 't');
+		const char *argname = rel_stats_arginfo[argno][1];
+		const char *argtype = rel_stats_arginfo[argno][2];
+		int			fieldno = PQfnumber(res, argname);
+
+		if (fieldno < 0)
+			pg_fatal("relation stats export query missing field '%s'",
+					 argname);
+
+		if (PQgetisnull(res, 0, fieldno))
+		{
+			if (!positional)
+				pg_fatal("relation stats export query unexpected NULL in '%s'",
+						 argname);
+			else
+				continue;
+		}
+
+		appendPQExpBufferStr(out, sep);
+		appendNamedArgument(out, fout, argname, positional,
+							PQgetvalue(res, 0, fieldno), argtype);
+
+		sep = ",\n";
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+/*
+ * appendAttStatsImport --
+ *
+ * Append a series of formatted pg_set_attribute_stats statements.
+ */
+static void
+appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	{
+		const char *sep = "";
+
+		appendPQExpBufferStr(out, "SELECT pg_catalog.pg_set_attribute_stats(\n");
+		for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
+		{
+			bool		positional = (att_stats_arginfo[argno][0][0] == 't');
+			const char *argname = att_stats_arginfo[argno][1];
+			const char *argtype = att_stats_arginfo[argno][2];
+			int			fieldno = PQfnumber(res, argname);
+
+			if (fieldno < 0)
+				pg_fatal("attribute stats export query missing field '%s'",
+						 argname);
+
+			if (PQgetisnull(res, rownum, fieldno))
+			{
+				if (positional)
+					pg_fatal("attribute stats export query unexpected NULL in '%s'",
+							 argname);
+				else
+					continue;
+			}
+
+			appendPQExpBufferStr(out, sep);
+			appendNamedArgument(out, fout, argname, positional,
+								PQgetvalue(res, rownum, fieldno), argtype);
+			sep = ",\n";
+		}
+		appendPQExpBufferStr(out, "\n);\n");
+	}
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const DumpableObject *dobj,
+				  const char *reltypename, DumpId dumpid)
+{
+	PGresult   *res;
+	PQExpBuffer query;
+	PQExpBuffer out;
+	PQExpBuffer tag;
+
+	/* do nothing, if --no-statistics is supplied */
+	if (fout->dopt->no_statistics)
+		return;
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", reltypename, fmtId(dobj->name));
+
+
+	query = createPQExpBuffer();
+	out = createPQExpBuffer();
+
+	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendRelStatsImport(out, fout, res);
+	PQclear(res);
+
+	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendRelStatsImport(out, fout, res);
+	PQclear(res);
+
+	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendAttStatsImport(out, fout, res);
+	PQclear(res);
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATISTICS DATA",
+							  .section = SECTION_NONE,
+							  .createStmt = out->data,
+							  .deps = &dumpid,
+							  .nDeps = 1));
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(out);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -16757,6 +17036,13 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 	if (tbinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
 		dumpTableSecLabel(fout, tbinfo, reltypename);
 
+	/* Statistics are dependent on the definition, not the data */
+	/* Views don't have stats */
+	if ((tbinfo->dobj.dump & DUMP_COMPONENT_STATISTICS) &&
+		(tbinfo->relkind != RELKIND_VIEW))
+		dumpRelationStats(fout, &tbinfo->dobj, reltypename,
+						  tbinfo->dobj.dumpId);
+
 	/* Dump comments on inlined table constraints */
 	for (j = 0; j < tbinfo->ncheck; j++)
 	{
@@ -16958,6 +17244,7 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 	PQExpBuffer delq;
 	char	   *qindxname;
 	char	   *qqindxname;
+	DumpId		dumpid;
 
 	/* Do nothing in data-only dump */
 	if (dopt->dataOnly)
@@ -17070,14 +17357,21 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 		free(indstatvalsarray);
 	}
 
+	/* Comments and stats share same .dep */
+	dumpid = is_constraint ? indxinfo->indexconstraint :
+		indxinfo->dobj.dumpId;
+
 	/* Dump Index Comments */
 	if (indxinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
 		dumpComment(fout, "INDEX", qindxname,
 					tbinfo->dobj.namespace->dobj.name,
 					tbinfo->rolname,
 					indxinfo->dobj.catId, 0,
-					is_constraint ? indxinfo->indexconstraint :
-					indxinfo->dobj.dumpId);
+					dumpid);
+
+	/* Dump Index Stats */
+	if (indxinfo->dobj.dump & DUMP_COMPONENT_STATISTICS)
+		dumpRelationStats(fout, &indxinfo->dobj, "INDEX", dumpid);
 
 	destroyPQExpBuffer(q);
 	destroyPQExpBuffer(delq);
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 2a7c5873a0..c0e6cda481 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -101,6 +101,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 046c0dc3b3..69652aa205 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -105,6 +105,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -174,6 +175,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -453,6 +455,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -668,6 +672,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 5ea78cf7cc..33ae4f92bc 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -75,6 +75,7 @@ main(int argc, char **argv)
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
 	static int	no_subscriptions = 0;
+	static int  no_statistics = 0;
 	static int	strict_names = 0;
 
 	struct option cmdopts[] = {
@@ -127,6 +128,7 @@ main(int argc, char **argv)
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
+		{"no-statistics", no_argument, &no_statistics, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -370,6 +372,7 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
-- 
2.44.0

#142Tom Lane
tgl@sss.pgh.pa.us
In reply to: Corey Huinker (#141)
Re: Statistics Import and Export

Corey Huinker <corey.huinker@gmail.com> writes:

- functions strive to not ERROR unless absolutely necessary. The biggest
exposure is the call to array_in().

As far as that goes, it shouldn't be that hard to deal with, at least
not for "soft" errors which hopefully cover most input-function
failures these days. You should be invoking array_in via
InputFunctionCallSafe and passing a suitably-set-up ErrorSaveContext.
(Look at pg_input_error_info() for useful precedent.)

There might be something to be said for handling all the error
cases via an ErrorSaveContext and use of ereturn() instead of
ereport(). Not sure if it's worth the trouble or not.

regards, tom lane

#143Corey Huinker
corey.huinker@gmail.com
In reply to: Tom Lane (#142)
Re: Statistics Import and Export

As far as that goes, it shouldn't be that hard to deal with, at least
not for "soft" errors which hopefully cover most input-function
failures these days. You should be invoking array_in via
InputFunctionCallSafe and passing a suitably-set-up ErrorSaveContext.
(Look at pg_input_error_info() for useful precedent.)

Ah, my understanding may be out of date. I was under the impression that
that mechanism relied on the the cooperation of the per-element input
function, so even if we got all the builtin datatypes to play nice with
*Safe(), we were always going to be at risk with a user-defined input
function.

There might be something to be said for handling all the error
cases via an ErrorSaveContext and use of ereturn() instead of
ereport(). Not sure if it's worth the trouble or not.

It would help us tailor the user experience. Right now we have several
endgames. To recap:

1. NULL input => Return NULL. (because strict).
2. Actual error (permissions, cache lookup not found, etc) => Raise ERROR
(thus ruining binary upgrade)
3. Call values are so bad (examples: attname not found, required stat
missing) that nothing can recover => WARN, return FALSE.
4. At least one stakind-stat is wonky (impossible for datatype, missing
stat pair, wrong type on input parameter), but that's the worst of it => 1
to N WARNs, write stats that do make sense, return TRUE.
5. Hunky-dory. => No warns. Write all stats. return TRUE.

Which of those seem like good ereturn candidates to you?

#144Tom Lane
tgl@sss.pgh.pa.us
In reply to: Corey Huinker (#143)
Re: Statistics Import and Export

Corey Huinker <corey.huinker@gmail.com> writes:

As far as that goes, it shouldn't be that hard to deal with, at least
not for "soft" errors which hopefully cover most input-function
failures these days. You should be invoking array_in via
InputFunctionCallSafe and passing a suitably-set-up ErrorSaveContext.
(Look at pg_input_error_info() for useful precedent.)

Ah, my understanding may be out of date. I was under the impression that
that mechanism relied on the the cooperation of the per-element input
function, so even if we got all the builtin datatypes to play nice with
*Safe(), we were always going to be at risk with a user-defined input
function.

That's correct, but it's silly not to do what we can. Also, I imagine
that there is going to be high evolutionary pressure on UDTs to
support soft error mode for COPY, so over time the problem will
decrease --- as long as we invoke the soft error mode.

1. NULL input => Return NULL. (because strict).
2. Actual error (permissions, cache lookup not found, etc) => Raise ERROR
(thus ruining binary upgrade)
3. Call values are so bad (examples: attname not found, required stat
missing) that nothing can recover => WARN, return FALSE.
4. At least one stakind-stat is wonky (impossible for datatype, missing
stat pair, wrong type on input parameter), but that's the worst of it => 1
to N WARNs, write stats that do make sense, return TRUE.
5. Hunky-dory. => No warns. Write all stats. return TRUE.

Which of those seem like good ereturn candidates to you?

I'm good with all those behaviors. On reflection, the design I was
vaguely imagining wouldn't cope with case 4 (multiple WARNs per call)
so never mind that.

regards, tom lane

#145Michael Paquier
michael@paquier.xyz
In reply to: Tom Lane (#118)
Re: Statistics Import and Export

On Mon, Apr 01, 2024 at 01:21:53PM -0400, Tom Lane wrote:

I'm not sure. I think if we put our heads down we could finish
the changes I'm suggesting and resolve the other issues this week.
However, it is starting to feel like the sort of large, barely-ready
patch that we often regret cramming in at the last minute. Maybe
we should agree that the first v18 CF would be a better time to
commit it.

There are still 4 days remaining, so there's still time, but my
overall experience on the matter with my RMT hat on is telling me that
we should not rush this patch set. Redesigning portions close to the
end of a dev cycle is not a good sign, I am afraid, especially if the
sub-parts of the design don't fit well in the global picture as that
could mean more maintenance work on stable branches in the long term.
Still, it is very good to be aware of the problems because you'd know
what to tackle to reach the goals of this patch set.
--
Michael

#146Corey Huinker
corey.huinker@gmail.com
In reply to: Tom Lane (#144)
2 attachment(s)
Re: Statistics Import and Export

I'm good with all those behaviors. On reflection, the design I was
vaguely imagining wouldn't cope with case 4 (multiple WARNs per call)
so never mind that.

regards, tom lane

v17

0001
- array_in now repackages cast errors as warnings and skips the stat, test
added
- version parameter added, though it's mostly for future compatibility,
tests modified
- both functions delay object/attribute locking until absolutely necessary
- general cleanup

0002
- added version parameter to dumps
- --schema-only will not dump stats unless in binary upgrade mode
- stats are dumped SECTION_NONE
- general cleanup

I think that covers the outstanding issues.

Attachments:

v17-0001-Create-pg_set_relation_stats-pg_set_attribute_st.patchtext/x-patch; charset=US-ASCII; name=v17-0001-Create-pg_set_relation_stats-pg_set_attribute_st.patchDownload
From caed92a6322fa50ddedd2fb8091c2651909e1302 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 21 Mar 2024 18:04:54 -0400
Subject: [PATCH v17 1/2] Create pg_set_relation_stats, pg_set_attribute_stats.

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

Both functions take an oid to identify the target relation that will
receive the statistics. There is nothing requiring that relation to be
the same one as the one exported, though the statistics supplied have
to make sense in the context of the new relation.

Both functions take a variadic list of statistic parameters name-value
pairs.

Each name given for pg_set_relation_stats must correspond to a
statistics attribute in pg_class, and the paired value must match the
datatype of said attribute in pg_class.

Each name given for pg_set_attribute_stats must correspond to a
statistics attribute in pg_stats, and the paird value must match the
datatype of said attribute in pg_stats, except for attributes of type
ANYARRAY, in which case a TEXT value is required.

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

These functions 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               |   22 +-
 src/include/statistics/statistics.h           |    2 +
 src/backend/statistics/Makefile               |    3 +-
 src/backend/statistics/meson.build            |    1 +
 src/backend/statistics/statistics.c           | 1239 +++++++++++++++++
 .../regress/expected/stats_export_import.out  |  853 ++++++++++++
 src/test/regress/parallel_schedule            |    3 +-
 src/test/regress/sql/stats_export_import.sql  |  671 +++++++++
 doc/src/sgml/func.sgml                        |  267 ++++
 9 files changed, 3056 insertions(+), 5 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 153d816a05..d8edcb25cd 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12183,9 +12183,6 @@
   proallargtypes => '{int8,pg_lsn,pg_lsn,oid,oid,oid,int2,int8,bool}',
   proargmodes => '{i,i,i,o,o,o,o,o,o}',
   proargnames => '{tli,start_lsn,end_lsn,relfilenode,reltablespace,reldatabase,relforknumber,relblocknumber,is_limit_block}',
-  prosrc => 'pg_wal_summary_contents' },
-{ oid => '8438',
-  descr => 'WAL summarizer state',
   proname => 'pg_get_wal_summarizer_state',
   provolatile => 'v', proparallel => 's',
   prorettype => 'record', proargtypes => '',
@@ -12200,4 +12197,23 @@
   proargtypes => 'int2',
   prosrc => 'gist_stratnum_identity' },
 
+# Statistics Import
+{ oid => '8048',
+  descr => 'set statistics on relation',
+  proname => 'pg_set_relation_stats', provariadic => 'any',
+  proisstrict => 't', provolatile => 'v',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'regclass int4 any',
+  proargnames => '{relation,version,stats}',
+  proargmodes => '{i,i,v}',
+  prosrc => 'pg_set_relation_stats' },
+{ oid => '8049',
+  descr => 'set statistics on attribute',
+  proname => 'pg_set_attribute_stats', provariadic => 'any',
+  proisstrict => 't', provolatile => 'v',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'regclass name bool int4 any',
+  proargnames => '{relation,attname,inherited,version,stats}',
+  proargmodes => '{i,i,i,i,v}',
+  prosrc => 'pg_set_attribute_stats' },
 ]
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 7f2bf18716..1dddf96576 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -127,4 +127,6 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												int nclauses);
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
+extern Datum pg_set_relation_stats(PG_FUNCTION_ARGS);
+extern Datum pg_set_attribute_stats(PG_FUNCTION_ARGS);
 #endif							/* STATISTICS_H */
diff --git a/src/backend/statistics/Makefile b/src/backend/statistics/Makefile
index 89cf8c2797..e4f8ab7c4f 100644
--- a/src/backend/statistics/Makefile
+++ b/src/backend/statistics/Makefile
@@ -16,6 +16,7 @@ OBJS = \
 	dependencies.o \
 	extended_stats.o \
 	mcv.o \
-	mvdistinct.o
+	mvdistinct.o \
+	statistics.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/statistics/meson.build b/src/backend/statistics/meson.build
index 73b29a3d50..331e82c776 100644
--- a/src/backend/statistics/meson.build
+++ b/src/backend/statistics/meson.build
@@ -5,4 +5,5 @@ backend_sources += files(
   'extended_stats.c',
   'mcv.c',
   'mvdistinct.c',
+  'statistics.c',
 )
diff --git a/src/backend/statistics/statistics.c b/src/backend/statistics/statistics.c
new file mode 100644
index 0000000000..08b720e7c2
--- /dev/null
+++ b/src/backend/statistics/statistics.c
@@ -0,0 +1,1239 @@
+/*-------------------------------------------------------------------------
+ * statistics.c
+ *
+ *	  POSTGRES statistics import
+ *
+ * Code supporting the direct importation of relation statistics, similar to
+ * what is done by the ANALYZE command.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *       src/backend/statistics/statistics.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "catalog/pg_collation.h"
+#include "catalog/pg_database.h"
+#include "catalog/pg_operator.h"
+#include "catalog/pg_type.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_type.h"
+#include "fmgr.h"
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "nodes/nodeFuncs.h"
+#include "statistics/statistics.h"
+#include "utils/acl.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/float.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/rangetypes.h"
+#include "utils/syscache.h"
+#include "utils/typcache.h"
+
+/*
+ * A role has privileges to set statistics on the relation if any of the
+ * following are true:
+ *   - the role owns the current database and the relation is not shared
+ *   - the role has the MAINTAIN privilege on the relation
+ *
+ */
+static bool
+can_modify_relation(Relation rel)
+{
+	Oid			relid = RelationGetRelid(rel);
+	Form_pg_class reltuple = rel->rd_rel;
+
+	return ((object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId())
+			 && !reltuple->relisshared) ||
+			pg_class_aclcheck(relid, GetUserId(), ACL_MAINTAIN) == ACLCHECK_OK);
+}
+
+/*
+ * Set statistics for a given pg_class entry.
+ *
+ * This does an in-place (i.e. non-transactional) update of pg_class, just as
+ * is done in ANALYZE.
+ *
+ */
+Datum
+pg_set_relation_stats(PG_FUNCTION_ARGS)
+{
+	Oid			relid = PG_GETARG_OID(0);
+	int			version = PG_GETARG_INT32(1);
+
+	/* indexes of where we found required stats */
+	int			i_relpages = 0;
+	int			i_reltuples = 0;
+	int			i_relallvisible = 0;
+
+	/* build argument values to build the object */
+	Datum	   *args;
+	bool	   *nulls;			/* placeholder, because strict */
+	Oid		   *types;
+	int			nargs;
+
+	/* Minimum version supported */
+	if (version <= 90200)
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Cannot export statistics prior to version 9.2")));
+
+	nargs = extract_variadic_args(fcinfo, 2, true, &args, &types, &nulls);
+
+	/* if the pairs aren't pairs, something is malformed */
+	if (nargs % 2 == 1)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("stats parameters must be in name-value pairs")));
+		PG_RETURN_BOOL(false);
+	}
+
+	/* loop through args, matching params to their arg indexes */
+	for (int i = 0; i < nargs; i += 2)
+	{
+		char	   *statname;
+		int			argidx = i + 1;
+
+		if (types[i] != TEXTOID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("stat names must be of type text")));
+			continue;
+		}
+		statname = TextDatumGetCString(args[i]);
+
+		/*
+		 * match each named parameter to the index of the value that follows
+		 * it
+		 */
+		if (strcmp(statname, "relpages") == 0)
+			i_relpages = argidx;
+		else if (strcmp(statname, "reltuples") == 0)
+			i_reltuples = argidx;
+		else if (strcmp(statname, "relallvisible") == 0)
+			i_relallvisible = argidx;
+		else
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("unknown stat naame '%s', skipping", statname)));
+
+		pfree(statname);
+	}
+
+	/*
+	 * Ensure that we got all required parameters, and they are of the correct
+	 * type.
+	 */
+	if (i_relpages == 0)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "relpages")));
+		PG_RETURN_BOOL(false);
+	}
+	else if (types[i_relpages] != INT4OID)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relpages must be of type integer")));
+		PG_RETURN_BOOL(false);
+	}
+	else if (i_reltuples == 0)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "reltuples")));
+		PG_RETURN_BOOL(false);
+	}
+	else if (types[i_reltuples] != FLOAT4OID)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("reltuples must be of type real")));
+		PG_RETURN_BOOL(false);
+	}
+	else if (types[i_relallvisible] != INT4OID)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "relallvisible")));
+		PG_RETURN_BOOL(false);
+	}
+	else if (i_relallvisible == -1)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relallvisible must be of type integer")));
+		PG_RETURN_BOOL(false);
+	}
+	else
+	{
+		/*
+		 * Open the relation, getting ShareUpdateExclusiveLock to ensure that
+		 * no other stat-setting operation can run on it concurrently.
+		 */
+		Relation	rel;
+		HeapTuple	ctup;
+		Form_pg_class pgcform;
+		int			relpages;
+		float4		reltuples;
+		int			relallvisible;
+
+		rel = table_open(RelationRelationId, ShareUpdateExclusiveLock);
+
+		if (!can_modify_relation(rel))
+		{
+			table_close(rel, NoLock);
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("must be owner to modify relation stats")));
+			PG_RETURN_BOOL(false);
+		}
+
+		relpages = DatumGetInt32(args[i_relpages]);
+		reltuples = DatumGetFloat4(args[i_reltuples]);
+		relallvisible = DatumGetInt32(args[i_relallvisible]);
+
+		ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+		if (!HeapTupleIsValid(ctup))
+			ereport(ERROR,
+					(errcode(ERRCODE_OBJECT_IN_USE),
+					 errmsg("pg_class entry for relid %u not found", relid)));
+
+		pgcform = (Form_pg_class) GETSTRUCT(ctup);
+		/* Only update pg_class if there is a meaningful change */
+		if ((pgcform->reltuples != reltuples)
+			|| (pgcform->relpages != relpages)
+			|| (pgcform->relallvisible != relallvisible))
+		{
+			pgcform->relpages = relpages;
+			pgcform->reltuples = reltuples;
+			pgcform->relallvisible = relallvisible;
+
+			heap_inplace_update(rel, ctup);
+		}
+
+		table_close(rel, NoLock);
+		PG_RETURN_BOOL(true);
+	}
+
+	PG_RETURN_BOOL(false);
+}
+
+/*
+ * Test if the type is a scalar for MCELEM purposes
+ */
+static bool
+type_is_scalar(Oid typid)
+{
+	HeapTuple	tp;
+	bool		result = false;
+
+	tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+	if (HeapTupleIsValid(tp))
+	{
+		Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+
+		result = (!OidIsValid(typtup->typanalyze));
+		ReleaseSysCache(tp);
+	}
+	return result;
+}
+
+/*
+ * Fetch datatype information, this is needed to derive the proper staopN
+ * and stacollN values.
+ *
+ * If this relation is an index and that index has expressions in it, and
+ * the attnum specified is known to be an expression, then we must walk
+ * the list attributes up to the specified attnum to get the right
+ * expression.
+ *
+ */
+static TypeCacheEntry *
+get_attr_stat_type(Relation rel, Name attname,
+				   int16 *attnum, int32 *typmod, Oid *typcoll)
+{
+	Oid			relid = RelationGetRelid(rel);
+	Form_pg_attribute attr;
+	HeapTuple	atup;
+	Oid			typid;
+
+	atup = SearchSysCache2(ATTNAME, ObjectIdGetDatum(relid),
+						   NameGetDatum(attname));
+
+	/* Attribute not found */
+	if (!HeapTupleIsValid(atup))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s has no attname %s",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+		return NULL;
+	}
+
+	attr = (Form_pg_attribute) GETSTRUCT(atup);
+	if (attr->attisdropped)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s attname %s is dropped",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+		return NULL;
+	}
+	*attnum = attr->attnum;
+
+	if ((rel->rd_rel->relkind == RELKIND_INDEX
+		 || (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+		&& (rel->rd_indexprs != NIL)
+		&& (rel->rd_index->indkey.values[attr->attnum - 1] == 0))
+	{
+		ListCell   *indexpr_item = list_head(rel->rd_indexprs);
+		Node	   *expr;
+
+		for (int i = 0; i < attr->attnum - 1; i++)
+			if (rel->rd_index->indkey.values[i] == 0)
+				indexpr_item = lnext(rel->rd_indexprs, indexpr_item);
+
+		if (indexpr_item == NULL)	/* shouldn't happen */
+			elog(ERROR, "too few entries in indexprs list");
+
+		expr = (Node *) lfirst(indexpr_item);
+
+		typid = exprType(expr);
+		*typmod = exprTypmod(expr);
+
+		/*
+		 * If a collation has been specified for the index column, use that in
+		 * preference to anything else; but if not, fall back to whatever we
+		 * can get from the expression.
+		 */
+		if (OidIsValid(attr->attcollation))
+			*typcoll = attr->attcollation;
+		else
+			*typcoll = exprCollation(expr);
+	}
+	else
+	{
+		typid = attr->atttypid;
+		*typmod = attr->atttypmod;
+		*typcoll = attr->attcollation;
+	}
+	ReleaseSysCache(atup);
+
+	/* if it's a multirange, step down to the range type */
+	if (type_is_multirange(typid))
+		typid = get_multirange_range(typid);
+
+	return lookup_type_cache(typid,
+							 TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR);
+}
+
+/*
+ * Perform the cast of a known TextDatum into the type specified.
+ *
+ * If no errors are found, ok is set to true. Otherwise, set ok
+ * to false, capture the error found, and re-throw at warning level.
+ */
+static Datum
+cast_stavalues(FmgrInfo *flinfo, Datum d, Oid typid, int32 typmod, bool *ok)
+{
+	LOCAL_FCINFO(fcinfo, 8);
+	char	   *s;
+	Datum		result;
+	ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+	escontext.details_wanted = true;
+
+	s = TextDatumGetCString(d);
+
+	InitFunctionCallInfoData(*fcinfo, flinfo, 3, InvalidOid,
+							 (Node *) &escontext, NULL);
+
+	fcinfo->args[0].value = CStringGetDatum(s);
+	fcinfo->args[0].isnull = false;
+	fcinfo->args[1].value = ObjectIdGetDatum(typid);
+	fcinfo->args[1].isnull = false;
+	fcinfo->args[2].value = Int32GetDatum(typmod);
+	fcinfo->args[2].isnull = false;
+
+	result = FunctionCallInvoke(fcinfo);
+
+	if (SOFT_ERROR_OCCURRED(&escontext))
+	{
+		escontext.error_data->elevel = WARNING;
+		ThrowErrorData(escontext.error_data);
+		*ok = false;
+	}
+	else
+		*ok = true;
+
+	pfree(s);
+
+	return result;
+}
+
+
+/*
+ * Check array for any NULLs, and optionally for one-dimensionality
+ *
+ * Report any failures as warnings.
+ */
+static bool
+array_check(Datum datum, int one_dim, const char *statname)
+{
+	ArrayType  *arr = DatumGetArrayTypeP(datum);
+	int16		elmlen;
+	char		elmalign;
+	bool		elembyval;
+	Datum	   *values;
+	bool	   *nulls;
+	int			nelems;
+
+	if (one_dim && (arr->ndim != 1))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be a multidimensional array", statname)));
+		return false;
+	}
+
+	get_typlenbyvalalign(ARR_ELEMTYPE(arr), &elmlen, &elembyval, &elmalign);
+
+	deconstruct_array(arr, ARR_ELEMTYPE(arr), elmlen, elembyval, elmalign,
+					  &values, &nulls, &nelems);
+
+	for (int i = 0; i < nelems; i++)
+		if (nulls[i])
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s array cannot contain NULL values", statname)));
+			return false;
+		}
+	return true;
+}
+
+/*
+ * Update the pg_statistic record.
+ */
+static void
+update_pg_statistic(Datum values[], bool nulls[])
+{
+	Relation	sd = table_open(StatisticRelationId, RowExclusiveLock);
+	CatalogIndexState indstate = CatalogOpenIndexes(sd);
+	HeapTuple	oldtup;
+	HeapTuple	stup;
+
+	/* Is there already a pg_statistic tuple for this attribute? */
+	oldtup = SearchSysCache3(STATRELATTINH,
+							 values[Anum_pg_statistic_starelid - 1],
+							 values[Anum_pg_statistic_staattnum - 1],
+							 values[Anum_pg_statistic_stainherit - 1]);
+
+	if (HeapTupleIsValid(oldtup))
+	{
+		/* Yes, replace it */
+		bool		replaces[Natts_pg_statistic];
+
+		for (int i = 0; i < Natts_pg_statistic; i++)
+			replaces[i] = true;
+
+		stup = heap_modify_tuple(oldtup, RelationGetDescr(sd),
+								 values, nulls, replaces);
+		ReleaseSysCache(oldtup);
+		CatalogTupleUpdateWithInfo(sd, &stup->t_self, stup, indstate);
+	}
+	else
+	{
+		/* No, insert new tuple */
+		stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
+		CatalogTupleInsertWithInfo(sd, stup, indstate);
+	}
+
+	heap_freetuple(stup);
+	CatalogCloseIndexes(indstate);
+	table_close(sd, NoLock);
+}
+
+/*
+ * Import statistics for a given relation attribute.
+ *
+ * This will insert/replace a row in pg_statistic for the given combinarion
+ * of relation, attribute, and inherited flag.
+ *
+ * The version parameter is for future use in the events as future versions
+ * may change them meaning of certain parameters.
+ *
+ * The variadic parameters all represent name-value pairs, with the names
+ * corresponding to attributes in pg_stats. Unkown names will generate a
+ * warning.
+ *
+ * Parameters null_frac, avg_width, and n_distinct are required because
+ * those attributes have no default value in pg_statistic.
+ *
+ * The remaining parameters all belong to a specific stakind, and all are
+ * optional. Some stakinds have multiple parameters, and in those cases
+ * both parameters must be specified if one of them is, otherwise a
+ * warning is generated but the rest of the stats may still be imported.
+ *
+ * If there is no attribute with a matching attname in the relation, the
+ * function will raise a warning and return false.
+ *
+ * Parameters corresponding to ANYARRAY columns are instead passed in as text
+ * values, which is a valid input string for an array of the type or element
+ * type of the attribute.
+ */
+Datum
+pg_set_attribute_stats(PG_FUNCTION_ARGS)
+{
+	Oid			relid = PG_GETARG_OID(0);
+	Name		attname = PG_GETARG_NAME(1);
+	bool		inherited = PG_GETARG_BOOL(2);
+	int			version = PG_GETARG_INT32(3);
+
+	/* build argument values to build the object */
+	Datum	   *args;
+	bool	   *argnulls;		/* placeholder, because strict */
+	Oid		   *types;
+	int			nargs = extract_variadic_args(fcinfo, 4, true,
+											  &args, &types, &argnulls);
+
+	Datum		values[Natts_pg_statistic] = {0};
+	bool		nulls[Natts_pg_statistic] = {false};
+
+	/*
+	 * argument indexes for each known statistic
+	 *
+	 * 0 = not found, -1 = error, n > 0 = found
+	 */
+
+	/* parameters that are required get indexes */
+	int			i_null_frac = 0;
+	int			i_avg_width = 0;
+	int			i_n_distinct = 0;
+
+	/* stakind stats are optional */
+	int			i_mc_vals = 0;
+	int			i_mc_freqs = 0;
+	int			i_hist_bounds = 0;
+	int			i_correlation = 0;
+	int			i_mc_elems = 0;
+	int			i_mc_elem_freqs = 0;
+	int			i_elem_count_hist = 0;
+	int			i_range_length_hist = 0;
+	int			i_range_empty_frac = 0;
+	int			i_range_bounds_hist = 0;
+
+	Relation	rel;
+
+	TypeCacheEntry *typcache;
+	TypeCacheEntry *elemtypcache;
+
+	int16		attnum;
+	int32		typmod;
+	Oid			typcoll;
+
+	FmgrInfo	finfo;
+
+	int			stakind_count;
+
+	int			k = 0;
+
+	if (version <= 90200)
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Cannot export statistics prior to version 9.2")));
+
+	/* if the pairs aren't pairs, something is malformed */
+	if (nargs % 2 == 1)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("stats parameters must be in name-value pairs")));
+		PG_RETURN_BOOL(false);
+	}
+
+	for (int i = 0; i < nargs; i += 2)
+	{
+		char	   *statname;
+		int			argidx = i + 1;
+
+		if (types[i] != TEXTOID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("stat names must be of type text")));
+			break;
+		}
+		statname = TextDatumGetCString(args[i]);
+
+		if (strcmp(statname, "null_frac") == 0)
+			i_null_frac = argidx;
+		else if (strcmp(statname, "avg_width") == 0)
+			i_avg_width = argidx;
+		else if (strcmp(statname, "n_distinct") == 0)
+			i_n_distinct = argidx;
+		else if (strcmp(statname, "most_common_vals") == 0)
+			i_mc_vals = argidx;
+		else if (strcmp(statname, "most_common_freqs") == 0)
+			i_mc_freqs = argidx;
+		else if (strcmp(statname, "histogram_bounds") == 0)
+			i_hist_bounds = argidx;
+		else if (strcmp(statname, "correlation") == 0)
+			i_correlation = argidx;
+		else if (strcmp(statname, "most_common_elems") == 0)
+			i_mc_elems = argidx;
+		else if (strcmp(statname, "most_common_elem_freqs") == 0)
+			i_mc_elem_freqs = argidx;
+		else if (strcmp(statname, "elem_count_histogram") == 0)
+			i_elem_count_hist = argidx;
+		else if (strcmp(statname, "range_length_histogram") == 0)
+			i_range_length_hist = argidx;
+		else if (strcmp(statname, "range_empty_frac") == 0)
+			i_range_empty_frac = argidx;
+		else if (strcmp(statname, "range_bounds_histogram") == 0)
+			i_range_bounds_hist = argidx;
+		else
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("unknown stat naame '%s', skipping", statname)));
+
+		pfree(statname);
+	}
+
+	/* check all required parameters */
+	if (i_null_frac > 0)
+	{
+		if (types[i_null_frac] == FLOAT4OID)
+			values[Anum_pg_statistic_stanullfrac - 1] = args[i_null_frac];
+		else
+		{
+			/* required param, not recoverable */
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be of type %s", "null_frac",
+							"real")));
+			PG_RETURN_BOOL(false);
+		}
+	}
+	else
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "null_frac")));
+		PG_RETURN_BOOL(false);
+	}
+
+	if (i_avg_width > 0)
+	{
+		if (types[i_avg_width] == INT4OID)
+			values[Anum_pg_statistic_stawidth - 1] = args[i_avg_width];
+		else
+		{
+			/* required param, not recoverable */
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be of type %s", "avg_width",
+							"integer")));
+			PG_RETURN_BOOL(false);
+		}
+	}
+	else
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "avg_width")));
+		PG_RETURN_BOOL(false);
+	}
+
+	if (i_n_distinct > 0)
+	{
+		if (types[i_n_distinct] == FLOAT4OID)
+			values[Anum_pg_statistic_stadistinct - 1] = args[i_n_distinct];
+		else
+		{
+			/* required param, not recoverable */
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be of type %s", "n_distinct",
+							"real")));
+			PG_RETURN_BOOL(false);
+		}
+	}
+	else
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "n_distinct")));
+		PG_RETURN_BOOL(false);
+	}
+
+	/* look for pair mismatches */
+	if ((i_mc_vals == 0) != (i_mc_freqs == 0))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be present if %s is missing",
+						(i_mc_vals == 0) ? "most_common_freqs" :
+						"most_common_vals",
+						(i_mc_vals == 0) ? "most_common_vals" :
+						"most_common_freqs")));
+		PG_RETURN_BOOL(false);
+	}
+	if ((i_mc_elems == 0) != (i_mc_elem_freqs == 0))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be present if %s is missing",
+						(i_mc_elems == 0) ?
+						"most_common_elem_freqs" :
+						"most_common_elems",
+						(i_mc_elems == 0) ?
+						"most_common_elems" :
+						"most_common_elem_freqs")));
+		PG_RETURN_BOOL(false);
+	}
+	if ((i_range_length_hist == 0) != (i_range_empty_frac == 0))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be present if %s is missing",
+						(i_range_length_hist == 0) ?
+						"range_empty_frac" :
+						"range_length_histogram",
+						(i_range_length_hist == 0) ?
+						"range_length_histogram" :
+						"range_empty_frac")));
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * count the number of stakinds we want to set, paired params count as
+	 * one. The count cannot exceed STATISTIC_NUM_SLOTS.
+	 */
+	stakind_count = (int) (i_mc_vals > 0) +
+		(int) (i_mc_elems > 0) +
+		(int) (i_range_length_hist > 0) +
+		(int) (i_hist_bounds > 0) +
+		(int) (i_correlation > 0) +
+		(int) (i_elem_count_hist > 0) +
+		(int) (i_range_bounds_hist > 0);
+
+	if (stakind_count > STATISTIC_NUM_SLOTS)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("imported statistics must have a maximum of %d slots "
+						"but %d given",
+						STATISTIC_NUM_SLOTS, stakind_count)));
+		PG_RETURN_BOOL(false);
+	}
+
+	rel = relation_open(relid, ShareUpdateExclusiveLock);
+
+	if (!can_modify_relation(rel))
+	{
+		relation_close(rel, NoLock);
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						RelationGetRelationName(rel))));
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * Many of the values that are set for a particular stakind are entirely
+	 * derived from the attribute itself, or it's expression.
+	 */
+	typcache = get_attr_stat_type(rel, attname, &attnum, &typmod, &typcoll);
+	if (typcache == NULL)
+		PG_RETURN_BOOL(false);
+
+	/*
+	 * Derive element type if we have stat kinds that need it.
+	 *
+	 * This duplicates some type-specific logic found in various typanalyze
+	 * functions which are called from vacuum's examine_attribute().
+	 */
+	if ((i_mc_elems > 0) || (i_elem_count_hist > 0))
+	{
+		Oid			elemtypid;
+
+		if (typcache->type_id == TSVECTOROID)
+		{
+			/*
+			 * tsvector elems always have a text oid type and default
+			 * collation
+			 */
+			elemtypid = TEXTOID;
+			typcoll = DEFAULT_COLLATION_OID;
+		}
+		else if (typcache->typtype == TYPTYPE_RANGE)
+			elemtypid = get_range_subtype(typcache->type_id);
+		else
+			elemtypid = get_base_element_type(typcache->type_id);
+
+		/* not finding a basetype means we already had it */
+		if (elemtypid == InvalidOid)
+			elemtypid = typcache->type_id;
+
+		/* The stats need the eq_opr, but validation needs the lt_opr */
+		elemtypcache = lookup_type_cache(elemtypid,
+										 TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR);
+	}
+
+	/*
+	 * histogram_bounds and correlation must have a type < operator. WARN and
+	 * skip if this attribute doesn't have one.
+	 */
+	if (typcache->lt_opr == InvalidOid)
+	{
+		if (i_hist_bounds > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"histogram_bounds")));
+			i_hist_bounds = -1;
+		}
+		if (i_correlation > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"correlation")));
+			i_correlation = -1;
+		}
+	}
+
+	/*
+	 * Scalar types can't have most_common_elems, most_common_elem_freqs,
+	 * elem_count_histogram. WARN and skip.
+	 */
+	if (type_is_scalar(typcache->type_id))
+	{
+		if (i_mc_elems > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"most_common_elems")));
+			i_mc_elems = -1;
+		}
+		if (i_mc_elem_freqs > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"most_common_elem_freqs")));
+			i_mc_elem_freqs = -1;
+		}
+		if (i_elem_count_hist > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"elem_count_histogram")));
+			i_elem_count_hist = -1;
+		}
+	}
+
+	/*
+	 * Only range types can have range_length_histogram, range_empty_frac, and
+	 * range_bounds_histogram. WARN and skip
+	 */
+	if ((typcache->typtype != TYPTYPE_RANGE) &&
+		(typcache->typtype != TYPTYPE_RANGE))
+	{
+		if (i_range_length_hist > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"range_length_histogram")));
+			i_range_length_hist = -1;
+		}
+		if (i_range_empty_frac > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"range_empty_frac")));
+			i_range_empty_frac = -1;
+		}
+		if (i_range_bounds_hist > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"range_bounds_histogram")));
+			i_range_bounds_hist = -1;
+		}
+	}
+
+	values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(relid);
+	values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(attnum);
+	values[Anum_pg_statistic_stainherit - 1] = BoolGetDatum(inherited);
+
+	fmgr_info(F_ARRAY_IN, &finfo);
+
+	/*
+	 * STATISTIC_KIND_MCV
+	 *
+	 * most_common_freqs: real[] most_common_vals : ANYARRAY::text
+	 */
+	if (i_mc_vals > 0)
+	{
+		bool		ok = true;
+		Oid			numberstype = types[i_mc_freqs];
+		Oid			valuestype = types[i_mc_vals];
+		Datum		stanumbers = args[i_mc_freqs];
+		Datum		strvalue = args[i_mc_vals];
+
+		if (get_element_type(numberstype) != FLOAT4OID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"most_common_freqs", "real[]")));
+			ok = false;
+		}
+		else if (valuestype != TEXTOID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"most_common_vals", "text")));
+			ok = false;
+		}
+
+		if (ok)
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_MCV);
+			Datum		staop = ObjectIdGetDatum(typcache->eq_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		stavalues;
+			bool		converted = false;
+
+			stavalues = cast_stavalues(&finfo, strvalue, typcache->type_id,
+									   typmod, &converted);
+
+			if (converted &&
+				array_check(stavalues, false, "most_common_vals") &&
+				array_check(stanumbers, true, "most_common_freqs"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_HISTOGRAM
+	 *
+	 * histogram_bounds: ANYARRAY::text
+	 */
+	if (i_hist_bounds > 0)
+	{
+		Oid			valuestype = types[i_hist_bounds];
+		Datum		strvalue = args[i_hist_bounds];
+
+		if (valuestype == TEXTOID)
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_HISTOGRAM);
+			Datum		staop = ObjectIdGetDatum(typcache->lt_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		stavalues;
+			bool		converted = false;
+
+			stavalues = cast_stavalues(&finfo, strvalue, typcache->type_id,
+									   typmod, &converted);
+
+			if (converted && array_check(stavalues, false, "histogram_bounds"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+		else
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"histogram_bounds", "text")));
+	}
+
+	/*
+	 * STATISTIC_KIND_CORRELATION
+	 *
+	 * correlation: real
+	 */
+	if (i_correlation > 0)
+	{
+		if (types[i_correlation] == FLOAT4OID)
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_CORRELATION);
+			Datum		staop = ObjectIdGetDatum(typcache->lt_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		elems[] = {args[i_correlation]};
+			ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+			Datum		stanumbers = PointerGetDatum(arry);
+
+			values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+			values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+			values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+			values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+			nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+			k++;
+		}
+		else
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be of type %s, ignored",
+							"correlation", "real")));
+	}
+
+	/*
+	 * STATISTIC_KIND_MCELEM
+	 *
+	 * most_common_elem_freqs: real[] most_common_elems     : ANYARRAY::text
+	 */
+	if (i_mc_elems > 0)
+	{
+		bool		ok = true;
+		Oid			numberstype = types[i_mc_elem_freqs];
+		Oid			valuestype = types[i_mc_elems];
+		Datum		stanumbers = args[i_mc_elem_freqs];
+
+		if (get_element_type(numberstype) != FLOAT4OID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"most_common_elem_freqs", "real[]")));
+			ok = false;
+		}
+		else if (valuestype != TEXTOID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"most_common_elems", "text")));
+			ok = false;
+		}
+
+		if (ok)
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_MCELEM);
+			Datum		staop = ObjectIdGetDatum(elemtypcache->eq_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		strvalue = args[i_mc_elems];
+			bool		converted = false;
+			Datum		stavalues;
+
+			stavalues = cast_stavalues(&finfo, strvalue, elemtypcache->type_id,
+									   typmod, &converted);
+
+			if (converted &&
+				array_check(stavalues, false, "most_common_elems") &&
+				array_check(stanumbers, true, "most_common_elem_freqs"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_DECHIST
+	 *
+	 * elem_count_histogram:	real[]
+	 */
+	if (i_elem_count_hist > 0)
+	{
+		Oid			numberstype = types[i_elem_count_hist];
+
+		if (get_element_type(numberstype) == FLOAT4OID)
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_DECHIST);
+			Datum		staop = ObjectIdGetDatum(elemtypcache->eq_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		stanumbers = args[i_elem_count_hist];
+
+			if (array_check(stanumbers, true, "elem_count_histogram"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+				nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+				k++;
+			}
+		}
+		else
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"elem_count_histogram", "real[]")));
+	}
+
+	/*
+	 * STATISTIC_KIND_BOUNDS_HISTOGRAM
+	 *
+	 * range_bounds_histogram: ANYARRAY::text
+	 *
+	 * This stakind appears before STATISTIC_KIND_BOUNDS_HISTOGRAM even though
+	 * it is numerically greater, and all other stakinds appear in numerical
+	 * order. We duplicate this quirk to make before/after tests of
+	 * pg_statistic records easier.
+	 */
+	if (i_range_bounds_hist > 0)
+	{
+		Oid			valuestype = types[i_range_bounds_hist];
+
+		if (valuestype == TEXTOID)
+		{
+
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_BOUNDS_HISTOGRAM);
+			Datum		staop = ObjectIdGetDatum(InvalidOid);
+			Datum		stacoll = ObjectIdGetDatum(InvalidOid);
+			Datum		strvalue = args[i_range_bounds_hist];
+			bool		converted = false;
+			Datum		stavalues;
+
+			stavalues = cast_stavalues(&finfo, strvalue, typcache->type_id,
+									   typmod, &converted);
+
+			if (converted &&
+				array_check(stavalues, false, "range_bounds_histogram"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+		else
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"range_bounds_histogram", "text")));
+	}
+
+	/*
+	 * STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM
+	 *
+	 * range_empty_frac:        real range_length_histogram:  double
+	 * precision[]::text
+	 */
+	if (i_range_length_hist > 0)
+	{
+		bool		ok = true;
+		Oid			numberstype = types[i_range_empty_frac];
+		Oid			valuestype = types[i_range_length_hist];
+
+		if (numberstype != FLOAT4OID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"range_empty_frac", "real")));
+			ok = false;
+		}
+		else if (valuestype != TEXTOID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"range_length_histogram", "text")));
+			ok = false;
+		}
+
+		if (ok)
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM);
+			Datum		staop = ObjectIdGetDatum(Float8LessOperator);
+			Datum		stacoll = ObjectIdGetDatum(InvalidOid);
+
+			/* The anyarray is always a float8[] for this stakind */
+			Datum		elem = args[i_range_empty_frac];
+			Datum		elems[] = {elem};
+			ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+			Datum		stanumbers = PointerGetDatum(arry);
+			Datum		strvalue = args[i_range_length_hist];
+			bool		converted = false;
+			Datum		stavalues;
+
+			stavalues = cast_stavalues(&finfo, strvalue, FLOAT8OID, 0, &converted);
+
+			if (converted &&
+				array_check(stavalues, false, "range_length_histogram"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+	}
+
+	/* fill in all remaining slots */
+	for (; k < STATISTIC_NUM_SLOTS; k++)
+	{
+		values[Anum_pg_statistic_stakind1 - 1 + k] = Int16GetDatum(0);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+	}
+
+	update_pg_statistic(values, nulls);
+
+	relation_close(rel, NoLock);
+	PG_RETURN_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..0ad4f903f4
--- /dev/null
+++ b/src/test/regress/expected/stats_export_import.out
@@ -0,0 +1,853 @@
+CREATE SCHEMA stats_export_import;
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+        0 |        -1 |             0
+(1 row)
+
+-- error: regclass not found
+SELECT pg_set_relation_stats('stats_export_import.nope'::regclass,
+                             150000::integer);
+ERROR:  relation "stats_export_import.nope" does not exist
+LINE 1: SELECT pg_set_relation_stats('stats_export_import.nope'::reg...
+                                     ^
+-- error: all three params missing
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer);
+ERROR:  function pg_set_relation_stats(regclass, integer) does not exist
+LINE 1: SELECT pg_set_relation_stats('stats_export_import.test'::reg...
+               ^
+HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+-- error: reltuples, relallvisible missing
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 4::integer);
+WARNING:  required parameter reltuples not set
+ pg_set_relation_stats 
+-----------------------
+ f
+(1 row)
+
+-- error: null value
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', NULL::real,
+                             'relallvisible', 4::integer);
+ pg_set_relation_stats 
+-----------------------
+ 
+(1 row)
+
+-- error: bad relpages type
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+WARNING:  relpages must be of type integer
+ pg_set_relation_stats 
+-----------------------
+ f
+(1 row)
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 17::integer,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+ pg_set_relation_stats 
+-----------------------
+ t
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       17 |       400 |             4
+(1 row)
+
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    '0'::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ERROR:  could not open relation with OID 0
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    NULL::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    NULL::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    NULL::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', NULL::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', NULL::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', NULL::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT stanullfrac, stawidth, stadistinct
+FROM pg_statistic
+WHERE starelid = 'stats_export_import.test'::regclass;
+ stanullfrac | stawidth | stadistinct 
+-------------+----------+-------------
+         0.1 |        2 |         0.3
+(1 row)
+
+-- warn: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_freqs', '{0.1,0.2,0.3}'::real[]
+    );
+WARNING:  most_common_freqs cannot be present if most_common_vals is missing
+ pg_set_attribute_stats 
+------------------------
+ f
+(1 row)
+
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{1,2,3}'::text
+    );
+WARNING:  most_common_vals cannot be present if most_common_freqs is missing
+ pg_set_attribute_stats 
+------------------------
+ f
+(1 row)
+
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.2,0.1}'::double precision[]
+    );
+WARNING:  most_common_freqs must be type real[], ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warning: mcv cast failure
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,four,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+WARNING:  invalid input syntax for type integer: "four"
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: histogram elements null value
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,NULL,3,4}'::text
+    );
+WARNING:  histogram_bounds array cannot contain NULL values
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,2,3,4}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'correlation', 0.5::real);
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |         0.5 |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{1,3}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+WARNING:  id cannot accept most_common_elems stats, ignored
+WARNING:  id cannot accept most_common_elem_freqs stats, ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,two}'::text
+    );
+WARNING:  most_common_elems cannot be present if most_common_elem_freqs is missing
+ pg_set_attribute_stats 
+------------------------
+ f
+(1 row)
+
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
+    );
+WARNING:  most_common_elem_freqs cannot be present if most_common_elems is missing
+ pg_set_attribute_stats 
+------------------------
+ f
+(1 row)
+
+-- ok: mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             | {one,three}       | {0.3,0.2,0.2,0.3,0}    |                      |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+WARNING:  id cannot accept elem_count_histogram stats, ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+WARNING:  elem_count_histogram array cannot contain NULL values
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+WARNING:  id cannot accept range_length_histogram stats, ignored
+WARNING:  id cannot accept range_empty_frac stats, ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+WARNING:  range_length_histogram cannot be present if range_empty_frac is missing
+ pg_set_attribute_stats 
+------------------------
+ f
+(1 row)
+
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real
+    );
+WARNING:  range_empty_frac cannot be present if range_length_histogram is missing
+ pg_set_attribute_stats 
+------------------------
+ f
+(1 row)
+
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
+(1 row)
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+WARNING:  id cannot accept range_bounds_histogram stats, ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
+ stats_export_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      |                        |                  | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+(1 row)
+
+-- warn: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
+    'histogram_bounds', '{1,2,3,4}'::text,
+    'correlation', 1.1::real,
+    'most_common_elems', '{3,1}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    'range_empty_frac', -0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+WARNING:  imported statistics must have a maximum of 5 slots but 6 given
+ pg_set_attribute_stats 
+------------------------
+ f
+(1 row)
+
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+   relname    | num_stats 
+--------------+-----------
+ is_odd       |         1
+ is_odd_clone |         1
+ test         |         5
+ test_clone   |         5
+(4 rows)
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 5ac6e871f5..a7a4dfd411 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,8 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc stats_export_import
+
 
 # ----------
 # Load huge amounts of data
diff --git a/src/test/regress/sql/stats_export_import.sql b/src/test/regress/sql/stats_export_import.sql
new file mode 100644
index 0000000000..ebdb58aba1
--- /dev/null
+++ b/src/test/regress/sql/stats_export_import.sql
@@ -0,0 +1,671 @@
+CREATE SCHEMA stats_export_import;
+
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+-- error: regclass not found
+SELECT pg_set_relation_stats('stats_export_import.nope'::regclass,
+                             150000::integer);
+
+-- error: all three params missing
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer);
+
+-- error: reltuples, relallvisible missing
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 4::integer);
+
+-- error: null value
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', NULL::real,
+                             'relallvisible', 4::integer);
+
+-- error: bad relpages type
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 17::integer,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_export_import.test'::regclass;
+
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    '0'::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    NULL::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    NULL::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    NULL::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', NULL::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', NULL::integer,
+    'n_distinct', 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', NULL::real);
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+SELECT stanullfrac, stawidth, stadistinct
+FROM pg_statistic
+WHERE starelid = 'stats_export_import.test'::regclass;
+
+-- warn: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_freqs', '{0.1,0.2,0.3}'::real[]
+    );
+
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{1,2,3}'::text
+    );
+
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.2,0.1}'::double precision[]
+    );
+
+-- warning: mcv cast failure
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,four,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: histogram elements null value
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,NULL,3,4}'::text
+    );
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,2,3,4}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'correlation', 0.5::real);
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: scalars can't have mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{1,3}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,two}'::text
+    );
+
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
+    );
+
+-- ok: mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- warn: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real
+    );
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
+    'histogram_bounds', '{1,2,3,4}'::text,
+    'correlation', 1.1::real,
+    'most_common_elems', '{3,1}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    'range_empty_frac', -0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+
+SELECT
+    format('SELECT pg_catalog.pg_set_attribute_stats( '
+            || '%L::regclass::oid, '
+            || '%L::name, '
+            || '%L::boolean, '
+            || '%L::integer, '
+            || '%L, %L::real, '
+            || '%L, %L::integer, '
+            || '%L, %L::real %s)',
+        'stats_export_import.' || s.tablename || '_clone',
+        s.attname,
+        s.inherited,
+        150000,
+        'null_frac', s.null_frac,
+        'avg_width', s.avg_width,
+        'n_distinct', s.n_distinct,
+        CASE
+            WHEN s.most_common_vals IS NULL THEN ''
+            ELSE format(', %L, %L::text, %L, %L::real[]',
+                        'most_common_vals', s.most_common_vals,
+                        'most_common_freqs', s.most_common_freqs)
+        END ||
+        CASE
+            WHEN s.histogram_bounds IS NULL THEN ''
+            ELSE format(', %L, %L::text',
+                        'histogram_bounds', s.histogram_bounds)
+        END ||
+        CASE
+            WHEN s.correlation IS NULL THEN ''
+            ELSE format(', %L, %L::real',
+                        'correlation', s.correlation)
+        END ||
+        CASE
+            WHEN s.most_common_elems IS NULL THEN ''
+            ELSE format(', %L, %L::text, %L, %L::real[]',
+                        'most_common_elems', s.most_common_elems,
+                        'most_common_elem_freqs', s.most_common_elem_freqs)
+        END ||
+        CASE
+            WHEN s.elem_count_histogram IS NULL THEN ''
+            ELSE format(', %L, %L::real[]',
+                        'elem_count_histogram', s.elem_count_histogram)
+        END ||
+        CASE
+            WHEN s.range_bounds_histogram IS NULL THEN ''
+            ELSE format(', %L, %L::text',
+                        'range_bounds_histogram', s.range_bounds_histogram)
+        END ||
+        CASE
+            WHEN s.range_empty_frac IS NULL THEN ''
+            ELSE format(', %L, %L::real, %L, %L::text',
+                        'range_empty_frac', s.range_empty_frac,
+                        'range_length_histogram', s.range_length_histogram)
+        END)
+FROM pg_catalog.pg_stats AS s
+WHERE s.schemaname = 'stats_export_import'
+AND s.tablename IN ('test', 'is_odd')
+\gexec
+
+-- restore ECHO to original value
+\set ECHO :orig_ECHO
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 192959ebc1..786832232a 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29260,6 +29260,273 @@ DETAIL:  Make sure pg_wal_replay_wait() isn't called within a transaction, anoth
     in identifying the specific disk files associated with database objects.
    </para>
 
+   <table id="functions-admin-statsimport">
+    <title>Database Object Statistics Import Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_relation_stats</primary>
+        </indexterm>
+        <function>pg_set_relation_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>version</parameter> <type>integer</type>,
+         <literal>VARIADIC</literal> <parameter>stats</parameter> <type>"any"</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Updates the <structname>pg_class</structname> row for the specified
+        <parameter>relation</parameter>, values from the variadic list of
+        name-value pairs. Each parameter name corresponds to the same-named
+        attribute in <structname>pg_class</structname>:
+       </para>
+       <variablelist>
+        <varlistentry>
+         <term><literal>relpages</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>integer</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>reltuples</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>relalltuples</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>integer</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+       </variablelist>
+       <para>
+        Any other parameter names given must also have a value pair, but will emit
+        a warning and thereafter be ignored.
+       </para>
+       <para>
+        The <parameter>version</parameter> is meant to reflect the server version
+        number of the system where this data was generated, as that may in the
+        future change how the data is imported.
+       </para>
+       <para>
+        To avoid table bloat in <structname>pg_class</structname>, this change
+        is made with an in-place update, and therefore cannot be rolled back
+        through normal transaction processing.
+       </para>
+       <para>
+        This function mimics the behavior of <command>ANALYZE</command> in its
+        effects on the values in <structname>pg_class</structname>, except that
+        the values are supplied as parameters rather than derived from table
+        sampling.
+       </para>
+       <para>
+        The caller must either be the owner of the relation, or have superuser
+        privileges.
+       </para>
+       <para>
+        The parameters given are checked for type validity and must all be
+        NOT NULL.
+       </para>
+       <para>
+        The parameters will return true if stats were updated and false if not.
+       </para>
+       </entry>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_attribute_stats</primary>
+        </indexterm>
+        <function>pg_set_attribute_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>
+         , <parameter>attname</parameter> <type>name</type>
+         , <parameter>inherited</parameter> <type>boolean</type>
+         , <parameter>version</parameter> <type>integer</type>
+         , <literal>VARIADIC</literal> <parameter>stats</parameter> <type>"any"</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Replaces the <structname>pg_statistic</structname> row for the
+        <structname>pg_attribute</structname> row specified by
+        <parameter>relation</parameter>, <parameter>attname</parameter>
+        and <parameter>inherited</parameter>. The <parameter>version</parameter>
+        should be set to the server version number of the server where
+        the variadic <parameter>stats</parameter> originated, as it may in the
+        future affect how the stats are imported. Values from the variadic
+        list form name-value pairs. Each parameter name corresponds to the
+        same-named attribute in
+        <link linkend="view-pg-stats"><structname>pg_stats</structname></link>:
+       </para>
+       <variablelist>
+        <varlistentry>
+         <term><literal>null_frac</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>avg_width</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>integer</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>n_distinct</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_vals</literal></term>
+         <listitem>
+          <para>
+          <literal>most_common_vals</literal>
+           Must be followed by a parameter of type <type>text</type>. If this
+           parameter is specified then <literal>most_common_freqs</literal>
+           must also be specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_freqs</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of <type>real[]</type>, and can only
+           be specified if <literal>most_common_vals</literal> is also specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>histogram_bounds</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>correlation</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_elems</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>. If this
+           parameter is specified then <literal>most_common_elem_freqs</literal>
+           must also be specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_elem_freqs</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real[]</type>, and can
+           only be specified if <literal>most_common_elems</literal> is also
+           specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>elem_count_histogram</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real[]</type>.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>range_length_histogram</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>. If this
+           parameter is specified then <literal>range_empty_frac</literal>
+           must also be specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>range_empty_frac</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>, and can
+           only be specified if <literal>range_length_histogram</literal>
+           is also specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>range_bounds_histogram</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>
+          </para>
+         </listitem>
+        </varlistentry>
+       </variablelist>
+       <para>
+        Any other parameter names given must also have a value pair, but will emit
+        a warning and thereafter be ignored.
+       </para>
+       <para>
+        The parameters will return true if stats were updated and false if not.
+       </para>
+       <para>
+        This function mimics the behavior of <command>ANALYZE</command> in its
+        effects on the values in <structname>pg_statistic</structname>, except
+        that the values are supplied as parameters rather than derived from
+        table sampling.
+       </para>
+       <para>
+        The purpose of this function is to apply statistics values in an
+        upgrade situation that are "good enough" for system operation until
+        they are replaced by the next <command>ANALYZE</command>, usually via
+        <command>autovacuum</command>.
+       </para>
+       <para>
+        The caller must either be the owner of the relation, or have superuser
+        privileges.
+       </para>
+       </entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
    <table id="functions-admin-dblocation">
     <title>Database Object Location Functions</title>
     <tgroup cols="1">
-- 
2.44.0

v17-0002-Enable-dumping-of-table-index-stats-in-pg_dump.patchtext/x-patch; charset=US-ASCII; name=v17-0002-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From 8e5b4e0fee175956b3a5293a53febcd9335a991e Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH v17 2/2] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will generate a statement that
calls pg_set_relation_stats(), and it will generate a series of
statements that call pg_set_attribute_stats(), one per attribute. These
statements will restore the statistics of the current system onto the
destination system.

Statistics are not dumped when -schema-only is specified, except during
a binary upgrade.

As is the pattern with pg_dump options, this can be disabled with
--no-statistics.
---
 src/bin/pg_dump/pg_backup.h          |   2 +
 src/bin/pg_dump/pg_backup_archiver.c |   5 +
 src/bin/pg_dump/pg_dump.c            | 300 ++++++++++++++++++++++++++-
 src/bin/pg_dump/pg_dump.h            |   1 +
 src/bin/pg_dump/pg_dumpall.c         |   5 +
 src/bin/pg_dump/pg_restore.c         |   3 +
 6 files changed, 314 insertions(+), 2 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index fbf5f1c515..9f9b9f8724 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -112,6 +112,7 @@ typedef struct _restoreOptions
 	int			no_publications;	/* Skip publication entries */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
@@ -181,6 +182,7 @@ typedef struct _dumpOptions
 	int			no_security_labels;
 	int			no_publications;
 	int			no_subscriptions;
+	int			no_statistics;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 465e9ce777..b7bd896414 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2947,6 +2947,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's a stats dump, maybe ignore it */
+	if (ropt->no_statistics && strcmp(te->desc, "STATISTICS") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2976,6 +2980,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index c52e961b30..001630c4bd 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -435,6 +435,7 @@ main(int argc, char **argv)
 		{"no-comments", no_argument, &dopt.no_comments, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -1151,6 +1152,7 @@ help(const char *progname)
 	printf(_("  --no-comments                do not dump comments\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -7067,6 +7069,7 @@ getTables(Archive *fout, int *numTables)
 
 		/* Tables have data */
 		tblinfo[i].dobj.components |= DUMP_COMPONENT_DATA;
+		tblinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
 
 		/* Mark whether table has an ACL */
 		if (!PQgetisnull(res, i, i_relacl))
@@ -7564,6 +7567,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -10313,6 +10317,283 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * Tabular description of the parameters to pg_set_relation_stats()
+ * required, param_name, param_type
+ */
+static const char *rel_stats_arginfo[][3] = {
+	{"t", "relation", "regclass"},
+	{"t", "version", "integer"},
+	{"f", "relpages", "integer"},
+	{"f", "reltuples", "real"},
+	{"f", "relallvisible", "integer"},
+};
+
+/*
+ * Tabular description of the parameters to pg_set_attribute_stats()
+ * required, param_name, param_type
+ */
+static const char *att_stats_arginfo[][3] = {
+	{"t", "relation", "regclass"},
+	{"t", "attname", "name"},
+	{"t", "inherited", "boolean"},
+	{"t", "version", "integer"},
+	{"f", "null_frac", "float4"},
+	{"f", "avg_width", "integer"},
+	{"f", "n_distinct", "float4"},
+	{"f", "most_common_vals", "text"},
+	{"f", "most_common_freqs", "float4[]"},
+	{"f", "histogram_bounds", "text"},
+	{"f", "correlation", "float4"},
+	{"f", "most_common_elems", "text"},
+	{"f", "most_common_elem_freqs", "float4[]"},
+	{"f", "elem_count_histogram", "float4[]"},
+	{"f", "range_length_histogram", "text"},
+	{"f", "range_empty_frac", "float4"},
+	{"f", "range_bounds_histogram", "text"},
+};
+
+/*
+ * getRelStatsExportQuery --
+ *
+ * Generate a query that will fetch all relation (e.g. pg_class)
+ * stats for a given relation.
+ */
+static void
+getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "current_setting('server_version_num') AS version, "
+						 "c.relpages, c.reltuples, c.relallvisible "
+						 "FROM pg_class c "
+						 "JOIN pg_namespace n "
+						 "ON n.oid = c.relnamespace "
+						 "WHERE n.nspname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND c.relname = ");
+	appendStringLiteralAH(query, relname, fout);
+}
+
+/*
+ * getAttStatsExportQuery --
+ *
+ * Generate a query that will fetch all attribute (e.g. pg_statistic)
+ * stats for a given relation.
+ */
+static void
+getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "s.attname,"
+						 "s.inherited,"
+						 "current_setting('server_version_num') AS version, "
+						 "s.null_frac,"
+						 "s.avg_width,"
+						 "s.n_distinct,"
+						 "s.most_common_vals,"
+						 "s.most_common_freqs,"
+						 "s.histogram_bounds,"
+						 "s.correlation,"
+						 "s.most_common_elems,"
+						 "s.most_common_elem_freqs,"
+						 "s.elem_count_histogram,");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 "s.range_length_histogram,"
+							 "s.range_empty_frac,"
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(query,
+							 "NULL AS range_length_histogram,"
+							 "NULL AS range_empty_frac,"
+							 "NULL AS range_bounds_histogram ");
+
+	appendPQExpBufferStr(query,
+						 "FROM pg_stats s "
+						 "JOIN pg_namespace n "
+						 "ON n.nspname = s.schemaname "
+						 "JOIN pg_class c "
+						 "ON c.relname = s.tablename "
+						 "AND c.relnamespace = n.oid "
+						 "WHERE s.schemaname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND s.tablename = ");
+	appendStringLiteralAH(query, relname, fout);
+	appendPQExpBufferStr(query, " ORDER BY s.attname, s.inherited");
+}
+
+
+/*
+ * appendNamedArgument --
+ *
+ * Convenience routine for constructing parameters of the form:
+ * 'paraname', 'value'::type
+ */
+static void
+appendNamedArgument(PQExpBuffer out, Archive *fout,
+					const char *argname, bool positional,
+					const char *argval, const char *argtype)
+{
+	appendPQExpBufferStr(out, "\t");
+
+	if (!positional)
+	{
+		appendStringLiteralAH(out, argname, fout);
+		appendPQExpBufferStr(out, ", ");
+	}
+
+	appendStringLiteralAH(out, argval, fout);
+	appendPQExpBuffer(out, "::%s", argtype);
+}
+
+/*
+ * appendRelStatsImport --
+ *
+ * Append a formatted pg_set_relation_stats statement.
+ */
+static void
+appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	const char *sep = "";
+
+	if (PQntuples(res) == 0)
+		return;
+
+	appendPQExpBufferStr(out, "SELECT pg_catalog.pg_set_relation_stats(\n");
+
+	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
+	{
+		bool		positional = (rel_stats_arginfo[argno][0][0] == 't');
+		const char *argname = rel_stats_arginfo[argno][1];
+		const char *argtype = rel_stats_arginfo[argno][2];
+		int			fieldno = PQfnumber(res, argname);
+
+		if (fieldno < 0)
+			pg_fatal("relation stats export query missing field '%s'",
+					 argname);
+
+		if (PQgetisnull(res, 0, fieldno))
+		{
+			if (!positional)
+				pg_fatal("relation stats export query unexpected NULL in '%s'",
+						 argname);
+			else
+				continue;
+		}
+
+		appendPQExpBufferStr(out, sep);
+		appendNamedArgument(out, fout, argname, positional,
+							PQgetvalue(res, 0, fieldno), argtype);
+
+		sep = ",\n";
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+/*
+ * appendAttStatsImport --
+ *
+ * Append a series of formatted pg_set_attribute_stats statements.
+ */
+static void
+appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	{
+		const char *sep = "";
+
+		appendPQExpBufferStr(out, "SELECT pg_catalog.pg_set_attribute_stats(\n");
+		for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
+		{
+			bool		positional = (att_stats_arginfo[argno][0][0] == 't');
+			const char *argname = att_stats_arginfo[argno][1];
+			const char *argtype = att_stats_arginfo[argno][2];
+			int			fieldno = PQfnumber(res, argname);
+
+			if (fieldno < 0)
+				pg_fatal("attribute stats export query missing field '%s'",
+						 argname);
+
+			if (PQgetisnull(res, rownum, fieldno))
+			{
+				if (positional)
+					pg_fatal("attribute stats export query unexpected NULL in '%s'",
+							 argname);
+				else
+					continue;
+			}
+
+			appendPQExpBufferStr(out, sep);
+			appendNamedArgument(out, fout, argname, positional,
+								PQgetvalue(res, rownum, fieldno), argtype);
+			sep = ",\n";
+		}
+		appendPQExpBufferStr(out, "\n);\n");
+	}
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const DumpableObject *dobj,
+				  const char *reltypename, DumpId dumpid)
+{
+	PGresult   *res;
+	PQExpBuffer query;
+	PQExpBuffer out;
+	PQExpBuffer tag;
+
+	/* do nothing, if --no-statistics is supplied */
+	if (fout->dopt->no_statistics)
+		return;
+
+	/* likewise, no stats on schema-only, unless in a binary upgrade */
+	if (fout->dopt->schemaOnly && !fout->dopt->binary_upgrade)
+		return;
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", reltypename, fmtId(dobj->name));
+
+	query = createPQExpBuffer();
+	out = createPQExpBuffer();
+
+	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendRelStatsImport(out, fout, res);
+	PQclear(res);
+
+	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendAttStatsImport(out, fout, res);
+	PQclear(res);
+
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATISTICS DATA",
+							  .section = SECTION_NONE,
+							  .createStmt = out->data,
+							  .deps = &dumpid,
+							  .nDeps = 1));
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(out);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -16757,6 +17038,13 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 	if (tbinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
 		dumpTableSecLabel(fout, tbinfo, reltypename);
 
+	/* Statistics are dependent on the definition, not the data */
+	/* Views don't have stats */
+	if ((tbinfo->dobj.dump & DUMP_COMPONENT_STATISTICS) &&
+		(tbinfo->relkind != RELKIND_VIEW))
+		dumpRelationStats(fout, &tbinfo->dobj, reltypename,
+						  tbinfo->dobj.dumpId);
+
 	/* Dump comments on inlined table constraints */
 	for (j = 0; j < tbinfo->ncheck; j++)
 	{
@@ -16958,6 +17246,7 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 	PQExpBuffer delq;
 	char	   *qindxname;
 	char	   *qqindxname;
+	DumpId		dumpid;
 
 	/* Do nothing in data-only dump */
 	if (dopt->dataOnly)
@@ -17070,14 +17359,21 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 		free(indstatvalsarray);
 	}
 
+	/* Comments and stats share same .dep */
+	dumpid = is_constraint ? indxinfo->indexconstraint :
+		indxinfo->dobj.dumpId;
+
 	/* Dump Index Comments */
 	if (indxinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
 		dumpComment(fout, "INDEX", qindxname,
 					tbinfo->dobj.namespace->dobj.name,
 					tbinfo->rolname,
 					indxinfo->dobj.catId, 0,
-					is_constraint ? indxinfo->indexconstraint :
-					indxinfo->dobj.dumpId);
+					dumpid);
+
+	/* Dump Index Stats */
+	if (indxinfo->dobj.dump & DUMP_COMPONENT_STATISTICS)
+		dumpRelationStats(fout, &indxinfo->dobj, "INDEX", dumpid);
 
 	destroyPQExpBuffer(q);
 	destroyPQExpBuffer(delq);
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 2a7c5873a0..c0e6cda481 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -101,6 +101,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 046c0dc3b3..69652aa205 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -105,6 +105,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -174,6 +175,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -453,6 +455,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -668,6 +672,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 5ea78cf7cc..33ae4f92bc 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -75,6 +75,7 @@ main(int argc, char **argv)
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
 	static int	no_subscriptions = 0;
+	static int  no_statistics = 0;
 	static int	strict_names = 0;
 
 	struct option cmdopts[] = {
@@ -127,6 +128,7 @@ main(int argc, char **argv)
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
+		{"no-statistics", no_argument, &no_statistics, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -370,6 +372,7 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
-- 
2.44.0

#147Corey Huinker
corey.huinker@gmail.com
In reply to: Ashutosh Bapat (#113)
Re: Statistics Import and Export

For a partitioned table this value has to be true. For a normal table when
setting this value to true, it should at least make sure that the table has
at least one child. Otherwise it should throw an error. Blindly accepting
the given value may render the statistics unusable. Prologue of the
function needs to be updated accordingly.

I can see rejecting non-inherited stats for a partitioned table. The
reverse, however, isn't true, because a table may end up being inherited by
another, so those statistics may be legit. Having said that, a great deal
of the data validation I was doing was seen as unnecessary, so I' not sure
where this check would fall on that line. It's a trivial check if we do add
it.

#148Ashutosh Bapat
ashutosh.bapat.oss@gmail.com
In reply to: Corey Huinker (#147)
Re: Statistics Import and Export

On Fri, Apr 5, 2024 at 7:00 AM Corey Huinker <corey.huinker@gmail.com>
wrote:

For a partitioned table this value has to be true. For a normal table when

setting this value to true, it should at least make sure that the table has
at least one child. Otherwise it should throw an error. Blindly accepting
the given value may render the statistics unusable. Prologue of the
function needs to be updated accordingly.

I can see rejecting non-inherited stats for a partitioned table. The
reverse, however, isn't true, because a table may end up being inherited by
another, so those statistics may be legit. Having said that, a great deal
of the data validation I was doing was seen as unnecessary, so I' not sure
where this check would fall on that line. It's a trivial check if we do add
it.

I read that discussion, and it may be ok for pg_upgrade/pg_dump usecase and
maybe also for IMPORT foreign schema where the SQL is generated by
PostgreSQL itself. But not for simulating statistics. In that case, if the
function happily installs statistics cooked by the user and those aren't
used anywhere, users may be misled by the plans that are generated
subsequently. Thus negating the very purpose of simulating statistics. Once
the feature is out there, we won't be able to restrict its usage unless we
document the possible anomalies.

--
Best Wishes,
Ashutosh Bapat

#149Tom Lane
tgl@sss.pgh.pa.us
In reply to: Ashutosh Bapat (#148)
Re: Statistics Import and Export

Ashutosh Bapat <ashutosh.bapat.oss@gmail.com> writes:

I read that discussion, and it may be ok for pg_upgrade/pg_dump usecase and
maybe also for IMPORT foreign schema where the SQL is generated by
PostgreSQL itself. But not for simulating statistics. In that case, if the
function happily installs statistics cooked by the user and those aren't
used anywhere, users may be misled by the plans that are generated
subsequently. Thus negating the very purpose of simulating
statistics.

I'm not sure what you think the "purpose of simulating statistics" is,
but it seems like you have an extremely narrow-minded view of it.
I think we should allow injecting any stats that won't actively crash
the backend. Such functionality could be useful for stress-testing
the planner, for example, or even just to see what it would do in
a situation that is not what you have.

Note that I don't think pg_dump or pg_upgrade need to support
injection of counterfactual statistics. But direct calls of the
stats insertion functions should be able to do so.

regards, tom lane

#150Ashutosh Bapat
ashutosh.bapat.oss@gmail.com
In reply to: Tom Lane (#149)
Re: Statistics Import and Export

On Fri, Apr 5, 2024 at 10:07 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:

Ashutosh Bapat <ashutosh.bapat.oss@gmail.com> writes:

I read that discussion, and it may be ok for pg_upgrade/pg_dump usecase

and

maybe also for IMPORT foreign schema where the SQL is generated by
PostgreSQL itself. But not for simulating statistics. In that case, if

the

function happily installs statistics cooked by the user and those aren't
used anywhere, users may be misled by the plans that are generated
subsequently. Thus negating the very purpose of simulating
statistics.

I'm not sure what you think the "purpose of simulating statistics" is,
but it seems like you have an extremely narrow-minded view of it.
I think we should allow injecting any stats that won't actively crash
the backend. Such functionality could be useful for stress-testing
the planner, for example, or even just to see what it would do in
a situation that is not what you have.

My reply was in the following context

For a partitioned table this value has to be true. For a normal table when

setting this value to true, it should at least make sure that the table has
at least one child. Otherwise it should throw an error. Blindly accepting
the given value may render the statistics unusable. Prologue of the
function needs to be updated accordingly.

I can see rejecting non-inherited stats for a partitioned table. The
reverse, however, isn't true, because a table may end up being inherited by
another, so those statistics may be legit. Having said that, a great deal
of the data validation I was doing was seen as unnecessary, so I' not sure
where this check would fall on that line. It's a trivial check if we do add
it.

If a user installs inherited stats for a non-inherited table by accidently
passing true to the corresponding argument, those stats won't be even used.
The user wouldn't know that those stats are not used. Yet, they would think
that any change in the plans is the result of their stats. So whatever
simulation experiment they are running would lead to wrong conclusions.
This could be easily avoided by raising an error. Similarly for installing
non-inherited stats for a partitioned table. There might be other scenarios
where the error won't be required.

--
Best Wishes,
Ashutosh Bapat

#151Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#146)
Re: Statistics Import and Export

On Thu, 2024-04-04 at 00:30 -0400, Corey Huinker wrote:

v17

0001
- array_in now repackages cast errors as warnings and skips the stat,
test added
- version parameter added, though it's mostly for future
compatibility, tests modified
- both functions delay object/attribute locking until absolutely
necessary
- general cleanup

0002
- added version parameter to dumps
- --schema-only will not dump stats unless in binary upgrade mode
- stats are dumped SECTION_NONE
- general cleanup

I think that covers the outstanding issues. 

Thank you, this has improved a lot and the fundamentals are very close.

I think it could benefit from a bit more time to settle on a few
issues:

1. SECTION_NONE. Conceptually, stats are more like data, and so
intuitively I would expect this in the SECTION_DATA or
SECTION_POST_DATA. However, the two most important use cases (in my
opinion) don't involve dumping the data: pg_upgrade (data doesn't come
from the dump) and planner simulations/repros. Perhaps the section we
place it in is not a critical decision, but we will need to stick with
it for a long time, and I'm not sure that we have consensus on that
point.

2. We changed the stats import function API to be VARIADIC very
recently. After we have a bit of time to think on it, I'm not 100% sure
we will want to stick with that new API. It's not easy to document,
which is something I always like to consider.

3. The error handling also changed recently to change soft errors (i.e.
type input errors) to warnings. I like this change but I'd need a bit
more time to get comfortable with how this is done, there is not a lot
of precedent for doing this kind of thing. This is connected to the
return value, as well as the machine-readability concern that Magnus
raised.

Additionally, a lot of people are simply very busy around this time,
and may not have had a chance to opine on all the recent changes yet.

Regards,
Jeff Davis

#152Tom Lane
tgl@sss.pgh.pa.us
In reply to: Jeff Davis (#151)
Re: Statistics Import and Export

Jeff Davis <pgsql@j-davis.com> writes:

Thank you, this has improved a lot and the fundamentals are very close.
I think it could benefit from a bit more time to settle on a few
issues:

Yeah ... it feels like we aren't quite going to manage to get this
over the line for v17. We could commit with the hope that these
last details will get sorted later, but that path inevitably leads
to a mess.

1. SECTION_NONE. Conceptually, stats are more like data, and so
intuitively I would expect this in the SECTION_DATA or
SECTION_POST_DATA. However, the two most important use cases (in my
opinion) don't involve dumping the data: pg_upgrade (data doesn't come
from the dump) and planner simulations/repros. Perhaps the section we
place it in is not a critical decision, but we will need to stick with
it for a long time, and I'm not sure that we have consensus on that
point.

I think it'll be a serious, serious error for this not to be
SECTION_DATA. Maybe POST_DATA is OK, but even that seems like
an implementation compromise not "the way it ought to be".

2. We changed the stats import function API to be VARIADIC very
recently. After we have a bit of time to think on it, I'm not 100% sure
we will want to stick with that new API. It's not easy to document,
which is something I always like to consider.

Perhaps. I think the argument of wanting to be able to salvage
something even in the presence of unrecognized stats types is
stronger, but I agree this could use more time in the oven.
Unlike many other things in this patch, this would be nigh
impossible to reconsider later.

3. The error handling also changed recently to change soft errors (i.e.
type input errors) to warnings. I like this change but I'd need a bit
more time to get comfortable with how this is done, there is not a lot
of precedent for doing this kind of thing.

I don't think there's much disagreement that that's the right thing,
but yeah there could be bugs or some more to do in this area.

regards, tom lane

#153Corey Huinker
corey.huinker@gmail.com
In reply to: Tom Lane (#152)
Re: Statistics Import and Export

I think it'll be a serious, serious error for this not to be
SECTION_DATA. Maybe POST_DATA is OK, but even that seems like
an implementation compromise not "the way it ought to be".

We'd have to split them on account of when the underlying object is
created. Index statistics would be SECTION_POST_DATA, and everything else
would be SECTION_DATA. Looking ahead, statistics data for extended
statistics objects would also be POST. That's not a big change, but my
first attempt at that resulted in a bunch of unrelated grants dumping in
the wrong section.

#154Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#153)
2 attachment(s)
Re: Statistics Import and Export

On Sat, Apr 6, 2024 at 5:23 PM Corey Huinker <corey.huinker@gmail.com>
wrote:

I think it'll be a serious, serious error for this not to be
SECTION_DATA. Maybe POST_DATA is OK, but even that seems like
an implementation compromise not "the way it ought to be".

We'd have to split them on account of when the underlying object is
created. Index statistics would be SECTION_POST_DATA, and everything else
would be SECTION_DATA. Looking ahead, statistics data for extended
statistics objects would also be POST. That's not a big change, but my
first attempt at that resulted in a bunch of unrelated grants dumping in
the wrong section.

At the request of a few people, attached is an attempt to move stats to
DATA/POST-DATA, and the TAP test failure that results from that.

The relevant errors are confusing, in that they all concern GRANT/REVOKE,
and the fact that I made no changes to the TAP test itself.

$ grep 'not ok' build/meson-logs/testlog.txt
not ok 9347 - section_data: should not dump GRANT INSERT(col1) ON TABLE
test_second_table
not ok 9348 - section_data: should not dump GRANT SELECT (proname ...) ON
TABLE pg_proc TO public
not ok 9349 - section_data: should not dump GRANT SELECT ON TABLE
measurement
not ok 9350 - section_data: should not dump GRANT SELECT ON TABLE
measurement_y2006m2
not ok 9351 - section_data: should not dump GRANT SELECT ON TABLE test_table
not ok 9379 - section_data: should not dump REVOKE SELECT ON TABLE pg_proc
FROM public
not ok 9788 - section_pre_data: should dump CREATE TABLE test_table
not ok 9837 - section_pre_data: should dump GRANT INSERT(col1) ON TABLE
test_second_table
not ok 9838 - section_pre_data: should dump GRANT SELECT (proname ...) ON
TABLE pg_proc TO public
not ok 9839 - section_pre_data: should dump GRANT SELECT ON TABLE
measurement
not ok 9840 - section_pre_data: should dump GRANT SELECT ON TABLE
measurement_y2006m2
not ok 9841 - section_pre_data: should dump GRANT SELECT ON TABLE test_table
not ok 9869 - section_pre_data: should dump REVOKE SELECT ON TABLE pg_proc
FROM public

Attachments:

v18-0001-Create-pg_set_relation_stats-pg_set_attribute_st.patchtext/x-patch; charset=US-ASCII; name=v18-0001-Create-pg_set_relation_stats-pg_set_attribute_st.patchDownload
From 21a554a545cef09d13b2e15420bc01ed325bbadf Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 21 Mar 2024 18:04:54 -0400
Subject: [PATCH v18 1/2] Create pg_set_relation_stats, pg_set_attribute_stats.

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

Both functions take an oid to identify the target relation that will
receive the statistics. There is nothing requiring that relation to be
the same one as the one exported, though the statistics supplied have
to make sense in the context of the new relation.

Both functions take a variadic list of statistic parameters name-value
pairs.

Each name given for pg_set_relation_stats must correspond to a
statistics attribute in pg_class, and the paired value must match the
datatype of said attribute in pg_class.

Each name given for pg_set_attribute_stats must correspond to a
statistics attribute in pg_stats, and the paird value must match the
datatype of said attribute in pg_stats, except for attributes of type
ANYARRAY, in which case a TEXT value is required.

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

These functions 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               |   22 +-
 src/include/statistics/statistics.h           |    2 +
 src/backend/statistics/Makefile               |    3 +-
 src/backend/statistics/meson.build            |    1 +
 src/backend/statistics/statistics.c           | 1271 +++++++++++++++++
 .../regress/expected/stats_export_import.out  |  853 +++++++++++
 src/test/regress/parallel_schedule            |    3 +-
 src/test/regress/sql/stats_export_import.sql  |  671 +++++++++
 doc/src/sgml/func.sgml                        |  267 ++++
 9 files changed, 3088 insertions(+), 5 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 134e3b22fd..26f62c3651 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12178,9 +12178,6 @@
   proallargtypes => '{int8,pg_lsn,pg_lsn,oid,oid,oid,int2,int8,bool}',
   proargmodes => '{i,i,i,o,o,o,o,o,o}',
   proargnames => '{tli,start_lsn,end_lsn,relfilenode,reltablespace,reldatabase,relforknumber,relblocknumber,is_limit_block}',
-  prosrc => 'pg_wal_summary_contents' },
-{ oid => '8438',
-  descr => 'WAL summarizer state',
   proname => 'pg_get_wal_summarizer_state',
   provolatile => 'v', proparallel => 's',
   prorettype => 'record', proargtypes => '',
@@ -12195,4 +12192,23 @@
   proargtypes => 'int2',
   prosrc => 'gist_stratnum_identity' },
 
+# Statistics Import
+{ oid => '8048',
+  descr => 'set statistics on relation',
+  proname => 'pg_set_relation_stats', provariadic => 'any',
+  proisstrict => 't', provolatile => 'v',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'regclass int4 any',
+  proargnames => '{relation,version,stats}',
+  proargmodes => '{i,i,v}',
+  prosrc => 'pg_set_relation_stats' },
+{ oid => '8049',
+  descr => 'set statistics on attribute',
+  proname => 'pg_set_attribute_stats', provariadic => 'any',
+  proisstrict => 't', provolatile => 'v',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'regclass name bool int4 any',
+  proargnames => '{relation,attname,inherited,version,stats}',
+  proargmodes => '{i,i,i,i,v}',
+  prosrc => 'pg_set_attribute_stats' },
 ]
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 7f2bf18716..1dddf96576 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -127,4 +127,6 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												int nclauses);
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
+extern Datum pg_set_relation_stats(PG_FUNCTION_ARGS);
+extern Datum pg_set_attribute_stats(PG_FUNCTION_ARGS);
 #endif							/* STATISTICS_H */
diff --git a/src/backend/statistics/Makefile b/src/backend/statistics/Makefile
index 89cf8c2797..e4f8ab7c4f 100644
--- a/src/backend/statistics/Makefile
+++ b/src/backend/statistics/Makefile
@@ -16,6 +16,7 @@ OBJS = \
 	dependencies.o \
 	extended_stats.o \
 	mcv.o \
-	mvdistinct.o
+	mvdistinct.o \
+	statistics.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/statistics/meson.build b/src/backend/statistics/meson.build
index 73b29a3d50..331e82c776 100644
--- a/src/backend/statistics/meson.build
+++ b/src/backend/statistics/meson.build
@@ -5,4 +5,5 @@ backend_sources += files(
   'extended_stats.c',
   'mcv.c',
   'mvdistinct.c',
+  'statistics.c',
 )
diff --git a/src/backend/statistics/statistics.c b/src/backend/statistics/statistics.c
new file mode 100644
index 0000000000..e092eb3dc3
--- /dev/null
+++ b/src/backend/statistics/statistics.c
@@ -0,0 +1,1271 @@
+/*-------------------------------------------------------------------------
+ * statistics.c
+ *
+ *	  POSTGRES statistics import
+ *
+ * Code supporting the direct importation of relation statistics, similar to
+ * what is done by the ANALYZE command.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *       src/backend/statistics/statistics.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_collation.h"
+#include "catalog/pg_database.h"
+#include "catalog/pg_operator.h"
+#include "catalog/pg_type.h"
+#include "fmgr.h"
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "nodes/nodeFuncs.h"
+#include "statistics/statistics.h"
+#include "utils/acl.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/float.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/rangetypes.h"
+#include "utils/syscache.h"
+#include "utils/typcache.h"
+
+/*
+ * A role has privileges to set statistics on the relation if any of the
+ * following are true:
+ *   - the role owns the current database and the relation is not shared
+ *   - the role has the MAINTAIN privilege on the relation
+ *
+ */
+static bool
+can_modify_relation(Relation rel)
+{
+	Oid			relid = RelationGetRelid(rel);
+	Form_pg_class reltuple = rel->rd_rel;
+
+	return ((object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId())
+			 && !reltuple->relisshared) ||
+			pg_class_aclcheck(relid, GetUserId(), ACL_MAINTAIN) == ACLCHECK_OK);
+}
+
+/*
+ * Set statistics for a given pg_class entry.
+ *
+ * This does an in-place (i.e. non-transactional) update of pg_class, just as
+ * is done in ANALYZE.
+ *
+ */
+Datum
+pg_set_relation_stats(PG_FUNCTION_ARGS)
+{
+	Oid			relid = PG_GETARG_OID(0);
+	int			version = PG_GETARG_INT32(1);
+
+	/* indexes of where we found required stats */
+	int			i_relpages = 0;
+	int			i_reltuples = 0;
+	int			i_relallvisible = 0;
+
+	/* build argument values to build the object */
+	Datum	   *args;
+	bool	   *nulls;			/* placeholder, because strict */
+	Oid		   *types;
+	int			nargs;
+
+	/* Minimum version supported */
+	if (version <= 90200)
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Cannot export statistics prior to version 9.2")));
+
+	nargs = extract_variadic_args(fcinfo, 2, true, &args, &types, &nulls);
+
+	/* if the pairs aren't pairs, something is malformed */
+	if (nargs % 2 == 1)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("stats parameters must be in name-value pairs")));
+		PG_RETURN_BOOL(false);
+	}
+
+	/* loop through args, matching params to their arg indexes */
+	for (int i = 0; i < nargs; i += 2)
+	{
+		char	   *statname;
+		int			argidx = i + 1;
+
+		if (types[i] != TEXTOID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("stat names must be of type text")));
+			continue;
+		}
+		statname = TextDatumGetCString(args[i]);
+
+		/*
+		 * match each named parameter to the index of the value that follows
+		 * it
+		 */
+		if (strcmp(statname, "relpages") == 0)
+			i_relpages = argidx;
+		else if (strcmp(statname, "reltuples") == 0)
+			i_reltuples = argidx;
+		else if (strcmp(statname, "relallvisible") == 0)
+			i_relallvisible = argidx;
+		else
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("unknown stat naame '%s', skipping", statname)));
+
+		pfree(statname);
+	}
+
+	/*
+	 * Ensure that we got all required parameters, and they are of the correct
+	 * type.
+	 */
+	if (i_relpages == 0)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "relpages")));
+		PG_RETURN_BOOL(false);
+	}
+	else if (types[i_relpages] != INT4OID)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relpages must be of type integer")));
+		PG_RETURN_BOOL(false);
+	}
+	else if (i_reltuples == 0)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "reltuples")));
+		PG_RETURN_BOOL(false);
+	}
+	else if (types[i_reltuples] != FLOAT4OID)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("reltuples must be of type real")));
+		PG_RETURN_BOOL(false);
+	}
+	else if (types[i_relallvisible] != INT4OID)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "relallvisible")));
+		PG_RETURN_BOOL(false);
+	}
+	else if (i_relallvisible == -1)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relallvisible must be of type integer")));
+		PG_RETURN_BOOL(false);
+	}
+	else
+	{
+		/*
+		 * Open the relation, getting ShareUpdateExclusiveLock to ensure that
+		 * no other stat-setting operation can run on it concurrently.
+		 */
+		Relation	rel;
+		HeapTuple	ctup;
+		Form_pg_class pgcform;
+		int			relpages;
+		float4		reltuples;
+		int			relallvisible;
+
+		rel = table_open(RelationRelationId, ShareUpdateExclusiveLock);
+
+		if (!can_modify_relation(rel))
+		{
+			table_close(rel, NoLock);
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("must be owner to modify relation stats")));
+			PG_RETURN_BOOL(false);
+		}
+
+		relpages = DatumGetInt32(args[i_relpages]);
+		reltuples = DatumGetFloat4(args[i_reltuples]);
+		relallvisible = DatumGetInt32(args[i_relallvisible]);
+
+		ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+		if (!HeapTupleIsValid(ctup))
+			ereport(ERROR,
+					(errcode(ERRCODE_OBJECT_IN_USE),
+					 errmsg("pg_class entry for relid %u not found", relid)));
+
+		pgcform = (Form_pg_class) GETSTRUCT(ctup);
+		/* Only update pg_class if there is a meaningful change */
+		if ((pgcform->reltuples != reltuples)
+			|| (pgcform->relpages != relpages)
+			|| (pgcform->relallvisible != relallvisible))
+		{
+			pgcform->relpages = relpages;
+			pgcform->reltuples = reltuples;
+			pgcform->relallvisible = relallvisible;
+
+			heap_inplace_update(rel, ctup);
+		}
+
+		table_close(rel, NoLock);
+		PG_RETURN_BOOL(true);
+	}
+
+	PG_RETURN_BOOL(false);
+}
+
+/*
+ * Test if the type is a scalar for MCELEM purposes
+ */
+static bool
+type_is_scalar(Oid typid)
+{
+	HeapTuple	tp;
+	bool		result = false;
+
+	tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+	if (HeapTupleIsValid(tp))
+	{
+		Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+
+		result = (!OidIsValid(typtup->typanalyze));
+		ReleaseSysCache(tp);
+	}
+	return result;
+}
+
+/*
+ * If this relation is an index and that index has expressions in it, and
+ * the attnum specified is known to be an expression, then we must walk
+ * the list attributes up to the specified attnum to get the right
+ * expression.
+ */
+static Node *
+get_attr_expr(Relation rel, int attnum)
+{
+	if ((rel->rd_rel->relkind == RELKIND_INDEX
+		 || (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+		&& (rel->rd_indexprs != NIL)
+		&& (rel->rd_index->indkey.values[attnum - 1] == 0))
+	{
+		ListCell   *indexpr_item = list_head(rel->rd_indexprs);
+
+		for (int i = 0; i < attnum - 1; i++)
+			if (rel->rd_index->indkey.values[i] == 0)
+				indexpr_item = lnext(rel->rd_indexprs, indexpr_item);
+
+		if (indexpr_item == NULL)	/* shouldn't happen */
+			elog(ERROR, "too few entries in indexprs list");
+
+		return (Node *) lfirst(indexpr_item);
+	}
+	return NULL;
+}
+
+/*
+ * Fetch datatype information, this is needed to derive the proper staopN
+ * and stacollN values.
+ *
+ */
+static TypeCacheEntry *
+get_attr_stat_type(Relation rel, Name attname,
+				   int16 *attnum, int32 *typmod, Oid *typcoll)
+{
+	Oid			relid = RelationGetRelid(rel);
+	Form_pg_attribute attr;
+	HeapTuple	atup;
+	Oid			typid;
+	Node	   *expr;
+
+	atup = SearchSysCache2(ATTNAME, ObjectIdGetDatum(relid),
+						   NameGetDatum(attname));
+
+	/* Attribute not found */
+	if (!HeapTupleIsValid(atup))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s has no attname %s",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+		return NULL;
+	}
+
+	attr = (Form_pg_attribute) GETSTRUCT(atup);
+	if (attr->attisdropped)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s attname %s is dropped",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+		return NULL;
+	}
+	*attnum = attr->attnum;
+
+	expr = get_attr_expr(rel, attr->attnum);
+
+	if (expr == NULL)
+	{
+		/* regular attribute */
+		typid = attr->atttypid;
+		*typmod = attr->atttypmod;
+		*typcoll = attr->attcollation;
+	}
+	else
+	{
+		typid = exprType(expr);
+		*typmod = exprTypmod(expr);
+
+		/*
+		 * If a collation has been specified for the index column, use that in
+		 * preference to anything else; but if not, fall back to whatever we
+		 * can get from the expression.
+		 */
+		if (OidIsValid(attr->attcollation))
+			*typcoll = attr->attcollation;
+		else
+			*typcoll = exprCollation(expr);
+	}
+	ReleaseSysCache(atup);
+
+	/* if it's a multirange, step down to the range type */
+	if (type_is_multirange(typid))
+		typid = get_multirange_range(typid);
+
+	return lookup_type_cache(typid,
+							 TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR);
+}
+
+/*
+ * Perform the cast of a known TextDatum into the type specified.
+ *
+ * If no errors are found, ok is set to true. Otherwise, set ok
+ * to false, capture the error found, and re-throw at warning level.
+ */
+static Datum
+cast_stavalues(FmgrInfo *flinfo, Datum d, Oid typid, int32 typmod, bool *ok)
+{
+	LOCAL_FCINFO(fcinfo, 8);
+	char	   *s;
+	Datum		result;
+	ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+	escontext.details_wanted = true;
+
+	s = TextDatumGetCString(d);
+
+	InitFunctionCallInfoData(*fcinfo, flinfo, 3, InvalidOid,
+							 (Node *) &escontext, NULL);
+
+	fcinfo->args[0].value = CStringGetDatum(s);
+	fcinfo->args[0].isnull = false;
+	fcinfo->args[1].value = ObjectIdGetDatum(typid);
+	fcinfo->args[1].isnull = false;
+	fcinfo->args[2].value = Int32GetDatum(typmod);
+	fcinfo->args[2].isnull = false;
+
+	result = FunctionCallInvoke(fcinfo);
+
+	if (SOFT_ERROR_OCCURRED(&escontext))
+	{
+		escontext.error_data->elevel = WARNING;
+		ThrowErrorData(escontext.error_data);
+		*ok = false;
+	}
+	else
+		*ok = true;
+
+	pfree(s);
+
+	return result;
+}
+
+
+/*
+ * Check array for any NULLs, and optionally for one-dimensionality.
+ *
+ * Report any failures as warnings.
+ */
+static bool
+array_check(Datum datum, int one_dim, const char *statname)
+{
+	ArrayType  *arr = DatumGetArrayTypeP(datum);
+	int16		elmlen;
+	char		elmalign;
+	bool		elembyval;
+	Datum	   *values;
+	bool	   *nulls;
+	int			nelems;
+
+	if (one_dim && (arr->ndim != 1))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be a multidimensional array", statname)));
+		return false;
+	}
+
+	get_typlenbyvalalign(ARR_ELEMTYPE(arr), &elmlen, &elembyval, &elmalign);
+
+	deconstruct_array(arr, ARR_ELEMTYPE(arr), elmlen, elembyval, elmalign,
+					  &values, &nulls, &nelems);
+
+	for (int i = 0; i < nelems; i++)
+		if (nulls[i])
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s array cannot contain NULL values", statname)));
+			return false;
+		}
+	return true;
+}
+
+/*
+ * Update the pg_statistic record.
+ */
+static void
+update_pg_statistic(Datum values[], bool nulls[])
+{
+	Relation	sd = table_open(StatisticRelationId, RowExclusiveLock);
+	CatalogIndexState indstate = CatalogOpenIndexes(sd);
+	HeapTuple	oldtup;
+	HeapTuple	stup;
+
+	/* Is there already a pg_statistic tuple for this attribute? */
+	oldtup = SearchSysCache3(STATRELATTINH,
+							 values[Anum_pg_statistic_starelid - 1],
+							 values[Anum_pg_statistic_staattnum - 1],
+							 values[Anum_pg_statistic_stainherit - 1]);
+
+	if (HeapTupleIsValid(oldtup))
+	{
+		/* Yes, replace it */
+		bool		replaces[Natts_pg_statistic];
+
+		for (int i = 0; i < Natts_pg_statistic; i++)
+			replaces[i] = true;
+
+		stup = heap_modify_tuple(oldtup, RelationGetDescr(sd),
+								 values, nulls, replaces);
+		ReleaseSysCache(oldtup);
+		CatalogTupleUpdateWithInfo(sd, &stup->t_self, stup, indstate);
+	}
+	else
+	{
+		/* No, insert new tuple */
+		stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
+		CatalogTupleInsertWithInfo(sd, stup, indstate);
+	}
+
+	heap_freetuple(stup);
+	CatalogCloseIndexes(indstate);
+	table_close(sd, NoLock);
+}
+
+/*
+ * Import statistics for a given relation attribute.
+ *
+ * This will insert/replace a row in pg_statistic for the given combinarion
+ * of relation, attribute, and inherited flag.
+ *
+ * The version parameter is for future use in the events as future versions
+ * may change them meaning of certain parameters.
+ *
+ * The variadic parameters all represent name-value pairs, with the names
+ * corresponding to attributes in pg_stats. Unkown names will generate a
+ * warning.
+ *
+ * Parameters null_frac, avg_width, and n_distinct are required because
+ * those attributes have no default value in pg_statistic.
+ *
+ * The remaining parameters all belong to a specific stakind, and all are
+ * optional. Some stakinds have multiple parameters, and in those cases
+ * both parameters must be specified if one of them is, otherwise a
+ * warning is generated but the rest of the stats may still be imported.
+ *
+ * If there is no attribute with a matching attname in the relation, the
+ * function will raise a warning and return false.
+ *
+ * Parameters corresponding to ANYARRAY columns are instead passed in as text
+ * values, which is a valid input string for an array of the type or element
+ * type of the attribute.
+ */
+Datum
+pg_set_attribute_stats(PG_FUNCTION_ARGS)
+{
+	Oid			relid = PG_GETARG_OID(0);
+	Name		attname = PG_GETARG_NAME(1);
+	bool		inherited = PG_GETARG_BOOL(2);
+	int			version = PG_GETARG_INT32(3);
+
+	/* build argument values to build the object */
+	Datum	   *args;
+	bool	   *argnulls;		/* placeholder, because strict */
+	Oid		   *types;
+	int			nargs = extract_variadic_args(fcinfo, 4, true,
+											  &args, &types, &argnulls);
+
+	Datum		values[Natts_pg_statistic] = {0};
+	bool		nulls[Natts_pg_statistic] = {false};
+
+	/*
+	 * argument indexes for each known statistic
+	 *
+	 * 0 = not found, -1 = error, n > 0 = found
+	 */
+
+	/* parameters that are required get indexes */
+	int			i_null_frac = 0;
+	int			i_avg_width = 0;
+	int			i_n_distinct = 0;
+
+	/* stakind stats are optional */
+	int			i_mc_vals = 0;
+	int			i_mc_freqs = 0;
+	int			i_hist_bounds = 0;
+	int			i_correlation = 0;
+	int			i_mc_elems = 0;
+	int			i_mc_elem_freqs = 0;
+	int			i_elem_count_hist = 0;
+	int			i_range_length_hist = 0;
+	int			i_range_empty_frac = 0;
+	int			i_range_bounds_hist = 0;
+
+	Relation	rel;
+
+	TypeCacheEntry *typcache;
+	TypeCacheEntry *elemtypcache = NULL;
+
+	int16		attnum;
+	int32		typmod;
+	Oid			typcoll;
+
+	FmgrInfo	finfo;
+
+	int			stakind_count;
+
+	int			k = 0;
+
+	if (version <= 90200)
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Cannot export statistics prior to version 9.2")));
+
+	/* if the pairs aren't pairs, something is malformed */
+	if (nargs % 2 == 1)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("stats parameters must be in name-value pairs")));
+		PG_RETURN_BOOL(false);
+	}
+
+	for (int i = 0; i < nargs; i += 2)
+	{
+		char	   *statname;
+		int			argidx = i + 1;
+
+		if (types[i] != TEXTOID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("stat names must be of type text")));
+			break;
+		}
+		statname = TextDatumGetCString(args[i]);
+
+		if (strcmp(statname, "null_frac") == 0)
+			i_null_frac = argidx;
+		else if (strcmp(statname, "avg_width") == 0)
+			i_avg_width = argidx;
+		else if (strcmp(statname, "n_distinct") == 0)
+			i_n_distinct = argidx;
+		else if (strcmp(statname, "most_common_vals") == 0)
+			i_mc_vals = argidx;
+		else if (strcmp(statname, "most_common_freqs") == 0)
+			i_mc_freqs = argidx;
+		else if (strcmp(statname, "histogram_bounds") == 0)
+			i_hist_bounds = argidx;
+		else if (strcmp(statname, "correlation") == 0)
+			i_correlation = argidx;
+		else if (strcmp(statname, "most_common_elems") == 0)
+			i_mc_elems = argidx;
+		else if (strcmp(statname, "most_common_elem_freqs") == 0)
+			i_mc_elem_freqs = argidx;
+		else if (strcmp(statname, "elem_count_histogram") == 0)
+			i_elem_count_hist = argidx;
+		else if (strcmp(statname, "range_length_histogram") == 0)
+			i_range_length_hist = argidx;
+		else if (strcmp(statname, "range_empty_frac") == 0)
+			i_range_empty_frac = argidx;
+		else if (strcmp(statname, "range_bounds_histogram") == 0)
+			i_range_bounds_hist = argidx;
+		else
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("unknown stat naame '%s', skipping", statname)));
+
+		pfree(statname);
+	}
+
+	/* check all required parameters */
+	if (i_null_frac > 0)
+	{
+		if (types[i_null_frac] != FLOAT4OID)
+		{
+			/* required param, not recoverable */
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be of type %s", "null_frac",
+							"real")));
+			PG_RETURN_BOOL(false);
+		}
+	}
+	else
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "null_frac")));
+		PG_RETURN_BOOL(false);
+	}
+
+	if (i_avg_width > 0)
+	{
+		if (types[i_avg_width] != INT4OID)
+		{
+			/* required param, not recoverable */
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be of type %s", "avg_width",
+							"integer")));
+			PG_RETURN_BOOL(false);
+		}
+	}
+	else
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "avg_width")));
+		PG_RETURN_BOOL(false);
+	}
+
+	if (i_n_distinct > 0)
+	{
+		if (types[i_n_distinct] != FLOAT4OID)
+		{
+			/* required param, not recoverable */
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be of type %s", "n_distinct",
+							"real")));
+			PG_RETURN_BOOL(false);
+		}
+	}
+	else
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "n_distinct")));
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * Look for pair mismatches, if found warn and disable.
+	 */
+	if ((i_mc_vals == 0) != (i_mc_freqs == 0))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be present if %s is missing",
+						(i_mc_vals == 0) ? "most_common_freqs" :
+						"most_common_vals",
+						(i_mc_vals == 0) ? "most_common_vals" :
+						"most_common_freqs")));
+		i_mc_vals = -1;
+		i_mc_freqs = -1;
+	}
+
+	if ((i_mc_elems == 0) != (i_mc_elem_freqs == 0))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be present if %s is missing",
+						(i_mc_elems == 0) ?
+						"most_common_elem_freqs" :
+						"most_common_elems",
+						(i_mc_elems == 0) ?
+						"most_common_elems" :
+						"most_common_elem_freqs")));
+		i_mc_elems = -1;
+		i_mc_elem_freqs = -1;
+	}
+
+	if ((i_range_length_hist == 0) != (i_range_empty_frac == 0))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be present if %s is missing",
+						(i_range_length_hist == 0) ?
+						"range_empty_frac" :
+						"range_length_histogram",
+						(i_range_length_hist == 0) ?
+						"range_length_histogram" :
+						"range_empty_frac")));
+		i_range_length_hist = -1;
+		i_range_empty_frac = -1;
+	}
+
+	rel = relation_open(relid, ShareUpdateExclusiveLock);
+
+	if (!can_modify_relation(rel))
+	{
+		relation_close(rel, NoLock);
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						RelationGetRelationName(rel))));
+		PG_RETURN_BOOL(false);
+	}
+
+	if ((!inherited) &&
+		((rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) ||
+		 (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX)))
+	{
+		relation_close(rel, NoLock);
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("%s is partitioned, can only accepted inherted stats",
+						RelationGetRelationName(rel))));
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * Many of the values that are set for a particular stakind are entirely
+	 * derived from the attribute itself, or it's expression.
+	 */
+	typcache = get_attr_stat_type(rel, attname, &attnum, &typmod, &typcoll);
+	if (typcache == NULL)
+	{
+		relation_close(rel, NoLock);
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * Derive element type if we have stat kinds that need it.
+	 *
+	 * This duplicates some type-specific logic found in various typanalyze
+	 * functions which are called from vacuum's examine_attribute().
+	 */
+	if ((i_mc_elems > 0) || (i_elem_count_hist > 0))
+	{
+		Oid			elemtypid;
+
+		if (typcache->type_id == TSVECTOROID)
+		{
+			/*
+			 * tsvector elems always have a text oid type and default
+			 * collation
+			 */
+			elemtypid = TEXTOID;
+			typcoll = DEFAULT_COLLATION_OID;
+		}
+		else if (typcache->typtype == TYPTYPE_RANGE)
+			elemtypid = get_range_subtype(typcache->type_id);
+		else
+			elemtypid = get_base_element_type(typcache->type_id);
+
+		/* not finding a basetype means we already had it */
+		if (elemtypid == InvalidOid)
+			elemtypid = typcache->type_id;
+
+		/* The stats need the eq_opr, any validation would need the lt_opr */
+		elemtypcache = lookup_type_cache(elemtypid, TYPECACHE_EQ_OPR);
+
+		if (elemtypcache == NULL)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							(i_mc_elems > 0) ?
+							"most_common_elems" :
+							"elem_count_histogram")));
+			i_mc_elems = -1;
+			i_elem_count_hist = -1;
+		}
+	}
+
+	/*
+	 * histogram_bounds and correlation must have a type < operator. WARN and
+	 * skip if this attribute doesn't have one.
+	 */
+	if (typcache->lt_opr == InvalidOid)
+	{
+		if (i_hist_bounds > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"histogram_bounds")));
+			i_hist_bounds = -1;
+		}
+		if (i_correlation > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"correlation")));
+			i_correlation = -1;
+		}
+	}
+
+	/*
+	 * Scalar types can't have most_common_elems, most_common_elem_freqs,
+	 * elem_count_histogram. WARN and skip.
+	 */
+	if (type_is_scalar(typcache->type_id))
+	{
+		if (i_mc_elems > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"most_common_elems")));
+			i_mc_elems = -1;
+		}
+		if (i_mc_elem_freqs > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"most_common_elem_freqs")));
+			i_mc_elem_freqs = -1;
+		}
+		if (i_elem_count_hist > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"elem_count_histogram")));
+			i_elem_count_hist = -1;
+		}
+	}
+
+	/*
+	 * Only range types can have range_length_histogram, range_empty_frac, and
+	 * range_bounds_histogram. WARN and skip
+	 */
+	if ((typcache->typtype != TYPTYPE_MULTIRANGE) &&
+		(typcache->typtype != TYPTYPE_RANGE))
+	{
+		if (i_range_length_hist > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"range_length_histogram")));
+			i_range_length_hist = -1;
+		}
+		if (i_range_empty_frac > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"range_empty_frac")));
+			i_range_empty_frac = -1;
+		}
+		if (i_range_bounds_hist > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"range_bounds_histogram")));
+			i_range_bounds_hist = -1;
+		}
+	}
+
+	/*
+	 * count the number of stakinds we still want to set, paired params count
+	 * as one. The count cannot exceed STATISTIC_NUM_SLOTS.
+	 */
+	stakind_count = (int) (i_mc_vals > 0) +
+		(int) (i_mc_elems > 0) +
+		(int) (i_range_length_hist > 0) +
+		(int) (i_hist_bounds > 0) +
+		(int) (i_correlation > 0) +
+		(int) (i_elem_count_hist > 0) +
+		(int) (i_range_bounds_hist > 0);
+
+	if (stakind_count > STATISTIC_NUM_SLOTS)
+	{
+		/*
+		 * This really shouldn't happen, as most datatypes exclude at least
+		 * one of these types of stats.
+		 */
+		relation_close(rel, NoLock);
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("imported statistics must have a maximum of %d slots "
+						"but %d given",
+						STATISTIC_NUM_SLOTS, stakind_count)));
+		PG_RETURN_BOOL(false);
+	}
+
+	fmgr_info(F_ARRAY_IN, &finfo);
+
+	values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(relid);
+	values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(attnum);
+	values[Anum_pg_statistic_stainherit - 1] = BoolGetDatum(inherited);
+	values[Anum_pg_statistic_stanullfrac - 1] = args[i_null_frac];
+	values[Anum_pg_statistic_stawidth - 1] = args[i_avg_width];
+	values[Anum_pg_statistic_stadistinct - 1] = args[i_n_distinct];
+
+	/*
+	 * STATISTIC_KIND_MCV
+	 *
+	 * most_common_freqs: real[]
+	 *
+	 * most_common_vals : ANYARRAY::text
+	 */
+	if (i_mc_vals > 0)
+	{
+		Oid			numberstype = types[i_mc_freqs];
+		Oid			valuestype = types[i_mc_vals];
+		Datum		stanumbers = args[i_mc_freqs];
+		Datum		strvalue = args[i_mc_vals];
+
+		if (get_element_type(numberstype) != FLOAT4OID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"most_common_freqs", "real[]")));
+		}
+		else if (valuestype != TEXTOID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"most_common_vals", "text")));
+		}
+		else
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_MCV);
+			Datum		staop = ObjectIdGetDatum(typcache->eq_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		stavalues;
+			bool		converted = false;
+
+			stavalues = cast_stavalues(&finfo, strvalue, typcache->type_id,
+									   typmod, &converted);
+
+			if (converted &&
+				array_check(stavalues, false, "most_common_vals") &&
+				array_check(stanumbers, true, "most_common_freqs"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_HISTOGRAM
+	 *
+	 * histogram_bounds: ANYARRAY::text
+	 */
+	if (i_hist_bounds > 0)
+	{
+		Oid			valuestype = types[i_hist_bounds];
+		Datum		strvalue = args[i_hist_bounds];
+
+		if (valuestype != TEXTOID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"histogram_bounds", "text")));
+		else
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_HISTOGRAM);
+			Datum		staop = ObjectIdGetDatum(typcache->lt_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		stavalues;
+			bool		converted = false;
+
+			stavalues = cast_stavalues(&finfo, strvalue, typcache->type_id,
+									   typmod, &converted);
+
+			if (converted && array_check(stavalues, false, "histogram_bounds"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_CORRELATION
+	 *
+	 * correlation: real
+	 */
+	if (i_correlation > 0)
+	{
+		if (types[i_correlation] != FLOAT4OID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be of type %s, ignored",
+							"correlation", "real")));
+		else
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_CORRELATION);
+			Datum		staop = ObjectIdGetDatum(typcache->lt_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		elems[] = {args[i_correlation]};
+			ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+			Datum		stanumbers = PointerGetDatum(arry);
+
+			values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+			values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+			values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+			values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+			nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+			k++;
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_MCELEM
+	 *
+	 * most_common_elem_freqs: real[]
+	 *
+	 * most_common_elems     : ANYARRAY::text
+	 */
+	if (i_mc_elems > 0)
+	{
+		Oid			numberstype = types[i_mc_elem_freqs];
+		Oid			valuestype = types[i_mc_elems];
+		Datum		stanumbers = args[i_mc_elem_freqs];
+
+		if (get_element_type(numberstype) != FLOAT4OID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"most_common_elem_freqs", "real[]")));
+		else if (valuestype != TEXTOID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"most_common_elems", "text")));
+		else
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_MCELEM);
+			Datum		staop = ObjectIdGetDatum(elemtypcache->eq_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		strvalue = args[i_mc_elems];
+			bool		converted = false;
+			Datum		stavalues;
+
+			stavalues = cast_stavalues(&finfo, strvalue, elemtypcache->type_id,
+									   typmod, &converted);
+
+			if (converted &&
+				array_check(stavalues, false, "most_common_elems") &&
+				array_check(stanumbers, true, "most_common_elem_freqs"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_DECHIST
+	 *
+	 * elem_count_histogram:	real[]
+	 */
+	if (i_elem_count_hist > 0)
+	{
+		Oid			numberstype = types[i_elem_count_hist];
+
+		if (get_element_type(numberstype) != FLOAT4OID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"elem_count_histogram", "real[]")));
+		else
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_DECHIST);
+			Datum		staop = ObjectIdGetDatum(elemtypcache->eq_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		stanumbers = args[i_elem_count_hist];
+
+			if (array_check(stanumbers, true, "elem_count_histogram"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+				nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+				k++;
+			}
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_BOUNDS_HISTOGRAM
+	 *
+	 * range_bounds_histogram: ANYARRAY::text
+	 *
+	 * This stakind appears before STATISTIC_KIND_BOUNDS_HISTOGRAM even though
+	 * it is numerically greater, and all other stakinds appear in numerical
+	 * order. We duplicate this quirk to make before/after tests of
+	 * pg_statistic records easier.
+	 */
+	if (i_range_bounds_hist > 0)
+	{
+		Oid			valuestype = types[i_range_bounds_hist];
+
+		if (valuestype != TEXTOID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"range_bounds_histogram", "text")));
+		else
+		{
+
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_BOUNDS_HISTOGRAM);
+			Datum		staop = ObjectIdGetDatum(InvalidOid);
+			Datum		stacoll = ObjectIdGetDatum(InvalidOid);
+			Datum		strvalue = args[i_range_bounds_hist];
+			bool		converted = false;
+			Datum		stavalues;
+
+			stavalues = cast_stavalues(&finfo, strvalue, typcache->type_id,
+									   typmod, &converted);
+
+			if (converted &&
+				array_check(stavalues, false, "range_bounds_histogram"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM
+	 *
+	 * range_empty_frac: real
+	 *
+	 * range_length_histogram:  double precision[]::text
+	 */
+	if (i_range_length_hist > 0)
+	{
+		Oid			numberstype = types[i_range_empty_frac];
+		Oid			valuestype = types[i_range_length_hist];
+
+		if (numberstype != FLOAT4OID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"range_empty_frac", "real")));
+		else if (valuestype != TEXTOID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"range_length_histogram", "text")));
+		else
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM);
+			Datum		staop = ObjectIdGetDatum(Float8LessOperator);
+			Datum		stacoll = ObjectIdGetDatum(InvalidOid);
+
+			/* The anyarray is always a float8[] for this stakind */
+			Datum		elem = args[i_range_empty_frac];
+			Datum		elems[] = {elem};
+			ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+			Datum		stanumbers = PointerGetDatum(arry);
+			Datum		strvalue = args[i_range_length_hist];
+			bool		converted = false;
+			Datum		stavalues;
+
+			stavalues = cast_stavalues(&finfo, strvalue, FLOAT8OID, 0, &converted);
+
+			if (converted &&
+				array_check(stavalues, false, "range_length_histogram"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+	}
+
+	/* fill in all remaining slots */
+	for (; k < STATISTIC_NUM_SLOTS; k++)
+	{
+		values[Anum_pg_statistic_stakind1 - 1 + k] = Int16GetDatum(0);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+	}
+
+	update_pg_statistic(values, nulls);
+
+	relation_close(rel, NoLock);
+	PG_RETURN_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..684df93993
--- /dev/null
+++ b/src/test/regress/expected/stats_export_import.out
@@ -0,0 +1,853 @@
+CREATE SCHEMA stats_export_import;
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+        0 |        -1 |             0
+(1 row)
+
+-- error: regclass not found
+SELECT pg_set_relation_stats('stats_export_import.nope'::regclass,
+                             150000::integer);
+ERROR:  relation "stats_export_import.nope" does not exist
+LINE 1: SELECT pg_set_relation_stats('stats_export_import.nope'::reg...
+                                     ^
+-- error: all three params missing
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer);
+ERROR:  function pg_set_relation_stats(regclass, integer) does not exist
+LINE 1: SELECT pg_set_relation_stats('stats_export_import.test'::reg...
+               ^
+HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+-- error: reltuples, relallvisible missing
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 4::integer);
+WARNING:  required parameter reltuples not set
+ pg_set_relation_stats 
+-----------------------
+ f
+(1 row)
+
+-- error: null value
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', NULL::real,
+                             'relallvisible', 4::integer);
+ pg_set_relation_stats 
+-----------------------
+ 
+(1 row)
+
+-- error: bad relpages type
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+WARNING:  relpages must be of type integer
+ pg_set_relation_stats 
+-----------------------
+ f
+(1 row)
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 17::integer,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+ pg_set_relation_stats 
+-----------------------
+ t
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       17 |       400 |             4
+(1 row)
+
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    '0'::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ERROR:  could not open relation with OID 0
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    NULL::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    NULL::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    NULL::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', NULL::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', NULL::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', NULL::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT stanullfrac, stawidth, stadistinct
+FROM pg_statistic
+WHERE starelid = 'stats_export_import.test'::regclass;
+ stanullfrac | stawidth | stadistinct 
+-------------+----------+-------------
+         0.1 |        2 |         0.3
+(1 row)
+
+-- warn: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_freqs', '{0.1,0.2,0.3}'::real[]
+    );
+WARNING:  most_common_freqs cannot be present if most_common_vals is missing
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{1,2,3}'::text
+    );
+WARNING:  most_common_vals cannot be present if most_common_freqs is missing
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.2,0.1}'::double precision[]
+    );
+WARNING:  most_common_freqs must be type real[], ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warning: mcv cast failure
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,four,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+WARNING:  invalid input syntax for type integer: "four"
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: histogram elements null value
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,NULL,3,4}'::text
+    );
+WARNING:  histogram_bounds array cannot contain NULL values
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,2,3,4}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'correlation', 0.5::real);
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |         0.5 |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{1,3}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+WARNING:  id cannot accept most_common_elems stats, ignored
+WARNING:  id cannot accept most_common_elem_freqs stats, ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,two}'::text
+    );
+WARNING:  most_common_elems cannot be present if most_common_elem_freqs is missing
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
+    );
+WARNING:  most_common_elem_freqs cannot be present if most_common_elems is missing
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             | {one,three}       | {0.3,0.2,0.2,0.3,0}    |                      |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+WARNING:  id cannot accept elem_count_histogram stats, ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+WARNING:  elem_count_histogram array cannot contain NULL values
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+WARNING:  id cannot accept range_length_histogram stats, ignored
+WARNING:  id cannot accept range_empty_frac stats, ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+WARNING:  range_length_histogram cannot be present if range_empty_frac is missing
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real
+    );
+WARNING:  range_empty_frac cannot be present if range_length_histogram is missing
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
+(1 row)
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+WARNING:  id cannot accept range_bounds_histogram stats, ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
+ stats_export_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      |                        |                  | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+(1 row)
+
+-- warn: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
+    'histogram_bounds', '{1,2,3,4}'::text,
+    'correlation', 1.1::real,
+    'most_common_elems', '{3,1}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    'range_empty_frac', -0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+WARNING:  imported statistics must have a maximum of 5 slots but 6 given
+ pg_set_attribute_stats 
+------------------------
+ f
+(1 row)
+
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+   relname    | num_stats 
+--------------+-----------
+ is_odd       |         1
+ is_odd_clone |         1
+ test         |         5
+ test_clone   |         5
+(4 rows)
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 675c567617..bcd7c0720e 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,8 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc stats_export_import
+
 
 # ----------
 # Load huge amounts of data
diff --git a/src/test/regress/sql/stats_export_import.sql b/src/test/regress/sql/stats_export_import.sql
new file mode 100644
index 0000000000..ebdb58aba1
--- /dev/null
+++ b/src/test/regress/sql/stats_export_import.sql
@@ -0,0 +1,671 @@
+CREATE SCHEMA stats_export_import;
+
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+-- error: regclass not found
+SELECT pg_set_relation_stats('stats_export_import.nope'::regclass,
+                             150000::integer);
+
+-- error: all three params missing
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer);
+
+-- error: reltuples, relallvisible missing
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 4::integer);
+
+-- error: null value
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', NULL::real,
+                             'relallvisible', 4::integer);
+
+-- error: bad relpages type
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 17::integer,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_export_import.test'::regclass;
+
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    '0'::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    NULL::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    NULL::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    NULL::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', NULL::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', NULL::integer,
+    'n_distinct', 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', NULL::real);
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+SELECT stanullfrac, stawidth, stadistinct
+FROM pg_statistic
+WHERE starelid = 'stats_export_import.test'::regclass;
+
+-- warn: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_freqs', '{0.1,0.2,0.3}'::real[]
+    );
+
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{1,2,3}'::text
+    );
+
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.2,0.1}'::double precision[]
+    );
+
+-- warning: mcv cast failure
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,four,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: histogram elements null value
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,NULL,3,4}'::text
+    );
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,2,3,4}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'correlation', 0.5::real);
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: scalars can't have mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{1,3}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,two}'::text
+    );
+
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
+    );
+
+-- ok: mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- warn: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real
+    );
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
+    'histogram_bounds', '{1,2,3,4}'::text,
+    'correlation', 1.1::real,
+    'most_common_elems', '{3,1}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    'range_empty_frac', -0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+
+SELECT
+    format('SELECT pg_catalog.pg_set_attribute_stats( '
+            || '%L::regclass::oid, '
+            || '%L::name, '
+            || '%L::boolean, '
+            || '%L::integer, '
+            || '%L, %L::real, '
+            || '%L, %L::integer, '
+            || '%L, %L::real %s)',
+        'stats_export_import.' || s.tablename || '_clone',
+        s.attname,
+        s.inherited,
+        150000,
+        'null_frac', s.null_frac,
+        'avg_width', s.avg_width,
+        'n_distinct', s.n_distinct,
+        CASE
+            WHEN s.most_common_vals IS NULL THEN ''
+            ELSE format(', %L, %L::text, %L, %L::real[]',
+                        'most_common_vals', s.most_common_vals,
+                        'most_common_freqs', s.most_common_freqs)
+        END ||
+        CASE
+            WHEN s.histogram_bounds IS NULL THEN ''
+            ELSE format(', %L, %L::text',
+                        'histogram_bounds', s.histogram_bounds)
+        END ||
+        CASE
+            WHEN s.correlation IS NULL THEN ''
+            ELSE format(', %L, %L::real',
+                        'correlation', s.correlation)
+        END ||
+        CASE
+            WHEN s.most_common_elems IS NULL THEN ''
+            ELSE format(', %L, %L::text, %L, %L::real[]',
+                        'most_common_elems', s.most_common_elems,
+                        'most_common_elem_freqs', s.most_common_elem_freqs)
+        END ||
+        CASE
+            WHEN s.elem_count_histogram IS NULL THEN ''
+            ELSE format(', %L, %L::real[]',
+                        'elem_count_histogram', s.elem_count_histogram)
+        END ||
+        CASE
+            WHEN s.range_bounds_histogram IS NULL THEN ''
+            ELSE format(', %L, %L::text',
+                        'range_bounds_histogram', s.range_bounds_histogram)
+        END ||
+        CASE
+            WHEN s.range_empty_frac IS NULL THEN ''
+            ELSE format(', %L, %L::real, %L, %L::text',
+                        'range_empty_frac', s.range_empty_frac,
+                        'range_length_histogram', s.range_length_histogram)
+        END)
+FROM pg_catalog.pg_stats AS s
+WHERE s.schemaname = 'stats_export_import'
+AND s.tablename IN ('test', 'is_odd')
+\gexec
+
+-- restore ECHO to original value
+\set ECHO :orig_ECHO
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 8dfb42ad4d..509566dbca 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29633,6 +29633,273 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
     in identifying the specific disk files associated with database objects.
    </para>
 
+   <table id="functions-admin-statsimport">
+    <title>Database Object Statistics Import Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_relation_stats</primary>
+        </indexterm>
+        <function>pg_set_relation_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>version</parameter> <type>integer</type>,
+         <literal>VARIADIC</literal> <parameter>stats</parameter> <type>"any"</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Updates the <structname>pg_class</structname> row for the specified
+        <parameter>relation</parameter>, values from the variadic list of
+        name-value pairs. Each parameter name corresponds to the same-named
+        attribute in <structname>pg_class</structname>:
+       </para>
+       <variablelist>
+        <varlistentry>
+         <term><literal>relpages</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>integer</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>reltuples</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>relalltuples</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>integer</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+       </variablelist>
+       <para>
+        Any other parameter names given must also have a value pair, but will emit
+        a warning and thereafter be ignored.
+       </para>
+       <para>
+        The <parameter>version</parameter> is meant to reflect the server version
+        number of the system where this data was generated, as that may in the
+        future change how the data is imported.
+       </para>
+       <para>
+        To avoid table bloat in <structname>pg_class</structname>, this change
+        is made with an in-place update, and therefore cannot be rolled back
+        through normal transaction processing.
+       </para>
+       <para>
+        This function mimics the behavior of <command>ANALYZE</command> in its
+        effects on the values in <structname>pg_class</structname>, except that
+        the values are supplied as parameters rather than derived from table
+        sampling.
+       </para>
+       <para>
+        The caller must either be the owner of the relation, or have superuser
+        privileges.
+       </para>
+       <para>
+        The parameters given are checked for type validity and must all be
+        NOT NULL.
+       </para>
+       <para>
+        The parameters will return true if stats were updated and false if not.
+       </para>
+       </entry>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_attribute_stats</primary>
+        </indexterm>
+        <function>pg_set_attribute_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>
+         , <parameter>attname</parameter> <type>name</type>
+         , <parameter>inherited</parameter> <type>boolean</type>
+         , <parameter>version</parameter> <type>integer</type>
+         , <literal>VARIADIC</literal> <parameter>stats</parameter> <type>"any"</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Replaces the <structname>pg_statistic</structname> row for the
+        <structname>pg_attribute</structname> row specified by
+        <parameter>relation</parameter>, <parameter>attname</parameter>
+        and <parameter>inherited</parameter>. The <parameter>version</parameter>
+        should be set to the server version number of the server where
+        the variadic <parameter>stats</parameter> originated, as it may in the
+        future affect how the stats are imported. Values from the variadic
+        list form name-value pairs. Each parameter name corresponds to the
+        same-named attribute in
+        <link linkend="view-pg-stats"><structname>pg_stats</structname></link>:
+       </para>
+       <variablelist>
+        <varlistentry>
+         <term><literal>null_frac</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>avg_width</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>integer</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>n_distinct</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_vals</literal></term>
+         <listitem>
+          <para>
+          <literal>most_common_vals</literal>
+           Must be followed by a parameter of type <type>text</type>. If this
+           parameter is specified then <literal>most_common_freqs</literal>
+           must also be specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_freqs</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of <type>real[]</type>, and can only
+           be specified if <literal>most_common_vals</literal> is also specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>histogram_bounds</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>correlation</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_elems</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>. If this
+           parameter is specified then <literal>most_common_elem_freqs</literal>
+           must also be specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_elem_freqs</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real[]</type>, and can
+           only be specified if <literal>most_common_elems</literal> is also
+           specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>elem_count_histogram</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real[]</type>.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>range_length_histogram</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>. If this
+           parameter is specified then <literal>range_empty_frac</literal>
+           must also be specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>range_empty_frac</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>, and can
+           only be specified if <literal>range_length_histogram</literal>
+           is also specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>range_bounds_histogram</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>
+          </para>
+         </listitem>
+        </varlistentry>
+       </variablelist>
+       <para>
+        Any other parameter names given must also have a value pair, but will emit
+        a warning and thereafter be ignored.
+       </para>
+       <para>
+        The parameters will return true if stats were updated and false if not.
+       </para>
+       <para>
+        This function mimics the behavior of <command>ANALYZE</command> in its
+        effects on the values in <structname>pg_statistic</structname>, except
+        that the values are supplied as parameters rather than derived from
+        table sampling.
+       </para>
+       <para>
+        The purpose of this function is to apply statistics values in an
+        upgrade situation that are "good enough" for system operation until
+        they are replaced by the next <command>ANALYZE</command>, usually via
+        <command>autovacuum</command>.
+       </para>
+       <para>
+        The caller must either be the owner of the relation, or have superuser
+        privileges.
+       </para>
+       </entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
    <table id="functions-admin-dblocation">
     <title>Database Object Location Functions</title>
     <tgroup cols="1">
-- 
2.44.0

v18-0002-Enable-dumping-of-table-index-stats-in-pg_dump.patchtext/x-patch; charset=US-ASCII; name=v18-0002-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From a1a53561850a7d3e8b54dbbaf9a565340edb85f7 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH v18 2/2] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will generate a statement that
calls pg_set_relation_stats(), and it will generate a series of
statements that call pg_set_attribute_stats(), one per attribute. These
statements will restore the statistics of the current system onto the
destination system.

Statistics are not dumped when -schema-only is specified, except during
a binary upgrade.

As is the pattern with pg_dump options, this can be disabled with
--no-statistics.
---
 src/bin/pg_dump/pg_backup.h          |   2 +
 src/bin/pg_dump/pg_backup_archiver.c |   5 +
 src/bin/pg_dump/pg_dump.c            | 303 ++++++++++++++++++++++++++-
 src/bin/pg_dump/pg_dump.h            |   1 +
 src/bin/pg_dump/pg_dumpall.c         |   5 +
 src/bin/pg_dump/pg_restore.c         |   3 +
 doc/src/sgml/ref/pg_dump.sgml        |   9 +
 doc/src/sgml/ref/pg_restore.sgml     |  10 +
 8 files changed, 336 insertions(+), 2 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index fbf5f1c515..9f9b9f8724 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -112,6 +112,7 @@ typedef struct _restoreOptions
 	int			no_publications;	/* Skip publication entries */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
@@ -181,6 +182,7 @@ typedef struct _dumpOptions
 	int			no_security_labels;
 	int			no_publications;
 	int			no_subscriptions;
+	int			no_statistics;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index c7a6c918a6..8e43acf0d5 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2947,6 +2947,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's a stats dump, maybe ignore it */
+	if (ropt->no_statistics && strcmp(te->desc, "STATISTICS") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2976,6 +2980,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index c52e961b30..0d6008f556 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -435,6 +435,7 @@ main(int argc, char **argv)
 		{"no-comments", no_argument, &dopt.no_comments, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -1151,6 +1152,7 @@ help(const char *progname)
 	printf(_("  --no-comments                do not dump comments\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -7067,6 +7069,7 @@ getTables(Archive *fout, int *numTables)
 
 		/* Tables have data */
 		tblinfo[i].dobj.components |= DUMP_COMPONENT_DATA;
+		tblinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
 
 		/* Mark whether table has an ACL */
 		if (!PQgetisnull(res, i, i_relacl))
@@ -7564,6 +7567,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -10313,6 +10317,284 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * Tabular description of the parameters to pg_set_relation_stats()
+ * required, param_name, param_type
+ */
+static const char *rel_stats_arginfo[][3] = {
+	{"t", "relation", "regclass"},
+	{"t", "version", "integer"},
+	{"f", "relpages", "integer"},
+	{"f", "reltuples", "real"},
+	{"f", "relallvisible", "integer"},
+};
+
+/*
+ * Tabular description of the parameters to pg_set_attribute_stats()
+ * required, param_name, param_type
+ */
+static const char *att_stats_arginfo[][3] = {
+	{"t", "relation", "regclass"},
+	{"t", "attname", "name"},
+	{"t", "inherited", "boolean"},
+	{"t", "version", "integer"},
+	{"f", "null_frac", "float4"},
+	{"f", "avg_width", "integer"},
+	{"f", "n_distinct", "float4"},
+	{"f", "most_common_vals", "text"},
+	{"f", "most_common_freqs", "float4[]"},
+	{"f", "histogram_bounds", "text"},
+	{"f", "correlation", "float4"},
+	{"f", "most_common_elems", "text"},
+	{"f", "most_common_elem_freqs", "float4[]"},
+	{"f", "elem_count_histogram", "float4[]"},
+	{"f", "range_length_histogram", "text"},
+	{"f", "range_empty_frac", "float4"},
+	{"f", "range_bounds_histogram", "text"},
+};
+
+/*
+ * getRelStatsExportQuery --
+ *
+ * Generate a query that will fetch all relation (e.g. pg_class)
+ * stats for a given relation.
+ */
+static void
+getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "current_setting('server_version_num') AS version, "
+						 "c.relpages, c.reltuples, c.relallvisible "
+						 "FROM pg_class c "
+						 "JOIN pg_namespace n "
+						 "ON n.oid = c.relnamespace "
+						 "WHERE n.nspname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND c.relname = ");
+	appendStringLiteralAH(query, relname, fout);
+}
+
+/*
+ * getAttStatsExportQuery --
+ *
+ * Generate a query that will fetch all attribute (e.g. pg_statistic)
+ * stats for a given relation.
+ */
+static void
+getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "s.attname,"
+						 "s.inherited,"
+						 "current_setting('server_version_num') AS version, "
+						 "s.null_frac,"
+						 "s.avg_width,"
+						 "s.n_distinct,"
+						 "s.most_common_vals,"
+						 "s.most_common_freqs,"
+						 "s.histogram_bounds,"
+						 "s.correlation,"
+						 "s.most_common_elems,"
+						 "s.most_common_elem_freqs,"
+						 "s.elem_count_histogram,");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 "s.range_length_histogram,"
+							 "s.range_empty_frac,"
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(query,
+							 "NULL AS range_length_histogram,"
+							 "NULL AS range_empty_frac,"
+							 "NULL AS range_bounds_histogram ");
+
+	appendPQExpBufferStr(query,
+						 "FROM pg_stats s "
+						 "JOIN pg_namespace n "
+						 "ON n.nspname = s.schemaname "
+						 "JOIN pg_class c "
+						 "ON c.relname = s.tablename "
+						 "AND c.relnamespace = n.oid "
+						 "WHERE s.schemaname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND s.tablename = ");
+	appendStringLiteralAH(query, relname, fout);
+	appendPQExpBufferStr(query, " ORDER BY s.attname, s.inherited");
+}
+
+
+/*
+ * appendNamedArgument --
+ *
+ * Convenience routine for constructing parameters of the form:
+ * 'paraname', 'value'::type
+ */
+static void
+appendNamedArgument(PQExpBuffer out, Archive *fout,
+					const char *argname, bool positional,
+					const char *argval, const char *argtype)
+{
+	appendPQExpBufferStr(out, "\t");
+
+	if (!positional)
+	{
+		appendStringLiteralAH(out, argname, fout);
+		appendPQExpBufferStr(out, ", ");
+	}
+
+	appendStringLiteralAH(out, argval, fout);
+	appendPQExpBuffer(out, "::%s", argtype);
+}
+
+/*
+ * appendRelStatsImport --
+ *
+ * Append a formatted pg_set_relation_stats statement.
+ */
+static void
+appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	const char *sep = "";
+
+	if (PQntuples(res) == 0)
+		return;
+
+	appendPQExpBufferStr(out, "SELECT pg_catalog.pg_set_relation_stats(\n");
+
+	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
+	{
+		bool		positional = (rel_stats_arginfo[argno][0][0] == 't');
+		const char *argname = rel_stats_arginfo[argno][1];
+		const char *argtype = rel_stats_arginfo[argno][2];
+		int			fieldno = PQfnumber(res, argname);
+
+		if (fieldno < 0)
+			pg_fatal("relation stats export query missing field '%s'",
+					 argname);
+
+		if (PQgetisnull(res, 0, fieldno))
+		{
+			if (!positional)
+				pg_fatal("relation stats export query unexpected NULL in '%s'",
+						 argname);
+			else
+				continue;
+		}
+
+		appendPQExpBufferStr(out, sep);
+		appendNamedArgument(out, fout, argname, positional,
+							PQgetvalue(res, 0, fieldno), argtype);
+
+		sep = ",\n";
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+/*
+ * appendAttStatsImport --
+ *
+ * Append a series of formatted pg_set_attribute_stats statements.
+ */
+static void
+appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	{
+		const char *sep = "";
+
+		appendPQExpBufferStr(out, "SELECT pg_catalog.pg_set_attribute_stats(\n");
+		for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
+		{
+			bool		positional = (att_stats_arginfo[argno][0][0] == 't');
+			const char *argname = att_stats_arginfo[argno][1];
+			const char *argtype = att_stats_arginfo[argno][2];
+			int			fieldno = PQfnumber(res, argname);
+
+			if (fieldno < 0)
+				pg_fatal("attribute stats export query missing field '%s'",
+						 argname);
+
+			if (PQgetisnull(res, rownum, fieldno))
+			{
+				if (positional)
+					pg_fatal("attribute stats export query unexpected NULL in '%s'",
+							 argname);
+				else
+					continue;
+			}
+
+			appendPQExpBufferStr(out, sep);
+			appendNamedArgument(out, fout, argname, positional,
+								PQgetvalue(res, rownum, fieldno), argtype);
+			sep = ",\n";
+		}
+		appendPQExpBufferStr(out, "\n);\n");
+	}
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const DumpableObject *dobj,
+				  const char *reltypename, teSection dumpsection,
+				  DumpId dumpid)
+{
+	PGresult   *res;
+	PQExpBuffer query;
+	PQExpBuffer out;
+	PQExpBuffer tag;
+
+	/* do nothing, if --no-statistics is supplied */
+	if (fout->dopt->no_statistics)
+		return;
+
+	/* likewise, no stats on schema-only, unless in a binary upgrade */
+	if (fout->dopt->schemaOnly && !fout->dopt->binary_upgrade)
+		return;
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", reltypename, fmtId(dobj->name));
+
+	query = createPQExpBuffer();
+	out = createPQExpBuffer();
+
+	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendRelStatsImport(out, fout, res);
+	PQclear(res);
+
+	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendAttStatsImport(out, fout, res);
+	PQclear(res);
+
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATISTICS DATA",
+							  .section = dumpsection,
+							  .createStmt = out->data,
+							  .deps = &dumpid,
+							  .nDeps = 1));
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(out);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -16757,6 +17039,14 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 	if (tbinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
 		dumpTableSecLabel(fout, tbinfo, reltypename);
 
+	/* Statistics are dependent on the definition, not the data */
+	/* Views don't have stats */
+	if ((tbinfo->dobj.dump & DUMP_COMPONENT_STATISTICS) &&
+		(tbinfo->relkind != RELKIND_VIEW))
+		dumpRelationStats(fout, &tbinfo->dobj, reltypename,
+						  SECTION_DATA,
+						  tbinfo->dobj.dumpId);
+
 	/* Dump comments on inlined table constraints */
 	for (j = 0; j < tbinfo->ncheck; j++)
 	{
@@ -16958,6 +17248,7 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 	PQExpBuffer delq;
 	char	   *qindxname;
 	char	   *qqindxname;
+	DumpId		dumpid;
 
 	/* Do nothing in data-only dump */
 	if (dopt->dataOnly)
@@ -17070,14 +17361,22 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 		free(indstatvalsarray);
 	}
 
+	/* Comments and stats share same .dep */
+	dumpid = is_constraint ? indxinfo->indexconstraint :
+		indxinfo->dobj.dumpId;
+
 	/* Dump Index Comments */
 	if (indxinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
 		dumpComment(fout, "INDEX", qindxname,
 					tbinfo->dobj.namespace->dobj.name,
 					tbinfo->rolname,
 					indxinfo->dobj.catId, 0,
-					is_constraint ? indxinfo->indexconstraint :
-					indxinfo->dobj.dumpId);
+					dumpid);
+
+	/* Dump Index Stats */
+	if (indxinfo->dobj.dump & DUMP_COMPONENT_STATISTICS)
+		dumpRelationStats(fout, &indxinfo->dobj, "INDEX",
+						  SECTION_POST_DATA, dumpid);
 
 	destroyPQExpBuffer(q);
 	destroyPQExpBuffer(delq);
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 2a7c5873a0..c0e6cda481 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -101,6 +101,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 73337f3392..c4eaff3063 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -103,6 +103,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -172,6 +173,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -451,6 +453,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -666,6 +670,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 5ea78cf7cc..33ae4f92bc 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -75,6 +75,7 @@ main(int argc, char **argv)
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
 	static int	no_subscriptions = 0;
+	static int  no_statistics = 0;
 	static int	strict_names = 0;
 
 	struct option cmdopts[] = {
@@ -127,6 +128,7 @@ main(int argc, char **argv)
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
+		{"no-statistics", no_argument, &no_statistics, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -370,6 +372,7 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index b99793e414..d3ebdc7829 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -1081,6 +1081,15 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index 2e3ba80258..845f739a45 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -723,6 +723,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore statistics, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
-- 
2.44.0

#155Nathan Bossart
nathandbossart@gmail.com
In reply to: Corey Huinker (#154)
Re: Statistics Import and Export

On Thu, Apr 11, 2024 at 03:54:07PM -0400, Corey Huinker wrote:

At the request of a few people, attached is an attempt to move stats to
DATA/POST-DATA, and the TAP test failure that results from that.

The relevant errors are confusing, in that they all concern GRANT/REVOKE,
and the fact that I made no changes to the TAP test itself.

$ grep 'not ok' build/meson-logs/testlog.txt
not ok 9347 - section_data: should not dump GRANT INSERT(col1) ON TABLE
test_second_table

It looks like the problem is that the ACLs are getting dumped in the data
section when we are also dumping stats. I'm able to get the tests to pass
by moving the call to dumpRelationStats() that's in dumpTableSchema() to
dumpTableData(). I'm not entirely sure why that fixes it yet, but if we're
treating stats as data, then it intuitively makes sense for us to dump it
in dumpTableData(). However, that seems to prevent the stats from getting
exported in the --schema-only/--binary-upgrade scenario, which presents a
problem for pg_upgrade. ISTM we'll need some extra hacks to get this to
work as desired.

--
Nathan Bossart
Amazon Web Services: https://aws.amazon.com

#156Jeff Davis
pgsql@j-davis.com
In reply to: Nathan Bossart (#155)
Re: Statistics Import and Export

On Wed, 2024-04-17 at 11:50 -0500, Nathan Bossart wrote:

It looks like the problem is that the ACLs are getting dumped in the
data
section when we are also dumping stats.  I'm able to get the tests to
pass
by moving the call to dumpRelationStats() that's in dumpTableSchema()
to
dumpTableData().  I'm not entirely sure why that fixes it yet, but if
we're
treating stats as data, then it intuitively makes sense for us to
dump it
in dumpTableData().

Would it make sense to have a new SECTION_STATS?

However, that seems to prevent the stats from getting
exported in the --schema-only/--binary-upgrade scenario, which
presents a
problem for pg_upgrade.  ISTM we'll need some extra hacks to get this
to
work as desired.

Philosophically, I suppose stats are data, but I still don't understand
why considering stats to be data is so important in pg_dump.

Practically, I want to dump stats XOR data. That's because, if I dump
the data, it's so costly to reload and rebuild indexes that it's not
very important to avoid a re-ANALYZE.

Regards,
Jeff Davis

#157Tom Lane
tgl@sss.pgh.pa.us
In reply to: Jeff Davis (#156)
Re: Statistics Import and Export

Jeff Davis <pgsql@j-davis.com> writes:

Would it make sense to have a new SECTION_STATS?

Perhaps, but the implications for pg_dump's API would be nontrivial,
eg would we break any applications that know about the current
options for --section. And you still have to face up to the question
"does --data-only include this stuff?".

Philosophically, I suppose stats are data, but I still don't understand
why considering stats to be data is so important in pg_dump.
Practically, I want to dump stats XOR data. That's because, if I dump
the data, it's so costly to reload and rebuild indexes that it's not
very important to avoid a re-ANALYZE.

Hmm, interesting point. But the counterargument to that is that
the cost of building indexes will also dwarf the cost of installing
stats, so why not do so? Loading data without stats, and hoping
that auto-analyze will catch up sooner not later, is exactly the
current behavior that we're doing all this work to get out of.
I don't really think we want it to continue to be the default.

regards, tom lane

#158Jeff Davis
pgsql@j-davis.com
In reply to: Tom Lane (#157)
Re: Statistics Import and Export

On Mon, 2024-04-22 at 16:19 -0400, Tom Lane wrote:

Loading data without stats, and hoping
that auto-analyze will catch up sooner not later, is exactly the
current behavior that we're doing all this work to get out of.

That's the disconnect, I think. For me, the main reason I'm excited
about this work is as a way to solve the bad-plans-after-upgrade
problem and to repro planner issues outside of production. Avoiding the
need to ANALYZE at the end of a data load is also a nice convenience,
but not a primary driver (for me).

Should we just itemize some common use cases for pg_dump, and then
choose the defaults that are least likely to cause surprise?

As for the section, I'm not sure what to do about that. Based on this
thread it seems that SECTION_NONE (or a SECTION_STATS?) is easiest to
implement, but I don't understand the long-term consequences of that
choice.

Regards,
Jeff Davis

#159Tom Lane
tgl@sss.pgh.pa.us
In reply to: Jeff Davis (#158)
Re: Statistics Import and Export

Jeff Davis <pgsql@j-davis.com> writes:

On Mon, 2024-04-22 at 16:19 -0400, Tom Lane wrote:

Loading data without stats, and hoping
that auto-analyze will catch up sooner not later, is exactly the
current behavior that we're doing all this work to get out of.

That's the disconnect, I think. For me, the main reason I'm excited
about this work is as a way to solve the bad-plans-after-upgrade
problem and to repro planner issues outside of production. Avoiding the
need to ANALYZE at the end of a data load is also a nice convenience,
but not a primary driver (for me).

Oh, I don't doubt that there are use-cases for dumping stats without
data. I'm just dubious about the reverse. I think data+stats should
be the default, even if only because pg_dump's default has always
been to dump everything. Then there should be a way to get stats
only, and maybe a way to get data only. Maybe this does argue for a
four-section definition, despite the ensuing churn in the pg_dump API.

Should we just itemize some common use cases for pg_dump, and then
choose the defaults that are least likely to cause surprise?

Per above, I don't find any difficulty in deciding what should be the
default. What I think we need to consider is what the pg_dump and
pg_restore switch sets should be. There's certainly a few different
ways we could present that; maybe we should sketch out the details for
a couple of ways.

regards, tom lane

#160Matthias van de Meent
boekewurm+postgres@gmail.com
In reply to: Tom Lane (#159)
Re: Statistics Import and Export

On Tue, 23 Apr 2024, 05:52 Tom Lane, <tgl@sss.pgh.pa.us> wrote:

Jeff Davis <pgsql@j-davis.com> writes:

On Mon, 2024-04-22 at 16:19 -0400, Tom Lane wrote:

Loading data without stats, and hoping
that auto-analyze will catch up sooner not later, is exactly the
current behavior that we're doing all this work to get out of.

That's the disconnect, I think. For me, the main reason I'm excited
about this work is as a way to solve the bad-plans-after-upgrade
problem and to repro planner issues outside of production. Avoiding the
need to ANALYZE at the end of a data load is also a nice convenience,
but not a primary driver (for me).

Oh, I don't doubt that there are use-cases for dumping stats without
data. I'm just dubious about the reverse. I think data+stats should
be the default, even if only because pg_dump's default has always
been to dump everything. Then there should be a way to get stats
only, and maybe a way to get data only. Maybe this does argue for a
four-section definition, despite the ensuing churn in the pg_dump API.

I've heard of use cases where dumping stats without data would help
with production database planner debugging on a non-prod system.

Sure, some planner inputs would have to be taken into account too, but
having an exact copy of production stats is at least a start and can
help build models and alerts for what'll happen when the tables grow
larger with the current stats.

As for other planner inputs: table size is relatively easy to shim
with sparse files; cumulative statistics can be copied from a donor
replica if needed, and btree indexes only really really need to
contain their highest and lowest values (and need their height set
correctly).

Kind regards,

Matthias van de Meent

#161Corey Huinker
corey.huinker@gmail.com
In reply to: Matthias van de Meent (#160)
2 attachment(s)
Re: Statistics Import and Export

I've heard of use cases where dumping stats without data would help
with production database planner debugging on a non-prod system.

So far, I'm seeing these use cases:

1. Binary upgrade. (schema: on, data: off, stats: on)
2. Dump to file/dir and restore elsewhere. (schema: on, data: on, stats: on)
3. Dump stats for one or more objects, either to directly apply those stats
to a remote database, or to allow a developer to edit/experiment with those
stats. (schema: off, data: off, stats: on)
4. restore situations where stats are not wanted and/or not trusted
(whatever: on, stats: off)

Case #1 is handled via pg_upgrade and special case flags in pg_dump.
Case #2 uses the default pg_dump options, so that's covered.
Case #3 would require a --statistics-only option mutually exclusive with
--data-only and --schema-only. Alternatively, I could reanimate the script
pg_export_statistics, but we'd end up duplicating a lot of filtering
options that pg_dump already has solved. Similarly, we may want server-side
functions that generate the statements for us (pg_get_*_stats paired with
each pg_set_*_stats)
Case #4 is handled via --no-statistics.

Attached is v19, which attempts to put table stats in SECTION_DATA and
matview/index stats in SECTION_POST_DATA. It's still failing one TAP test
(004_pg_dump_parallel: parallel restore as inserts). I'm still unclear as
to why using SECTION_NONE is a bad idea, but I'm willing to go along with
DATA/POST_DATA, assuming we can make it work.

Attachments:

v19-0001-Create-pg_set_relation_stats-pg_set_attribute_st.patchtext/x-patch; charset=US-ASCII; name=v19-0001-Create-pg_set_relation_stats-pg_set_attribute_st.patchDownload
From fbe1de62659f4822b1861f002ef7f961ad36a210 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 21 Mar 2024 18:04:54 -0400
Subject: [PATCH v19 1/2] Create pg_set_relation_stats, pg_set_attribute_stats.

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

Both functions take an oid to identify the target relation that will
receive the statistics. There is nothing requiring that relation to be
the same one as the one exported, though the statistics supplied have
to make sense in the context of the new relation.

Both functions take a variadic list of statistic parameters name-value
pairs.

Each name given for pg_set_relation_stats must correspond to a
statistics attribute in pg_class, and the paired value must match the
datatype of said attribute in pg_class.

Each name given for pg_set_attribute_stats must correspond to a
statistics attribute in pg_stats, and the paird value must match the
datatype of said attribute in pg_stats, except for attributes of type
ANYARRAY, in which case a TEXT value is required.

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

These functions 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               |   22 +-
 src/include/statistics/statistics.h           |    2 +
 src/backend/statistics/Makefile               |    3 +-
 src/backend/statistics/meson.build            |    1 +
 src/backend/statistics/statistics.c           | 1271 +++++++++++++++++
 .../regress/expected/stats_export_import.out  |  853 +++++++++++
 src/test/regress/parallel_schedule            |    3 +-
 src/test/regress/sql/stats_export_import.sql  |  671 +++++++++
 doc/src/sgml/func.sgml                        |  267 ++++
 9 files changed, 3088 insertions(+), 5 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 134e3b22fd..26f62c3651 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12178,9 +12178,6 @@
   proallargtypes => '{int8,pg_lsn,pg_lsn,oid,oid,oid,int2,int8,bool}',
   proargmodes => '{i,i,i,o,o,o,o,o,o}',
   proargnames => '{tli,start_lsn,end_lsn,relfilenode,reltablespace,reldatabase,relforknumber,relblocknumber,is_limit_block}',
-  prosrc => 'pg_wal_summary_contents' },
-{ oid => '8438',
-  descr => 'WAL summarizer state',
   proname => 'pg_get_wal_summarizer_state',
   provolatile => 'v', proparallel => 's',
   prorettype => 'record', proargtypes => '',
@@ -12195,4 +12192,23 @@
   proargtypes => 'int2',
   prosrc => 'gist_stratnum_identity' },
 
+# Statistics Import
+{ oid => '8048',
+  descr => 'set statistics on relation',
+  proname => 'pg_set_relation_stats', provariadic => 'any',
+  proisstrict => 't', provolatile => 'v',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'regclass int4 any',
+  proargnames => '{relation,version,stats}',
+  proargmodes => '{i,i,v}',
+  prosrc => 'pg_set_relation_stats' },
+{ oid => '8049',
+  descr => 'set statistics on attribute',
+  proname => 'pg_set_attribute_stats', provariadic => 'any',
+  proisstrict => 't', provolatile => 'v',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'regclass name bool int4 any',
+  proargnames => '{relation,attname,inherited,version,stats}',
+  proargmodes => '{i,i,i,i,v}',
+  prosrc => 'pg_set_attribute_stats' },
 ]
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 7f2bf18716..1dddf96576 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -127,4 +127,6 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												int nclauses);
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
+extern Datum pg_set_relation_stats(PG_FUNCTION_ARGS);
+extern Datum pg_set_attribute_stats(PG_FUNCTION_ARGS);
 #endif							/* STATISTICS_H */
diff --git a/src/backend/statistics/Makefile b/src/backend/statistics/Makefile
index 89cf8c2797..e4f8ab7c4f 100644
--- a/src/backend/statistics/Makefile
+++ b/src/backend/statistics/Makefile
@@ -16,6 +16,7 @@ OBJS = \
 	dependencies.o \
 	extended_stats.o \
 	mcv.o \
-	mvdistinct.o
+	mvdistinct.o \
+	statistics.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/statistics/meson.build b/src/backend/statistics/meson.build
index 73b29a3d50..331e82c776 100644
--- a/src/backend/statistics/meson.build
+++ b/src/backend/statistics/meson.build
@@ -5,4 +5,5 @@ backend_sources += files(
   'extended_stats.c',
   'mcv.c',
   'mvdistinct.c',
+  'statistics.c',
 )
diff --git a/src/backend/statistics/statistics.c b/src/backend/statistics/statistics.c
new file mode 100644
index 0000000000..e092eb3dc3
--- /dev/null
+++ b/src/backend/statistics/statistics.c
@@ -0,0 +1,1271 @@
+/*-------------------------------------------------------------------------
+ * statistics.c
+ *
+ *	  POSTGRES statistics import
+ *
+ * Code supporting the direct importation of relation statistics, similar to
+ * what is done by the ANALYZE command.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *       src/backend/statistics/statistics.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_collation.h"
+#include "catalog/pg_database.h"
+#include "catalog/pg_operator.h"
+#include "catalog/pg_type.h"
+#include "fmgr.h"
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "nodes/nodeFuncs.h"
+#include "statistics/statistics.h"
+#include "utils/acl.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/float.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/rangetypes.h"
+#include "utils/syscache.h"
+#include "utils/typcache.h"
+
+/*
+ * A role has privileges to set statistics on the relation if any of the
+ * following are true:
+ *   - the role owns the current database and the relation is not shared
+ *   - the role has the MAINTAIN privilege on the relation
+ *
+ */
+static bool
+can_modify_relation(Relation rel)
+{
+	Oid			relid = RelationGetRelid(rel);
+	Form_pg_class reltuple = rel->rd_rel;
+
+	return ((object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId())
+			 && !reltuple->relisshared) ||
+			pg_class_aclcheck(relid, GetUserId(), ACL_MAINTAIN) == ACLCHECK_OK);
+}
+
+/*
+ * Set statistics for a given pg_class entry.
+ *
+ * This does an in-place (i.e. non-transactional) update of pg_class, just as
+ * is done in ANALYZE.
+ *
+ */
+Datum
+pg_set_relation_stats(PG_FUNCTION_ARGS)
+{
+	Oid			relid = PG_GETARG_OID(0);
+	int			version = PG_GETARG_INT32(1);
+
+	/* indexes of where we found required stats */
+	int			i_relpages = 0;
+	int			i_reltuples = 0;
+	int			i_relallvisible = 0;
+
+	/* build argument values to build the object */
+	Datum	   *args;
+	bool	   *nulls;			/* placeholder, because strict */
+	Oid		   *types;
+	int			nargs;
+
+	/* Minimum version supported */
+	if (version <= 90200)
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Cannot export statistics prior to version 9.2")));
+
+	nargs = extract_variadic_args(fcinfo, 2, true, &args, &types, &nulls);
+
+	/* if the pairs aren't pairs, something is malformed */
+	if (nargs % 2 == 1)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("stats parameters must be in name-value pairs")));
+		PG_RETURN_BOOL(false);
+	}
+
+	/* loop through args, matching params to their arg indexes */
+	for (int i = 0; i < nargs; i += 2)
+	{
+		char	   *statname;
+		int			argidx = i + 1;
+
+		if (types[i] != TEXTOID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("stat names must be of type text")));
+			continue;
+		}
+		statname = TextDatumGetCString(args[i]);
+
+		/*
+		 * match each named parameter to the index of the value that follows
+		 * it
+		 */
+		if (strcmp(statname, "relpages") == 0)
+			i_relpages = argidx;
+		else if (strcmp(statname, "reltuples") == 0)
+			i_reltuples = argidx;
+		else if (strcmp(statname, "relallvisible") == 0)
+			i_relallvisible = argidx;
+		else
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("unknown stat naame '%s', skipping", statname)));
+
+		pfree(statname);
+	}
+
+	/*
+	 * Ensure that we got all required parameters, and they are of the correct
+	 * type.
+	 */
+	if (i_relpages == 0)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "relpages")));
+		PG_RETURN_BOOL(false);
+	}
+	else if (types[i_relpages] != INT4OID)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relpages must be of type integer")));
+		PG_RETURN_BOOL(false);
+	}
+	else if (i_reltuples == 0)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "reltuples")));
+		PG_RETURN_BOOL(false);
+	}
+	else if (types[i_reltuples] != FLOAT4OID)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("reltuples must be of type real")));
+		PG_RETURN_BOOL(false);
+	}
+	else if (types[i_relallvisible] != INT4OID)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "relallvisible")));
+		PG_RETURN_BOOL(false);
+	}
+	else if (i_relallvisible == -1)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relallvisible must be of type integer")));
+		PG_RETURN_BOOL(false);
+	}
+	else
+	{
+		/*
+		 * Open the relation, getting ShareUpdateExclusiveLock to ensure that
+		 * no other stat-setting operation can run on it concurrently.
+		 */
+		Relation	rel;
+		HeapTuple	ctup;
+		Form_pg_class pgcform;
+		int			relpages;
+		float4		reltuples;
+		int			relallvisible;
+
+		rel = table_open(RelationRelationId, ShareUpdateExclusiveLock);
+
+		if (!can_modify_relation(rel))
+		{
+			table_close(rel, NoLock);
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("must be owner to modify relation stats")));
+			PG_RETURN_BOOL(false);
+		}
+
+		relpages = DatumGetInt32(args[i_relpages]);
+		reltuples = DatumGetFloat4(args[i_reltuples]);
+		relallvisible = DatumGetInt32(args[i_relallvisible]);
+
+		ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+		if (!HeapTupleIsValid(ctup))
+			ereport(ERROR,
+					(errcode(ERRCODE_OBJECT_IN_USE),
+					 errmsg("pg_class entry for relid %u not found", relid)));
+
+		pgcform = (Form_pg_class) GETSTRUCT(ctup);
+		/* Only update pg_class if there is a meaningful change */
+		if ((pgcform->reltuples != reltuples)
+			|| (pgcform->relpages != relpages)
+			|| (pgcform->relallvisible != relallvisible))
+		{
+			pgcform->relpages = relpages;
+			pgcform->reltuples = reltuples;
+			pgcform->relallvisible = relallvisible;
+
+			heap_inplace_update(rel, ctup);
+		}
+
+		table_close(rel, NoLock);
+		PG_RETURN_BOOL(true);
+	}
+
+	PG_RETURN_BOOL(false);
+}
+
+/*
+ * Test if the type is a scalar for MCELEM purposes
+ */
+static bool
+type_is_scalar(Oid typid)
+{
+	HeapTuple	tp;
+	bool		result = false;
+
+	tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+	if (HeapTupleIsValid(tp))
+	{
+		Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+
+		result = (!OidIsValid(typtup->typanalyze));
+		ReleaseSysCache(tp);
+	}
+	return result;
+}
+
+/*
+ * If this relation is an index and that index has expressions in it, and
+ * the attnum specified is known to be an expression, then we must walk
+ * the list attributes up to the specified attnum to get the right
+ * expression.
+ */
+static Node *
+get_attr_expr(Relation rel, int attnum)
+{
+	if ((rel->rd_rel->relkind == RELKIND_INDEX
+		 || (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+		&& (rel->rd_indexprs != NIL)
+		&& (rel->rd_index->indkey.values[attnum - 1] == 0))
+	{
+		ListCell   *indexpr_item = list_head(rel->rd_indexprs);
+
+		for (int i = 0; i < attnum - 1; i++)
+			if (rel->rd_index->indkey.values[i] == 0)
+				indexpr_item = lnext(rel->rd_indexprs, indexpr_item);
+
+		if (indexpr_item == NULL)	/* shouldn't happen */
+			elog(ERROR, "too few entries in indexprs list");
+
+		return (Node *) lfirst(indexpr_item);
+	}
+	return NULL;
+}
+
+/*
+ * Fetch datatype information, this is needed to derive the proper staopN
+ * and stacollN values.
+ *
+ */
+static TypeCacheEntry *
+get_attr_stat_type(Relation rel, Name attname,
+				   int16 *attnum, int32 *typmod, Oid *typcoll)
+{
+	Oid			relid = RelationGetRelid(rel);
+	Form_pg_attribute attr;
+	HeapTuple	atup;
+	Oid			typid;
+	Node	   *expr;
+
+	atup = SearchSysCache2(ATTNAME, ObjectIdGetDatum(relid),
+						   NameGetDatum(attname));
+
+	/* Attribute not found */
+	if (!HeapTupleIsValid(atup))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s has no attname %s",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+		return NULL;
+	}
+
+	attr = (Form_pg_attribute) GETSTRUCT(atup);
+	if (attr->attisdropped)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s attname %s is dropped",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+		return NULL;
+	}
+	*attnum = attr->attnum;
+
+	expr = get_attr_expr(rel, attr->attnum);
+
+	if (expr == NULL)
+	{
+		/* regular attribute */
+		typid = attr->atttypid;
+		*typmod = attr->atttypmod;
+		*typcoll = attr->attcollation;
+	}
+	else
+	{
+		typid = exprType(expr);
+		*typmod = exprTypmod(expr);
+
+		/*
+		 * If a collation has been specified for the index column, use that in
+		 * preference to anything else; but if not, fall back to whatever we
+		 * can get from the expression.
+		 */
+		if (OidIsValid(attr->attcollation))
+			*typcoll = attr->attcollation;
+		else
+			*typcoll = exprCollation(expr);
+	}
+	ReleaseSysCache(atup);
+
+	/* if it's a multirange, step down to the range type */
+	if (type_is_multirange(typid))
+		typid = get_multirange_range(typid);
+
+	return lookup_type_cache(typid,
+							 TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR);
+}
+
+/*
+ * Perform the cast of a known TextDatum into the type specified.
+ *
+ * If no errors are found, ok is set to true. Otherwise, set ok
+ * to false, capture the error found, and re-throw at warning level.
+ */
+static Datum
+cast_stavalues(FmgrInfo *flinfo, Datum d, Oid typid, int32 typmod, bool *ok)
+{
+	LOCAL_FCINFO(fcinfo, 8);
+	char	   *s;
+	Datum		result;
+	ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+	escontext.details_wanted = true;
+
+	s = TextDatumGetCString(d);
+
+	InitFunctionCallInfoData(*fcinfo, flinfo, 3, InvalidOid,
+							 (Node *) &escontext, NULL);
+
+	fcinfo->args[0].value = CStringGetDatum(s);
+	fcinfo->args[0].isnull = false;
+	fcinfo->args[1].value = ObjectIdGetDatum(typid);
+	fcinfo->args[1].isnull = false;
+	fcinfo->args[2].value = Int32GetDatum(typmod);
+	fcinfo->args[2].isnull = false;
+
+	result = FunctionCallInvoke(fcinfo);
+
+	if (SOFT_ERROR_OCCURRED(&escontext))
+	{
+		escontext.error_data->elevel = WARNING;
+		ThrowErrorData(escontext.error_data);
+		*ok = false;
+	}
+	else
+		*ok = true;
+
+	pfree(s);
+
+	return result;
+}
+
+
+/*
+ * Check array for any NULLs, and optionally for one-dimensionality.
+ *
+ * Report any failures as warnings.
+ */
+static bool
+array_check(Datum datum, int one_dim, const char *statname)
+{
+	ArrayType  *arr = DatumGetArrayTypeP(datum);
+	int16		elmlen;
+	char		elmalign;
+	bool		elembyval;
+	Datum	   *values;
+	bool	   *nulls;
+	int			nelems;
+
+	if (one_dim && (arr->ndim != 1))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be a multidimensional array", statname)));
+		return false;
+	}
+
+	get_typlenbyvalalign(ARR_ELEMTYPE(arr), &elmlen, &elembyval, &elmalign);
+
+	deconstruct_array(arr, ARR_ELEMTYPE(arr), elmlen, elembyval, elmalign,
+					  &values, &nulls, &nelems);
+
+	for (int i = 0; i < nelems; i++)
+		if (nulls[i])
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s array cannot contain NULL values", statname)));
+			return false;
+		}
+	return true;
+}
+
+/*
+ * Update the pg_statistic record.
+ */
+static void
+update_pg_statistic(Datum values[], bool nulls[])
+{
+	Relation	sd = table_open(StatisticRelationId, RowExclusiveLock);
+	CatalogIndexState indstate = CatalogOpenIndexes(sd);
+	HeapTuple	oldtup;
+	HeapTuple	stup;
+
+	/* Is there already a pg_statistic tuple for this attribute? */
+	oldtup = SearchSysCache3(STATRELATTINH,
+							 values[Anum_pg_statistic_starelid - 1],
+							 values[Anum_pg_statistic_staattnum - 1],
+							 values[Anum_pg_statistic_stainherit - 1]);
+
+	if (HeapTupleIsValid(oldtup))
+	{
+		/* Yes, replace it */
+		bool		replaces[Natts_pg_statistic];
+
+		for (int i = 0; i < Natts_pg_statistic; i++)
+			replaces[i] = true;
+
+		stup = heap_modify_tuple(oldtup, RelationGetDescr(sd),
+								 values, nulls, replaces);
+		ReleaseSysCache(oldtup);
+		CatalogTupleUpdateWithInfo(sd, &stup->t_self, stup, indstate);
+	}
+	else
+	{
+		/* No, insert new tuple */
+		stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
+		CatalogTupleInsertWithInfo(sd, stup, indstate);
+	}
+
+	heap_freetuple(stup);
+	CatalogCloseIndexes(indstate);
+	table_close(sd, NoLock);
+}
+
+/*
+ * Import statistics for a given relation attribute.
+ *
+ * This will insert/replace a row in pg_statistic for the given combinarion
+ * of relation, attribute, and inherited flag.
+ *
+ * The version parameter is for future use in the events as future versions
+ * may change them meaning of certain parameters.
+ *
+ * The variadic parameters all represent name-value pairs, with the names
+ * corresponding to attributes in pg_stats. Unkown names will generate a
+ * warning.
+ *
+ * Parameters null_frac, avg_width, and n_distinct are required because
+ * those attributes have no default value in pg_statistic.
+ *
+ * The remaining parameters all belong to a specific stakind, and all are
+ * optional. Some stakinds have multiple parameters, and in those cases
+ * both parameters must be specified if one of them is, otherwise a
+ * warning is generated but the rest of the stats may still be imported.
+ *
+ * If there is no attribute with a matching attname in the relation, the
+ * function will raise a warning and return false.
+ *
+ * Parameters corresponding to ANYARRAY columns are instead passed in as text
+ * values, which is a valid input string for an array of the type or element
+ * type of the attribute.
+ */
+Datum
+pg_set_attribute_stats(PG_FUNCTION_ARGS)
+{
+	Oid			relid = PG_GETARG_OID(0);
+	Name		attname = PG_GETARG_NAME(1);
+	bool		inherited = PG_GETARG_BOOL(2);
+	int			version = PG_GETARG_INT32(3);
+
+	/* build argument values to build the object */
+	Datum	   *args;
+	bool	   *argnulls;		/* placeholder, because strict */
+	Oid		   *types;
+	int			nargs = extract_variadic_args(fcinfo, 4, true,
+											  &args, &types, &argnulls);
+
+	Datum		values[Natts_pg_statistic] = {0};
+	bool		nulls[Natts_pg_statistic] = {false};
+
+	/*
+	 * argument indexes for each known statistic
+	 *
+	 * 0 = not found, -1 = error, n > 0 = found
+	 */
+
+	/* parameters that are required get indexes */
+	int			i_null_frac = 0;
+	int			i_avg_width = 0;
+	int			i_n_distinct = 0;
+
+	/* stakind stats are optional */
+	int			i_mc_vals = 0;
+	int			i_mc_freqs = 0;
+	int			i_hist_bounds = 0;
+	int			i_correlation = 0;
+	int			i_mc_elems = 0;
+	int			i_mc_elem_freqs = 0;
+	int			i_elem_count_hist = 0;
+	int			i_range_length_hist = 0;
+	int			i_range_empty_frac = 0;
+	int			i_range_bounds_hist = 0;
+
+	Relation	rel;
+
+	TypeCacheEntry *typcache;
+	TypeCacheEntry *elemtypcache = NULL;
+
+	int16		attnum;
+	int32		typmod;
+	Oid			typcoll;
+
+	FmgrInfo	finfo;
+
+	int			stakind_count;
+
+	int			k = 0;
+
+	if (version <= 90200)
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Cannot export statistics prior to version 9.2")));
+
+	/* if the pairs aren't pairs, something is malformed */
+	if (nargs % 2 == 1)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("stats parameters must be in name-value pairs")));
+		PG_RETURN_BOOL(false);
+	}
+
+	for (int i = 0; i < nargs; i += 2)
+	{
+		char	   *statname;
+		int			argidx = i + 1;
+
+		if (types[i] != TEXTOID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("stat names must be of type text")));
+			break;
+		}
+		statname = TextDatumGetCString(args[i]);
+
+		if (strcmp(statname, "null_frac") == 0)
+			i_null_frac = argidx;
+		else if (strcmp(statname, "avg_width") == 0)
+			i_avg_width = argidx;
+		else if (strcmp(statname, "n_distinct") == 0)
+			i_n_distinct = argidx;
+		else if (strcmp(statname, "most_common_vals") == 0)
+			i_mc_vals = argidx;
+		else if (strcmp(statname, "most_common_freqs") == 0)
+			i_mc_freqs = argidx;
+		else if (strcmp(statname, "histogram_bounds") == 0)
+			i_hist_bounds = argidx;
+		else if (strcmp(statname, "correlation") == 0)
+			i_correlation = argidx;
+		else if (strcmp(statname, "most_common_elems") == 0)
+			i_mc_elems = argidx;
+		else if (strcmp(statname, "most_common_elem_freqs") == 0)
+			i_mc_elem_freqs = argidx;
+		else if (strcmp(statname, "elem_count_histogram") == 0)
+			i_elem_count_hist = argidx;
+		else if (strcmp(statname, "range_length_histogram") == 0)
+			i_range_length_hist = argidx;
+		else if (strcmp(statname, "range_empty_frac") == 0)
+			i_range_empty_frac = argidx;
+		else if (strcmp(statname, "range_bounds_histogram") == 0)
+			i_range_bounds_hist = argidx;
+		else
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("unknown stat naame '%s', skipping", statname)));
+
+		pfree(statname);
+	}
+
+	/* check all required parameters */
+	if (i_null_frac > 0)
+	{
+		if (types[i_null_frac] != FLOAT4OID)
+		{
+			/* required param, not recoverable */
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be of type %s", "null_frac",
+							"real")));
+			PG_RETURN_BOOL(false);
+		}
+	}
+	else
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "null_frac")));
+		PG_RETURN_BOOL(false);
+	}
+
+	if (i_avg_width > 0)
+	{
+		if (types[i_avg_width] != INT4OID)
+		{
+			/* required param, not recoverable */
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be of type %s", "avg_width",
+							"integer")));
+			PG_RETURN_BOOL(false);
+		}
+	}
+	else
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "avg_width")));
+		PG_RETURN_BOOL(false);
+	}
+
+	if (i_n_distinct > 0)
+	{
+		if (types[i_n_distinct] != FLOAT4OID)
+		{
+			/* required param, not recoverable */
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be of type %s", "n_distinct",
+							"real")));
+			PG_RETURN_BOOL(false);
+		}
+	}
+	else
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "n_distinct")));
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * Look for pair mismatches, if found warn and disable.
+	 */
+	if ((i_mc_vals == 0) != (i_mc_freqs == 0))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be present if %s is missing",
+						(i_mc_vals == 0) ? "most_common_freqs" :
+						"most_common_vals",
+						(i_mc_vals == 0) ? "most_common_vals" :
+						"most_common_freqs")));
+		i_mc_vals = -1;
+		i_mc_freqs = -1;
+	}
+
+	if ((i_mc_elems == 0) != (i_mc_elem_freqs == 0))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be present if %s is missing",
+						(i_mc_elems == 0) ?
+						"most_common_elem_freqs" :
+						"most_common_elems",
+						(i_mc_elems == 0) ?
+						"most_common_elems" :
+						"most_common_elem_freqs")));
+		i_mc_elems = -1;
+		i_mc_elem_freqs = -1;
+	}
+
+	if ((i_range_length_hist == 0) != (i_range_empty_frac == 0))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be present if %s is missing",
+						(i_range_length_hist == 0) ?
+						"range_empty_frac" :
+						"range_length_histogram",
+						(i_range_length_hist == 0) ?
+						"range_length_histogram" :
+						"range_empty_frac")));
+		i_range_length_hist = -1;
+		i_range_empty_frac = -1;
+	}
+
+	rel = relation_open(relid, ShareUpdateExclusiveLock);
+
+	if (!can_modify_relation(rel))
+	{
+		relation_close(rel, NoLock);
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						RelationGetRelationName(rel))));
+		PG_RETURN_BOOL(false);
+	}
+
+	if ((!inherited) &&
+		((rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) ||
+		 (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX)))
+	{
+		relation_close(rel, NoLock);
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("%s is partitioned, can only accepted inherted stats",
+						RelationGetRelationName(rel))));
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * Many of the values that are set for a particular stakind are entirely
+	 * derived from the attribute itself, or it's expression.
+	 */
+	typcache = get_attr_stat_type(rel, attname, &attnum, &typmod, &typcoll);
+	if (typcache == NULL)
+	{
+		relation_close(rel, NoLock);
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * Derive element type if we have stat kinds that need it.
+	 *
+	 * This duplicates some type-specific logic found in various typanalyze
+	 * functions which are called from vacuum's examine_attribute().
+	 */
+	if ((i_mc_elems > 0) || (i_elem_count_hist > 0))
+	{
+		Oid			elemtypid;
+
+		if (typcache->type_id == TSVECTOROID)
+		{
+			/*
+			 * tsvector elems always have a text oid type and default
+			 * collation
+			 */
+			elemtypid = TEXTOID;
+			typcoll = DEFAULT_COLLATION_OID;
+		}
+		else if (typcache->typtype == TYPTYPE_RANGE)
+			elemtypid = get_range_subtype(typcache->type_id);
+		else
+			elemtypid = get_base_element_type(typcache->type_id);
+
+		/* not finding a basetype means we already had it */
+		if (elemtypid == InvalidOid)
+			elemtypid = typcache->type_id;
+
+		/* The stats need the eq_opr, any validation would need the lt_opr */
+		elemtypcache = lookup_type_cache(elemtypid, TYPECACHE_EQ_OPR);
+
+		if (elemtypcache == NULL)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							(i_mc_elems > 0) ?
+							"most_common_elems" :
+							"elem_count_histogram")));
+			i_mc_elems = -1;
+			i_elem_count_hist = -1;
+		}
+	}
+
+	/*
+	 * histogram_bounds and correlation must have a type < operator. WARN and
+	 * skip if this attribute doesn't have one.
+	 */
+	if (typcache->lt_opr == InvalidOid)
+	{
+		if (i_hist_bounds > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"histogram_bounds")));
+			i_hist_bounds = -1;
+		}
+		if (i_correlation > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"correlation")));
+			i_correlation = -1;
+		}
+	}
+
+	/*
+	 * Scalar types can't have most_common_elems, most_common_elem_freqs,
+	 * elem_count_histogram. WARN and skip.
+	 */
+	if (type_is_scalar(typcache->type_id))
+	{
+		if (i_mc_elems > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"most_common_elems")));
+			i_mc_elems = -1;
+		}
+		if (i_mc_elem_freqs > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"most_common_elem_freqs")));
+			i_mc_elem_freqs = -1;
+		}
+		if (i_elem_count_hist > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"elem_count_histogram")));
+			i_elem_count_hist = -1;
+		}
+	}
+
+	/*
+	 * Only range types can have range_length_histogram, range_empty_frac, and
+	 * range_bounds_histogram. WARN and skip
+	 */
+	if ((typcache->typtype != TYPTYPE_MULTIRANGE) &&
+		(typcache->typtype != TYPTYPE_RANGE))
+	{
+		if (i_range_length_hist > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"range_length_histogram")));
+			i_range_length_hist = -1;
+		}
+		if (i_range_empty_frac > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"range_empty_frac")));
+			i_range_empty_frac = -1;
+		}
+		if (i_range_bounds_hist > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"range_bounds_histogram")));
+			i_range_bounds_hist = -1;
+		}
+	}
+
+	/*
+	 * count the number of stakinds we still want to set, paired params count
+	 * as one. The count cannot exceed STATISTIC_NUM_SLOTS.
+	 */
+	stakind_count = (int) (i_mc_vals > 0) +
+		(int) (i_mc_elems > 0) +
+		(int) (i_range_length_hist > 0) +
+		(int) (i_hist_bounds > 0) +
+		(int) (i_correlation > 0) +
+		(int) (i_elem_count_hist > 0) +
+		(int) (i_range_bounds_hist > 0);
+
+	if (stakind_count > STATISTIC_NUM_SLOTS)
+	{
+		/*
+		 * This really shouldn't happen, as most datatypes exclude at least
+		 * one of these types of stats.
+		 */
+		relation_close(rel, NoLock);
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("imported statistics must have a maximum of %d slots "
+						"but %d given",
+						STATISTIC_NUM_SLOTS, stakind_count)));
+		PG_RETURN_BOOL(false);
+	}
+
+	fmgr_info(F_ARRAY_IN, &finfo);
+
+	values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(relid);
+	values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(attnum);
+	values[Anum_pg_statistic_stainherit - 1] = BoolGetDatum(inherited);
+	values[Anum_pg_statistic_stanullfrac - 1] = args[i_null_frac];
+	values[Anum_pg_statistic_stawidth - 1] = args[i_avg_width];
+	values[Anum_pg_statistic_stadistinct - 1] = args[i_n_distinct];
+
+	/*
+	 * STATISTIC_KIND_MCV
+	 *
+	 * most_common_freqs: real[]
+	 *
+	 * most_common_vals : ANYARRAY::text
+	 */
+	if (i_mc_vals > 0)
+	{
+		Oid			numberstype = types[i_mc_freqs];
+		Oid			valuestype = types[i_mc_vals];
+		Datum		stanumbers = args[i_mc_freqs];
+		Datum		strvalue = args[i_mc_vals];
+
+		if (get_element_type(numberstype) != FLOAT4OID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"most_common_freqs", "real[]")));
+		}
+		else if (valuestype != TEXTOID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"most_common_vals", "text")));
+		}
+		else
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_MCV);
+			Datum		staop = ObjectIdGetDatum(typcache->eq_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		stavalues;
+			bool		converted = false;
+
+			stavalues = cast_stavalues(&finfo, strvalue, typcache->type_id,
+									   typmod, &converted);
+
+			if (converted &&
+				array_check(stavalues, false, "most_common_vals") &&
+				array_check(stanumbers, true, "most_common_freqs"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_HISTOGRAM
+	 *
+	 * histogram_bounds: ANYARRAY::text
+	 */
+	if (i_hist_bounds > 0)
+	{
+		Oid			valuestype = types[i_hist_bounds];
+		Datum		strvalue = args[i_hist_bounds];
+
+		if (valuestype != TEXTOID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"histogram_bounds", "text")));
+		else
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_HISTOGRAM);
+			Datum		staop = ObjectIdGetDatum(typcache->lt_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		stavalues;
+			bool		converted = false;
+
+			stavalues = cast_stavalues(&finfo, strvalue, typcache->type_id,
+									   typmod, &converted);
+
+			if (converted && array_check(stavalues, false, "histogram_bounds"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_CORRELATION
+	 *
+	 * correlation: real
+	 */
+	if (i_correlation > 0)
+	{
+		if (types[i_correlation] != FLOAT4OID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be of type %s, ignored",
+							"correlation", "real")));
+		else
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_CORRELATION);
+			Datum		staop = ObjectIdGetDatum(typcache->lt_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		elems[] = {args[i_correlation]};
+			ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+			Datum		stanumbers = PointerGetDatum(arry);
+
+			values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+			values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+			values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+			values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+			nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+			k++;
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_MCELEM
+	 *
+	 * most_common_elem_freqs: real[]
+	 *
+	 * most_common_elems     : ANYARRAY::text
+	 */
+	if (i_mc_elems > 0)
+	{
+		Oid			numberstype = types[i_mc_elem_freqs];
+		Oid			valuestype = types[i_mc_elems];
+		Datum		stanumbers = args[i_mc_elem_freqs];
+
+		if (get_element_type(numberstype) != FLOAT4OID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"most_common_elem_freqs", "real[]")));
+		else if (valuestype != TEXTOID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"most_common_elems", "text")));
+		else
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_MCELEM);
+			Datum		staop = ObjectIdGetDatum(elemtypcache->eq_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		strvalue = args[i_mc_elems];
+			bool		converted = false;
+			Datum		stavalues;
+
+			stavalues = cast_stavalues(&finfo, strvalue, elemtypcache->type_id,
+									   typmod, &converted);
+
+			if (converted &&
+				array_check(stavalues, false, "most_common_elems") &&
+				array_check(stanumbers, true, "most_common_elem_freqs"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_DECHIST
+	 *
+	 * elem_count_histogram:	real[]
+	 */
+	if (i_elem_count_hist > 0)
+	{
+		Oid			numberstype = types[i_elem_count_hist];
+
+		if (get_element_type(numberstype) != FLOAT4OID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"elem_count_histogram", "real[]")));
+		else
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_DECHIST);
+			Datum		staop = ObjectIdGetDatum(elemtypcache->eq_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		stanumbers = args[i_elem_count_hist];
+
+			if (array_check(stanumbers, true, "elem_count_histogram"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+				nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+				k++;
+			}
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_BOUNDS_HISTOGRAM
+	 *
+	 * range_bounds_histogram: ANYARRAY::text
+	 *
+	 * This stakind appears before STATISTIC_KIND_BOUNDS_HISTOGRAM even though
+	 * it is numerically greater, and all other stakinds appear in numerical
+	 * order. We duplicate this quirk to make before/after tests of
+	 * pg_statistic records easier.
+	 */
+	if (i_range_bounds_hist > 0)
+	{
+		Oid			valuestype = types[i_range_bounds_hist];
+
+		if (valuestype != TEXTOID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"range_bounds_histogram", "text")));
+		else
+		{
+
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_BOUNDS_HISTOGRAM);
+			Datum		staop = ObjectIdGetDatum(InvalidOid);
+			Datum		stacoll = ObjectIdGetDatum(InvalidOid);
+			Datum		strvalue = args[i_range_bounds_hist];
+			bool		converted = false;
+			Datum		stavalues;
+
+			stavalues = cast_stavalues(&finfo, strvalue, typcache->type_id,
+									   typmod, &converted);
+
+			if (converted &&
+				array_check(stavalues, false, "range_bounds_histogram"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM
+	 *
+	 * range_empty_frac: real
+	 *
+	 * range_length_histogram:  double precision[]::text
+	 */
+	if (i_range_length_hist > 0)
+	{
+		Oid			numberstype = types[i_range_empty_frac];
+		Oid			valuestype = types[i_range_length_hist];
+
+		if (numberstype != FLOAT4OID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"range_empty_frac", "real")));
+		else if (valuestype != TEXTOID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"range_length_histogram", "text")));
+		else
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM);
+			Datum		staop = ObjectIdGetDatum(Float8LessOperator);
+			Datum		stacoll = ObjectIdGetDatum(InvalidOid);
+
+			/* The anyarray is always a float8[] for this stakind */
+			Datum		elem = args[i_range_empty_frac];
+			Datum		elems[] = {elem};
+			ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+			Datum		stanumbers = PointerGetDatum(arry);
+			Datum		strvalue = args[i_range_length_hist];
+			bool		converted = false;
+			Datum		stavalues;
+
+			stavalues = cast_stavalues(&finfo, strvalue, FLOAT8OID, 0, &converted);
+
+			if (converted &&
+				array_check(stavalues, false, "range_length_histogram"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+	}
+
+	/* fill in all remaining slots */
+	for (; k < STATISTIC_NUM_SLOTS; k++)
+	{
+		values[Anum_pg_statistic_stakind1 - 1 + k] = Int16GetDatum(0);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+	}
+
+	update_pg_statistic(values, nulls);
+
+	relation_close(rel, NoLock);
+	PG_RETURN_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..684df93993
--- /dev/null
+++ b/src/test/regress/expected/stats_export_import.out
@@ -0,0 +1,853 @@
+CREATE SCHEMA stats_export_import;
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+        0 |        -1 |             0
+(1 row)
+
+-- error: regclass not found
+SELECT pg_set_relation_stats('stats_export_import.nope'::regclass,
+                             150000::integer);
+ERROR:  relation "stats_export_import.nope" does not exist
+LINE 1: SELECT pg_set_relation_stats('stats_export_import.nope'::reg...
+                                     ^
+-- error: all three params missing
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer);
+ERROR:  function pg_set_relation_stats(regclass, integer) does not exist
+LINE 1: SELECT pg_set_relation_stats('stats_export_import.test'::reg...
+               ^
+HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+-- error: reltuples, relallvisible missing
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 4::integer);
+WARNING:  required parameter reltuples not set
+ pg_set_relation_stats 
+-----------------------
+ f
+(1 row)
+
+-- error: null value
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', NULL::real,
+                             'relallvisible', 4::integer);
+ pg_set_relation_stats 
+-----------------------
+ 
+(1 row)
+
+-- error: bad relpages type
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+WARNING:  relpages must be of type integer
+ pg_set_relation_stats 
+-----------------------
+ f
+(1 row)
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 17::integer,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+ pg_set_relation_stats 
+-----------------------
+ t
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       17 |       400 |             4
+(1 row)
+
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    '0'::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ERROR:  could not open relation with OID 0
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    NULL::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    NULL::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    NULL::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', NULL::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', NULL::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', NULL::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT stanullfrac, stawidth, stadistinct
+FROM pg_statistic
+WHERE starelid = 'stats_export_import.test'::regclass;
+ stanullfrac | stawidth | stadistinct 
+-------------+----------+-------------
+         0.1 |        2 |         0.3
+(1 row)
+
+-- warn: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_freqs', '{0.1,0.2,0.3}'::real[]
+    );
+WARNING:  most_common_freqs cannot be present if most_common_vals is missing
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{1,2,3}'::text
+    );
+WARNING:  most_common_vals cannot be present if most_common_freqs is missing
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.2,0.1}'::double precision[]
+    );
+WARNING:  most_common_freqs must be type real[], ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warning: mcv cast failure
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,four,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+WARNING:  invalid input syntax for type integer: "four"
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: histogram elements null value
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,NULL,3,4}'::text
+    );
+WARNING:  histogram_bounds array cannot contain NULL values
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,2,3,4}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'correlation', 0.5::real);
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |         0.5 |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{1,3}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+WARNING:  id cannot accept most_common_elems stats, ignored
+WARNING:  id cannot accept most_common_elem_freqs stats, ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,two}'::text
+    );
+WARNING:  most_common_elems cannot be present if most_common_elem_freqs is missing
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
+    );
+WARNING:  most_common_elem_freqs cannot be present if most_common_elems is missing
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             | {one,three}       | {0.3,0.2,0.2,0.3,0}    |                      |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+WARNING:  id cannot accept elem_count_histogram stats, ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+WARNING:  elem_count_histogram array cannot contain NULL values
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+WARNING:  id cannot accept range_length_histogram stats, ignored
+WARNING:  id cannot accept range_empty_frac stats, ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+WARNING:  range_length_histogram cannot be present if range_empty_frac is missing
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real
+    );
+WARNING:  range_empty_frac cannot be present if range_length_histogram is missing
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
+(1 row)
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+WARNING:  id cannot accept range_bounds_histogram stats, ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
+ stats_export_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      |                        |                  | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+(1 row)
+
+-- warn: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
+    'histogram_bounds', '{1,2,3,4}'::text,
+    'correlation', 1.1::real,
+    'most_common_elems', '{3,1}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    'range_empty_frac', -0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+WARNING:  imported statistics must have a maximum of 5 slots but 6 given
+ pg_set_attribute_stats 
+------------------------
+ f
+(1 row)
+
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+   relname    | num_stats 
+--------------+-----------
+ is_odd       |         1
+ is_odd_clone |         1
+ test         |         5
+ test_clone   |         5
+(4 rows)
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 675c567617..bcd7c0720e 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,8 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc stats_export_import
+
 
 # ----------
 # Load huge amounts of data
diff --git a/src/test/regress/sql/stats_export_import.sql b/src/test/regress/sql/stats_export_import.sql
new file mode 100644
index 0000000000..ebdb58aba1
--- /dev/null
+++ b/src/test/regress/sql/stats_export_import.sql
@@ -0,0 +1,671 @@
+CREATE SCHEMA stats_export_import;
+
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+-- error: regclass not found
+SELECT pg_set_relation_stats('stats_export_import.nope'::regclass,
+                             150000::integer);
+
+-- error: all three params missing
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer);
+
+-- error: reltuples, relallvisible missing
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 4::integer);
+
+-- error: null value
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', NULL::real,
+                             'relallvisible', 4::integer);
+
+-- error: bad relpages type
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 17::integer,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_export_import.test'::regclass;
+
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    '0'::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    NULL::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    NULL::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    NULL::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', NULL::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', NULL::integer,
+    'n_distinct', 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', NULL::real);
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+SELECT stanullfrac, stawidth, stadistinct
+FROM pg_statistic
+WHERE starelid = 'stats_export_import.test'::regclass;
+
+-- warn: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_freqs', '{0.1,0.2,0.3}'::real[]
+    );
+
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{1,2,3}'::text
+    );
+
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.2,0.1}'::double precision[]
+    );
+
+-- warning: mcv cast failure
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,four,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: histogram elements null value
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,NULL,3,4}'::text
+    );
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,2,3,4}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'correlation', 0.5::real);
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: scalars can't have mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{1,3}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,two}'::text
+    );
+
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
+    );
+
+-- ok: mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- warn: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real
+    );
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
+    'histogram_bounds', '{1,2,3,4}'::text,
+    'correlation', 1.1::real,
+    'most_common_elems', '{3,1}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    'range_empty_frac', -0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+
+SELECT
+    format('SELECT pg_catalog.pg_set_attribute_stats( '
+            || '%L::regclass::oid, '
+            || '%L::name, '
+            || '%L::boolean, '
+            || '%L::integer, '
+            || '%L, %L::real, '
+            || '%L, %L::integer, '
+            || '%L, %L::real %s)',
+        'stats_export_import.' || s.tablename || '_clone',
+        s.attname,
+        s.inherited,
+        150000,
+        'null_frac', s.null_frac,
+        'avg_width', s.avg_width,
+        'n_distinct', s.n_distinct,
+        CASE
+            WHEN s.most_common_vals IS NULL THEN ''
+            ELSE format(', %L, %L::text, %L, %L::real[]',
+                        'most_common_vals', s.most_common_vals,
+                        'most_common_freqs', s.most_common_freqs)
+        END ||
+        CASE
+            WHEN s.histogram_bounds IS NULL THEN ''
+            ELSE format(', %L, %L::text',
+                        'histogram_bounds', s.histogram_bounds)
+        END ||
+        CASE
+            WHEN s.correlation IS NULL THEN ''
+            ELSE format(', %L, %L::real',
+                        'correlation', s.correlation)
+        END ||
+        CASE
+            WHEN s.most_common_elems IS NULL THEN ''
+            ELSE format(', %L, %L::text, %L, %L::real[]',
+                        'most_common_elems', s.most_common_elems,
+                        'most_common_elem_freqs', s.most_common_elem_freqs)
+        END ||
+        CASE
+            WHEN s.elem_count_histogram IS NULL THEN ''
+            ELSE format(', %L, %L::real[]',
+                        'elem_count_histogram', s.elem_count_histogram)
+        END ||
+        CASE
+            WHEN s.range_bounds_histogram IS NULL THEN ''
+            ELSE format(', %L, %L::text',
+                        'range_bounds_histogram', s.range_bounds_histogram)
+        END ||
+        CASE
+            WHEN s.range_empty_frac IS NULL THEN ''
+            ELSE format(', %L, %L::real, %L, %L::text',
+                        'range_empty_frac', s.range_empty_frac,
+                        'range_length_histogram', s.range_length_histogram)
+        END)
+FROM pg_catalog.pg_stats AS s
+WHERE s.schemaname = 'stats_export_import'
+AND s.tablename IN ('test', 'is_odd')
+\gexec
+
+-- restore ECHO to original value
+\set ECHO :orig_ECHO
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 92a0f49e6a..8f1c535286 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29634,6 +29634,273 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
     in identifying the specific disk files associated with database objects.
    </para>
 
+   <table id="functions-admin-statsimport">
+    <title>Database Object Statistics Import Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_relation_stats</primary>
+        </indexterm>
+        <function>pg_set_relation_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>version</parameter> <type>integer</type>,
+         <literal>VARIADIC</literal> <parameter>stats</parameter> <type>"any"</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Updates the <structname>pg_class</structname> row for the specified
+        <parameter>relation</parameter>, values from the variadic list of
+        name-value pairs. Each parameter name corresponds to the same-named
+        attribute in <structname>pg_class</structname>:
+       </para>
+       <variablelist>
+        <varlistentry>
+         <term><literal>relpages</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>integer</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>reltuples</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>relalltuples</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>integer</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+       </variablelist>
+       <para>
+        Any other parameter names given must also have a value pair, but will emit
+        a warning and thereafter be ignored.
+       </para>
+       <para>
+        The <parameter>version</parameter> is meant to reflect the server version
+        number of the system where this data was generated, as that may in the
+        future change how the data is imported.
+       </para>
+       <para>
+        To avoid table bloat in <structname>pg_class</structname>, this change
+        is made with an in-place update, and therefore cannot be rolled back
+        through normal transaction processing.
+       </para>
+       <para>
+        This function mimics the behavior of <command>ANALYZE</command> in its
+        effects on the values in <structname>pg_class</structname>, except that
+        the values are supplied as parameters rather than derived from table
+        sampling.
+       </para>
+       <para>
+        The caller must either be the owner of the relation, or have superuser
+        privileges.
+       </para>
+       <para>
+        The parameters given are checked for type validity and must all be
+        NOT NULL.
+       </para>
+       <para>
+        The parameters will return true if stats were updated and false if not.
+       </para>
+       </entry>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_attribute_stats</primary>
+        </indexterm>
+        <function>pg_set_attribute_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>
+         , <parameter>attname</parameter> <type>name</type>
+         , <parameter>inherited</parameter> <type>boolean</type>
+         , <parameter>version</parameter> <type>integer</type>
+         , <literal>VARIADIC</literal> <parameter>stats</parameter> <type>"any"</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Replaces the <structname>pg_statistic</structname> row for the
+        <structname>pg_attribute</structname> row specified by
+        <parameter>relation</parameter>, <parameter>attname</parameter>
+        and <parameter>inherited</parameter>. The <parameter>version</parameter>
+        should be set to the server version number of the server where
+        the variadic <parameter>stats</parameter> originated, as it may in the
+        future affect how the stats are imported. Values from the variadic
+        list form name-value pairs. Each parameter name corresponds to the
+        same-named attribute in
+        <link linkend="view-pg-stats"><structname>pg_stats</structname></link>:
+       </para>
+       <variablelist>
+        <varlistentry>
+         <term><literal>null_frac</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>avg_width</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>integer</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>n_distinct</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_vals</literal></term>
+         <listitem>
+          <para>
+          <literal>most_common_vals</literal>
+           Must be followed by a parameter of type <type>text</type>. If this
+           parameter is specified then <literal>most_common_freqs</literal>
+           must also be specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_freqs</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of <type>real[]</type>, and can only
+           be specified if <literal>most_common_vals</literal> is also specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>histogram_bounds</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>correlation</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_elems</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>. If this
+           parameter is specified then <literal>most_common_elem_freqs</literal>
+           must also be specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_elem_freqs</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real[]</type>, and can
+           only be specified if <literal>most_common_elems</literal> is also
+           specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>elem_count_histogram</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real[]</type>.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>range_length_histogram</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>. If this
+           parameter is specified then <literal>range_empty_frac</literal>
+           must also be specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>range_empty_frac</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>, and can
+           only be specified if <literal>range_length_histogram</literal>
+           is also specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>range_bounds_histogram</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>
+          </para>
+         </listitem>
+        </varlistentry>
+       </variablelist>
+       <para>
+        Any other parameter names given must also have a value pair, but will emit
+        a warning and thereafter be ignored.
+       </para>
+       <para>
+        The parameters will return true if stats were updated and false if not.
+       </para>
+       <para>
+        This function mimics the behavior of <command>ANALYZE</command> in its
+        effects on the values in <structname>pg_statistic</structname>, except
+        that the values are supplied as parameters rather than derived from
+        table sampling.
+       </para>
+       <para>
+        The purpose of this function is to apply statistics values in an
+        upgrade situation that are "good enough" for system operation until
+        they are replaced by the next <command>ANALYZE</command>, usually via
+        <command>autovacuum</command>.
+       </para>
+       <para>
+        The caller must either be the owner of the relation, or have superuser
+        privileges.
+       </para>
+       </entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
    <table id="functions-admin-dblocation">
     <title>Database Object Location Functions</title>
     <tgroup cols="1">
-- 
2.44.0

v19-0002-Enable-dumping-of-table-index-stats-in-pg_dump.patchtext/x-patch; charset=US-ASCII; name=v19-0002-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From b645c3645ba88fd10f1d1e2f25ff8a74939d8c50 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH v19 2/2] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will generate a statement that
calls pg_set_relation_stats(), and it will generate a series of
statements that call pg_set_attribute_stats(), one per attribute. These
statements will restore the statistics of the current system onto the
destination system.

Statistics are not dumped when -schema-only is specified, except during
a binary upgrade.

As is the pattern with pg_dump options, this can be disabled with
--no-statistics.
---
 src/include/catalog/pg_proc.dat      |   3 +
 src/bin/pg_dump/pg_backup.h          |   2 +
 src/bin/pg_dump/pg_backup_archiver.c |   5 +
 src/bin/pg_dump/pg_dump.c            | 373 ++++++++++++++++++++++++++-
 src/bin/pg_dump/pg_dump.h            |   8 +
 src/bin/pg_dump/pg_dump_sort.c       |   5 +
 src/bin/pg_dump/pg_dumpall.c         |   5 +
 src/bin/pg_dump/pg_restore.c         |   3 +
 doc/src/sgml/ref/pg_dump.sgml        |   9 +
 doc/src/sgml/ref/pg_restore.sgml     |  10 +
 10 files changed, 421 insertions(+), 2 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 26f62c3651..4e71dc6227 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12178,6 +12178,9 @@
   proallargtypes => '{int8,pg_lsn,pg_lsn,oid,oid,oid,int2,int8,bool}',
   proargmodes => '{i,i,i,o,o,o,o,o,o}',
   proargnames => '{tli,start_lsn,end_lsn,relfilenode,reltablespace,reldatabase,relforknumber,relblocknumber,is_limit_block}',
+  prosrc => 'pg_wal_summary_contents' },
+{ oid => '8438',
+  descr => 'WAL summarizer state',
   proname => 'pg_get_wal_summarizer_state',
   provolatile => 'v', proparallel => 's',
   prorettype => 'record', proargtypes => '',
diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index fbf5f1c515..9f9b9f8724 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -112,6 +112,7 @@ typedef struct _restoreOptions
 	int			no_publications;	/* Skip publication entries */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
@@ -181,6 +182,7 @@ typedef struct _dumpOptions
 	int			no_security_labels;
 	int			no_publications;
 	int			no_subscriptions;
+	int			no_statistics;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index c6c101c118..62d2a50117 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2955,6 +2955,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's a stats dump, maybe ignore it */
+	if (ropt->no_statistics && strcmp(te->desc, "STATISTICS") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2984,6 +2988,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 242ebe807f..c38ee21133 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -435,6 +435,7 @@ main(int argc, char **argv)
 		{"no-comments", no_argument, &dopt.no_comments, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -1151,6 +1152,7 @@ help(const char *progname)
 	printf(_("  --no-comments                do not dump comments\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -6687,6 +6689,32 @@ getFuncs(Archive *fout, int *numFuncs)
 	return finfo;
 }
 
+/*
+ * getRelationStatistics
+ *    register the statistics object as a dependent of the relation.
+ *
+ */
+static void
+getRelationStatistics(Archive *fout, DumpableObject *reldobj, char relkind)
+{
+	RelStatsInfo	   *info = pg_malloc0(sizeof(RelStatsInfo));
+	DumpableObject	   *dobj = &info->dobj;
+
+	dobj->objType = DO_REL_STATS;
+	dobj->catId.tableoid = 0;
+	dobj->catId.oid = 0;
+	AssignDumpId(dobj);
+	dobj->dependencies = (DumpId *) pg_malloc(sizeof(DumpId));
+	dobj->dependencies[0] = reldobj->dumpId;
+	dobj->nDeps = 1;
+	dobj->allocDeps = 1;
+	dobj->components |= DUMP_COMPONENT_STATISTICS;
+	dobj->dump = reldobj->dump;
+	dobj->name = pg_strdup(reldobj->name);
+	dobj->namespace = reldobj->namespace;
+	info->relkind = relkind;
+}
+
 /*
  * getTables
  *	  read all the tables (no indexes) in the system catalogs,
@@ -7064,6 +7092,7 @@ getTables(Archive *fout, int *numTables)
 
 		/* Tables have data */
 		tblinfo[i].dobj.components |= DUMP_COMPONENT_DATA;
+		tblinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
 
 		/* Mark whether table has an ACL */
 		if (!PQgetisnull(res, i, i_relacl))
@@ -7112,6 +7141,8 @@ getTables(Archive *fout, int *numTables)
 				}
 			}
 		}
+		if (tblinfo[i].interesting)
+			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relkind);
 	}
 
 	if (query->len != 0)
@@ -7556,11 +7587,13 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 		for (int c = 0; c < numinds; c++, j++)
 		{
 			char		contype;
+			char		indexkind;
 
 			indxinfo[j].dobj.objType = DO_INDEX;
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -7583,6 +7616,12 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			{
 				NULL, NULL
 			};
+
+			if (indxinfo[j].parentidx == 0)
+				indexkind = RELKIND_INDEX;
+			else
+				indexkind = RELKIND_PARTITIONED_INDEX;
+
 			contype = *(PQgetvalue(res, j, i_contype));
 
 			if (contype == 'p' || contype == 'u' || contype == 'x')
@@ -7617,11 +7656,13 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				constrinfo->separate = true;
 
 				indxinfo[j].indexconstraint = constrinfo->dobj.dumpId;
+				getRelationStatistics(fout, &constrinfo->dobj, indexkind);
 			}
 			else
 			{
 				/* Plain secondary index */
 				indxinfo[j].indexconstraint = 0;
+				getRelationStatistics(fout, &indxinfo[j].dobj, indexkind);
 			}
 		}
 	}
@@ -10327,6 +10368,300 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * Tabular description of the parameters to pg_set_relation_stats()
+ * required, param_name, param_type
+ */
+static const char *rel_stats_arginfo[][3] = {
+	{"t", "relation", "regclass"},
+	{"t", "version", "integer"},
+	{"f", "relpages", "integer"},
+	{"f", "reltuples", "real"},
+	{"f", "relallvisible", "integer"},
+};
+
+/*
+ * Tabular description of the parameters to pg_set_attribute_stats()
+ * required, param_name, param_type
+ */
+static const char *att_stats_arginfo[][3] = {
+	{"t", "relation", "regclass"},
+	{"t", "attname", "name"},
+	{"t", "inherited", "boolean"},
+	{"t", "version", "integer"},
+	{"f", "null_frac", "float4"},
+	{"f", "avg_width", "integer"},
+	{"f", "n_distinct", "float4"},
+	{"f", "most_common_vals", "text"},
+	{"f", "most_common_freqs", "float4[]"},
+	{"f", "histogram_bounds", "text"},
+	{"f", "correlation", "float4"},
+	{"f", "most_common_elems", "text"},
+	{"f", "most_common_elem_freqs", "float4[]"},
+	{"f", "elem_count_histogram", "float4[]"},
+	{"f", "range_length_histogram", "text"},
+	{"f", "range_empty_frac", "float4"},
+	{"f", "range_bounds_histogram", "text"},
+};
+
+/*
+ * getRelStatsExportQuery --
+ *
+ * Generate a query that will fetch all relation (e.g. pg_class)
+ * stats for a given relation.
+ */
+static void
+getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "current_setting('server_version_num') AS version, "
+						 "c.relpages, c.reltuples, c.relallvisible "
+						 "FROM pg_class c "
+						 "JOIN pg_namespace n "
+						 "ON n.oid = c.relnamespace "
+						 "WHERE n.nspname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND c.relname = ");
+	appendStringLiteralAH(query, relname, fout);
+}
+
+/*
+ * getAttStatsExportQuery --
+ *
+ * Generate a query that will fetch all attribute (e.g. pg_statistic)
+ * stats for a given relation.
+ */
+static void
+getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "s.attname,"
+						 "s.inherited,"
+						 "current_setting('server_version_num') AS version, "
+						 "s.null_frac,"
+						 "s.avg_width,"
+						 "s.n_distinct,"
+						 "s.most_common_vals,"
+						 "s.most_common_freqs,"
+						 "s.histogram_bounds,"
+						 "s.correlation,"
+						 "s.most_common_elems,"
+						 "s.most_common_elem_freqs,"
+						 "s.elem_count_histogram,");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 "s.range_length_histogram,"
+							 "s.range_empty_frac,"
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(query,
+							 "NULL AS range_length_histogram,"
+							 "NULL AS range_empty_frac,"
+							 "NULL AS range_bounds_histogram ");
+
+	appendPQExpBufferStr(query,
+						 "FROM pg_stats s "
+						 "JOIN pg_namespace n "
+						 "ON n.nspname = s.schemaname "
+						 "JOIN pg_class c "
+						 "ON c.relname = s.tablename "
+						 "AND c.relnamespace = n.oid "
+						 "WHERE s.schemaname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND s.tablename = ");
+	appendStringLiteralAH(query, relname, fout);
+	appendPQExpBufferStr(query, " ORDER BY s.attname, s.inherited");
+}
+
+
+/*
+ * appendNamedArgument --
+ *
+ * Convenience routine for constructing parameters of the form:
+ * 'paraname', 'value'::type
+ */
+static void
+appendNamedArgument(PQExpBuffer out, Archive *fout,
+					const char *argname, bool positional,
+					const char *argval, const char *argtype)
+{
+	appendPQExpBufferStr(out, "\t");
+
+	if (!positional)
+	{
+		appendStringLiteralAH(out, argname, fout);
+		appendPQExpBufferStr(out, ", ");
+	}
+
+	appendStringLiteralAH(out, argval, fout);
+	appendPQExpBuffer(out, "::%s", argtype);
+}
+
+/*
+ * appendRelStatsImport --
+ *
+ * Append a formatted pg_set_relation_stats statement.
+ */
+static void
+appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	const char *sep = "";
+
+	if (PQntuples(res) == 0)
+		return;
+
+	appendPQExpBufferStr(out, "SELECT pg_catalog.pg_set_relation_stats(\n");
+
+	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
+	{
+		bool		positional = (rel_stats_arginfo[argno][0][0] == 't');
+		const char *argname = rel_stats_arginfo[argno][1];
+		const char *argtype = rel_stats_arginfo[argno][2];
+		int			fieldno = PQfnumber(res, argname);
+
+		if (fieldno < 0)
+			pg_fatal("relation stats export query missing field '%s'",
+					 argname);
+
+		if (PQgetisnull(res, 0, fieldno))
+		{
+			if (!positional)
+				pg_fatal("relation stats export query unexpected NULL in '%s'",
+						 argname);
+			else
+				continue;
+		}
+
+		appendPQExpBufferStr(out, sep);
+		appendNamedArgument(out, fout, argname, positional,
+							PQgetvalue(res, 0, fieldno), argtype);
+
+		sep = ",\n";
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+/*
+ * appendAttStatsImport --
+ *
+ * Append a series of formatted pg_set_attribute_stats statements.
+ */
+static void
+appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	{
+		const char *sep = "";
+
+		appendPQExpBufferStr(out, "SELECT pg_catalog.pg_set_attribute_stats(\n");
+		for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
+		{
+			bool		positional = (att_stats_arginfo[argno][0][0] == 't');
+			const char *argname = att_stats_arginfo[argno][1];
+			const char *argtype = att_stats_arginfo[argno][2];
+			int			fieldno = PQfnumber(res, argname);
+
+			if (fieldno < 0)
+				pg_fatal("attribute stats export query missing field '%s'",
+						 argname);
+
+			if (PQgetisnull(res, rownum, fieldno))
+			{
+				if (positional)
+					pg_fatal("attribute stats export query unexpected NULL in '%s'",
+							 argname);
+				else
+					continue;
+			}
+
+			appendPQExpBufferStr(out, sep);
+			appendNamedArgument(out, fout, argname, positional,
+								PQgetvalue(res, rownum, fieldno), argtype);
+			sep = ",\n";
+		}
+		appendPQExpBufferStr(out, "\n);\n");
+	}
+}
+
+/*
+ * get statistics dump section, which depends on the parent object type
+ *
+ * objects created in SECTION_PRE_DATA have stats in SECTION_DATA
+ * objects created in SECTION_POST_DATA have stats in SECTION_POST_DATA
+ */
+static teSection
+statisticsDumpSection(const RelStatsInfo *rsinfo)
+{
+
+	if ((rsinfo->relkind == RELKIND_MATVIEW) ||
+		(rsinfo->relkind == RELKIND_INDEX) ||
+		(rsinfo->relkind == RELKIND_PARTITIONED_INDEX))
+			return SECTION_POST_DATA;
+
+	return SECTION_DATA;
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	PGresult	   *res;
+	PQExpBuffer		query;
+	PQExpBuffer		out;
+	PQExpBuffer		tag;
+	DumpableObject *dobj = (DumpableObject *) &rsinfo->dobj;
+
+	/* do nothing, if --no-statistics is supplied */
+	if (fout->dopt->no_statistics)
+		return;
+
+	/* likewise, no stats on schema-only, unless in a binary upgrade */
+	if (fout->dopt->schemaOnly && !fout->dopt->binary_upgrade)
+		return;
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", "STATISTICS DATA", fmtId(dobj->name));
+
+	query = createPQExpBuffer();
+	out = createPQExpBuffer();
+
+	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendRelStatsImport(out, fout, res);
+	PQclear(res);
+
+	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendAttStatsImport(out, fout, res);
+	PQclear(res);
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATISTICS DATA",
+							  .section = statisticsDumpSection(rsinfo),
+							  .createStmt = out->data,
+							  .deps = &(dobj->dumpId),
+							  .nDeps = 1));
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(out);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -10775,6 +11110,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_SUBSCRIPTION_REL:
 			dumpSubscriptionTable(fout, (const SubRelInfo *) dobj);
 			break;
+		case DO_REL_STATS:
+			dumpRelationStats(fout, (const RelStatsInfo *) dobj);
+			break;
 		case DO_PRE_DATA_BOUNDARY:
 		case DO_POST_DATA_BOUNDARY:
 			/* never dumped, nothing to do */
@@ -16772,6 +17110,16 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 	if (tbinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
 		dumpTableSecLabel(fout, tbinfo, reltypename);
 
+	/* Statistics are dependent on the definition, not the data */
+	/* Views don't have stats */
+	/* TODO remove
+	if ((tbinfo->dobj.dump & DUMP_COMPONENT_STATISTICS) &&
+		(tbinfo->relkind != RELKIND_VIEW))
+		dumpRelationStats(fout, &tbinfo->dobj, reltypename,
+						  SECTION_DATA,
+						  tbinfo->dobj.dumpId);
+						  */
+
 	/* Dump comments on inlined table constraints */
 	for (j = 0; j < tbinfo->ncheck; j++)
 	{
@@ -16973,6 +17321,7 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 	PQExpBuffer delq;
 	char	   *qindxname;
 	char	   *qqindxname;
+	DumpId		dumpid;
 
 	/* Do nothing in data-only dump */
 	if (dopt->dataOnly)
@@ -17085,14 +17434,24 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 		free(indstatvalsarray);
 	}
 
+	/* Comments and stats share same .dep */
+	dumpid = is_constraint ? indxinfo->indexconstraint :
+		indxinfo->dobj.dumpId;
+
 	/* Dump Index Comments */
 	if (indxinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
 		dumpComment(fout, "INDEX", qindxname,
 					tbinfo->dobj.namespace->dobj.name,
 					tbinfo->rolname,
 					indxinfo->dobj.catId, 0,
-					is_constraint ? indxinfo->indexconstraint :
-					indxinfo->dobj.dumpId);
+					dumpid);
+
+	/* Dump Index Stats */
+	/* TODO remove
+	if (indxinfo->dobj.dump & DUMP_COMPONENT_STATISTICS)
+		dumpRelationStats(fout, &indxinfo->dobj, "INDEX",
+						  SECTION_POST_DATA, dumpid);
+						  */
 
 	destroyPQExpBuffer(q);
 	destroyPQExpBuffer(delq);
@@ -18782,6 +19141,16 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 				/* must come after the pre-data boundary */
 				addObjectDependency(dobj, preDataBound->dumpId);
 				break;
+			case DO_REL_STATS:
+				/* stats section varies by parent object type, DATA or POST */
+				if (statisticsDumpSection((RelStatsInfo *) dobj) == SECTION_DATA)
+					addObjectDependency(dobj, postDataBound->dumpId);
+				else
+				{
+					addObjectDependency(dobj, preDataBound->dumpId);
+					addObjectDependency(postDataBound, dobj->dumpId);
+				}
+				break;
 		}
 	}
 }
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 2a7c5873a0..86f984d579 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -84,6 +84,7 @@ typedef enum
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION,
 	DO_SUBSCRIPTION_REL,		/* see note for SubRelInfo */
+	DO_REL_STATS,
 } DumpableObjectType;
 
 /*
@@ -101,6 +102,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
@@ -421,6 +423,12 @@ typedef struct _indexAttachInfo
 	IndxInfo   *partitionIdx;	/* link to index on partition */
 } IndexAttachInfo;
 
+typedef struct _relStatsInfo
+{
+	DumpableObject	dobj;
+	char			relkind;		/* 'r', 'v', 'c', etc */
+} RelStatsInfo;
+
 typedef struct _statsExtInfo
 {
 	DumpableObject dobj;
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 4cb754caa5..cfe277f5ce 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -1490,6 +1490,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "POST-DATA BOUNDARY  (ID %d)",
 					 obj->dumpId);
 			return;
+		case DO_REL_STATS:
+			snprintf(buf, bufsize,
+					 "RELATION STATISTICS FOR %s  (ID %d OID %u)",
+					 obj->name, obj->dumpId, obj->catId.oid);
+			return;
 	}
 	/* shouldn't get here */
 	snprintf(buf, bufsize,
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 73337f3392..c4eaff3063 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -103,6 +103,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -172,6 +173,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -451,6 +453,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -666,6 +670,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 5ea78cf7cc..33ae4f92bc 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -75,6 +75,7 @@ main(int argc, char **argv)
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
 	static int	no_subscriptions = 0;
+	static int  no_statistics = 0;
 	static int	strict_names = 0;
 
 	struct option cmdopts[] = {
@@ -127,6 +128,7 @@ main(int argc, char **argv)
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
+		{"no-statistics", no_argument, &no_statistics, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -370,6 +372,7 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index 08d775379f..c86793b7db 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -1081,6 +1081,15 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index 2e3ba80258..845f739a45 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -723,6 +723,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore statistics, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
-- 
2.44.0

#162Bruce Momjian
bruce@momjian.us
In reply to: Matthias van de Meent (#160)
Re: Statistics Import and Export

On Tue, Apr 23, 2024 at 06:33:48PM +0200, Matthias van de Meent wrote:

I've heard of use cases where dumping stats without data would help
with production database planner debugging on a non-prod system.

Sure, some planner inputs would have to be taken into account too, but
having an exact copy of production stats is at least a start and can
help build models and alerts for what'll happen when the tables grow
larger with the current stats.

As for other planner inputs: table size is relatively easy to shim
with sparse files; cumulative statistics can be copied from a donor
replica if needed, and btree indexes only really really need to
contain their highest and lowest values (and need their height set
correctly).

Is it possible to prevent stats from being updated by autovacuum and
other methods?

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

Only you can decide what is important to you.

#163Matthias van de Meent
boekewurm+postgres@gmail.com
In reply to: Bruce Momjian (#162)
Re: Statistics Import and Export

On Wed, 24 Apr 2024 at 21:31, Bruce Momjian <bruce@momjian.us> wrote:

On Tue, Apr 23, 2024 at 06:33:48PM +0200, Matthias van de Meent wrote:

I've heard of use cases where dumping stats without data would help
with production database planner debugging on a non-prod system.

Sure, some planner inputs would have to be taken into account too, but
having an exact copy of production stats is at least a start and can
help build models and alerts for what'll happen when the tables grow
larger with the current stats.

As for other planner inputs: table size is relatively easy to shim
with sparse files; cumulative statistics can be copied from a donor
replica if needed, and btree indexes only really really need to
contain their highest and lowest values (and need their height set
correctly).

Is it possible to prevent stats from being updated by autovacuum

You can set autovacuum_analyze_threshold and *_scale_factor to
excessively high values, which has the effect of disabling autoanalyze
until it has had similarly excessive tuple churn. But that won't
guarantee autoanalyze won't run; that guarantee only exists with
autovacuum = off.

and other methods?

No nice ways. AFAIK there is no command (or command sequence) that can
"disable" only ANALYZE and which also guarantee statistics won't be
updated until ANALYZE is manually "re-enabled" for that table. An
extension could maybe do this, but I'm not aware of any extension
points where this would hook into PostgreSQL in a nice way.

You can limit maintenance access on the table to only trusted roles
that you know won't go in and run ANALYZE for those tables, or even
only your superuser (so only they can run ANALYZE, and have them
promise they won't). Alternatively, you can also constantly keep a
lock on the table that conflicts with ANALYZE. The last few are just
workarounds though, and not all something I'd suggest running on a
production database.

Kind regards,

Matthias van de Meent

#164Corey Huinker
corey.huinker@gmail.com
In reply to: Matthias van de Meent (#163)
2 attachment(s)
Re: Statistics Import and Export

You can set autovacuum_analyze_threshold and *_scale_factor to
excessively high values, which has the effect of disabling autoanalyze
until it has had similarly excessive tuple churn. But that won't
guarantee autoanalyze won't run; that guarantee only exists with
autovacuum = off.

I'd be a bit afraid to set to those values so high, for fear that they
wouldn't get reset when normal operations resumed, and nobody would notice
until things got bad.

v20 is attached. It resolves the dependency issue in v19, so while I'm
still unclear as to why we want it this way vs the simplicity of
SECTION_NONE, I'm going to roll with it.

Next up for question is how to handle --statistics-only or an equivalent.
The option would be mutually exclusive with --schema-only and --data-only,
and it would be mildly incongruous if it didn't have a short option like
the others, so I'm suggested -P for Probablity / Percentile / ρ:
correlation / etc.

One wrinkle with having three mutually exclusive options instead of two is
that the existing code was able to assume that one of the options being
true meant that we could bail out of certain dumpXYZ() functions, and now
those tests have to compare against two, which makes me think we should add
three new DumpOptions that are the non-exclusive positives (yesSchema,
yesData, yesStats) and set those in addition to the schemaOnly, dataOnly,
and statsOnly flags. Thoughts?

Attachments:

v20-0001-Create-pg_set_relation_stats-pg_set_attribute_st.patchtext/x-patch; charset=US-ASCII; name=v20-0001-Create-pg_set_relation_stats-pg_set_attribute_st.patchDownload
From 1a04c103f4f41e88098108d0bbcc8d54a1530000 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 21 Mar 2024 18:04:54 -0400
Subject: [PATCH v20 1/2] Create pg_set_relation_stats, pg_set_attribute_stats.

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

Both functions take an oid to identify the target relation that will
receive the statistics. There is nothing requiring that relation to be
the same one as the one exported, though the statistics supplied have
to make sense in the context of the new relation.

Both functions take a variadic list of statistic parameters name-value
pairs.

Each name given for pg_set_relation_stats must correspond to a
statistics attribute in pg_class, and the paired value must match the
datatype of said attribute in pg_class.

Each name given for pg_set_attribute_stats must correspond to a
statistics attribute in pg_stats, and the paird value must match the
datatype of said attribute in pg_stats, except for attributes of type
ANYARRAY, in which case a TEXT value is required.

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

These functions 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               |   22 +-
 src/include/statistics/statistics.h           |    2 +
 src/backend/statistics/Makefile               |    3 +-
 src/backend/statistics/meson.build            |    1 +
 src/backend/statistics/statistics.c           | 1271 +++++++++++++++++
 .../regress/expected/stats_export_import.out  |  853 +++++++++++
 src/test/regress/parallel_schedule            |    3 +-
 src/test/regress/sql/stats_export_import.sql  |  671 +++++++++
 doc/src/sgml/func.sgml                        |  267 ++++
 9 files changed, 3088 insertions(+), 5 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 134e3b22fd..26f62c3651 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12178,9 +12178,6 @@
   proallargtypes => '{int8,pg_lsn,pg_lsn,oid,oid,oid,int2,int8,bool}',
   proargmodes => '{i,i,i,o,o,o,o,o,o}',
   proargnames => '{tli,start_lsn,end_lsn,relfilenode,reltablespace,reldatabase,relforknumber,relblocknumber,is_limit_block}',
-  prosrc => 'pg_wal_summary_contents' },
-{ oid => '8438',
-  descr => 'WAL summarizer state',
   proname => 'pg_get_wal_summarizer_state',
   provolatile => 'v', proparallel => 's',
   prorettype => 'record', proargtypes => '',
@@ -12195,4 +12192,23 @@
   proargtypes => 'int2',
   prosrc => 'gist_stratnum_identity' },
 
+# Statistics Import
+{ oid => '8048',
+  descr => 'set statistics on relation',
+  proname => 'pg_set_relation_stats', provariadic => 'any',
+  proisstrict => 't', provolatile => 'v',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'regclass int4 any',
+  proargnames => '{relation,version,stats}',
+  proargmodes => '{i,i,v}',
+  prosrc => 'pg_set_relation_stats' },
+{ oid => '8049',
+  descr => 'set statistics on attribute',
+  proname => 'pg_set_attribute_stats', provariadic => 'any',
+  proisstrict => 't', provolatile => 'v',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'regclass name bool int4 any',
+  proargnames => '{relation,attname,inherited,version,stats}',
+  proargmodes => '{i,i,i,i,v}',
+  prosrc => 'pg_set_attribute_stats' },
 ]
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 7f2bf18716..1dddf96576 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -127,4 +127,6 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												int nclauses);
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
+extern Datum pg_set_relation_stats(PG_FUNCTION_ARGS);
+extern Datum pg_set_attribute_stats(PG_FUNCTION_ARGS);
 #endif							/* STATISTICS_H */
diff --git a/src/backend/statistics/Makefile b/src/backend/statistics/Makefile
index 89cf8c2797..e4f8ab7c4f 100644
--- a/src/backend/statistics/Makefile
+++ b/src/backend/statistics/Makefile
@@ -16,6 +16,7 @@ OBJS = \
 	dependencies.o \
 	extended_stats.o \
 	mcv.o \
-	mvdistinct.o
+	mvdistinct.o \
+	statistics.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/statistics/meson.build b/src/backend/statistics/meson.build
index 73b29a3d50..331e82c776 100644
--- a/src/backend/statistics/meson.build
+++ b/src/backend/statistics/meson.build
@@ -5,4 +5,5 @@ backend_sources += files(
   'extended_stats.c',
   'mcv.c',
   'mvdistinct.c',
+  'statistics.c',
 )
diff --git a/src/backend/statistics/statistics.c b/src/backend/statistics/statistics.c
new file mode 100644
index 0000000000..e092eb3dc3
--- /dev/null
+++ b/src/backend/statistics/statistics.c
@@ -0,0 +1,1271 @@
+/*-------------------------------------------------------------------------
+ * statistics.c
+ *
+ *	  POSTGRES statistics import
+ *
+ * Code supporting the direct importation of relation statistics, similar to
+ * what is done by the ANALYZE command.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *       src/backend/statistics/statistics.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_collation.h"
+#include "catalog/pg_database.h"
+#include "catalog/pg_operator.h"
+#include "catalog/pg_type.h"
+#include "fmgr.h"
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "nodes/nodeFuncs.h"
+#include "statistics/statistics.h"
+#include "utils/acl.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/float.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/rangetypes.h"
+#include "utils/syscache.h"
+#include "utils/typcache.h"
+
+/*
+ * A role has privileges to set statistics on the relation if any of the
+ * following are true:
+ *   - the role owns the current database and the relation is not shared
+ *   - the role has the MAINTAIN privilege on the relation
+ *
+ */
+static bool
+can_modify_relation(Relation rel)
+{
+	Oid			relid = RelationGetRelid(rel);
+	Form_pg_class reltuple = rel->rd_rel;
+
+	return ((object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId())
+			 && !reltuple->relisshared) ||
+			pg_class_aclcheck(relid, GetUserId(), ACL_MAINTAIN) == ACLCHECK_OK);
+}
+
+/*
+ * Set statistics for a given pg_class entry.
+ *
+ * This does an in-place (i.e. non-transactional) update of pg_class, just as
+ * is done in ANALYZE.
+ *
+ */
+Datum
+pg_set_relation_stats(PG_FUNCTION_ARGS)
+{
+	Oid			relid = PG_GETARG_OID(0);
+	int			version = PG_GETARG_INT32(1);
+
+	/* indexes of where we found required stats */
+	int			i_relpages = 0;
+	int			i_reltuples = 0;
+	int			i_relallvisible = 0;
+
+	/* build argument values to build the object */
+	Datum	   *args;
+	bool	   *nulls;			/* placeholder, because strict */
+	Oid		   *types;
+	int			nargs;
+
+	/* Minimum version supported */
+	if (version <= 90200)
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Cannot export statistics prior to version 9.2")));
+
+	nargs = extract_variadic_args(fcinfo, 2, true, &args, &types, &nulls);
+
+	/* if the pairs aren't pairs, something is malformed */
+	if (nargs % 2 == 1)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("stats parameters must be in name-value pairs")));
+		PG_RETURN_BOOL(false);
+	}
+
+	/* loop through args, matching params to their arg indexes */
+	for (int i = 0; i < nargs; i += 2)
+	{
+		char	   *statname;
+		int			argidx = i + 1;
+
+		if (types[i] != TEXTOID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("stat names must be of type text")));
+			continue;
+		}
+		statname = TextDatumGetCString(args[i]);
+
+		/*
+		 * match each named parameter to the index of the value that follows
+		 * it
+		 */
+		if (strcmp(statname, "relpages") == 0)
+			i_relpages = argidx;
+		else if (strcmp(statname, "reltuples") == 0)
+			i_reltuples = argidx;
+		else if (strcmp(statname, "relallvisible") == 0)
+			i_relallvisible = argidx;
+		else
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("unknown stat naame '%s', skipping", statname)));
+
+		pfree(statname);
+	}
+
+	/*
+	 * Ensure that we got all required parameters, and they are of the correct
+	 * type.
+	 */
+	if (i_relpages == 0)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "relpages")));
+		PG_RETURN_BOOL(false);
+	}
+	else if (types[i_relpages] != INT4OID)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relpages must be of type integer")));
+		PG_RETURN_BOOL(false);
+	}
+	else if (i_reltuples == 0)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "reltuples")));
+		PG_RETURN_BOOL(false);
+	}
+	else if (types[i_reltuples] != FLOAT4OID)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("reltuples must be of type real")));
+		PG_RETURN_BOOL(false);
+	}
+	else if (types[i_relallvisible] != INT4OID)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "relallvisible")));
+		PG_RETURN_BOOL(false);
+	}
+	else if (i_relallvisible == -1)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relallvisible must be of type integer")));
+		PG_RETURN_BOOL(false);
+	}
+	else
+	{
+		/*
+		 * Open the relation, getting ShareUpdateExclusiveLock to ensure that
+		 * no other stat-setting operation can run on it concurrently.
+		 */
+		Relation	rel;
+		HeapTuple	ctup;
+		Form_pg_class pgcform;
+		int			relpages;
+		float4		reltuples;
+		int			relallvisible;
+
+		rel = table_open(RelationRelationId, ShareUpdateExclusiveLock);
+
+		if (!can_modify_relation(rel))
+		{
+			table_close(rel, NoLock);
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("must be owner to modify relation stats")));
+			PG_RETURN_BOOL(false);
+		}
+
+		relpages = DatumGetInt32(args[i_relpages]);
+		reltuples = DatumGetFloat4(args[i_reltuples]);
+		relallvisible = DatumGetInt32(args[i_relallvisible]);
+
+		ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+		if (!HeapTupleIsValid(ctup))
+			ereport(ERROR,
+					(errcode(ERRCODE_OBJECT_IN_USE),
+					 errmsg("pg_class entry for relid %u not found", relid)));
+
+		pgcform = (Form_pg_class) GETSTRUCT(ctup);
+		/* Only update pg_class if there is a meaningful change */
+		if ((pgcform->reltuples != reltuples)
+			|| (pgcform->relpages != relpages)
+			|| (pgcform->relallvisible != relallvisible))
+		{
+			pgcform->relpages = relpages;
+			pgcform->reltuples = reltuples;
+			pgcform->relallvisible = relallvisible;
+
+			heap_inplace_update(rel, ctup);
+		}
+
+		table_close(rel, NoLock);
+		PG_RETURN_BOOL(true);
+	}
+
+	PG_RETURN_BOOL(false);
+}
+
+/*
+ * Test if the type is a scalar for MCELEM purposes
+ */
+static bool
+type_is_scalar(Oid typid)
+{
+	HeapTuple	tp;
+	bool		result = false;
+
+	tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+	if (HeapTupleIsValid(tp))
+	{
+		Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+
+		result = (!OidIsValid(typtup->typanalyze));
+		ReleaseSysCache(tp);
+	}
+	return result;
+}
+
+/*
+ * If this relation is an index and that index has expressions in it, and
+ * the attnum specified is known to be an expression, then we must walk
+ * the list attributes up to the specified attnum to get the right
+ * expression.
+ */
+static Node *
+get_attr_expr(Relation rel, int attnum)
+{
+	if ((rel->rd_rel->relkind == RELKIND_INDEX
+		 || (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+		&& (rel->rd_indexprs != NIL)
+		&& (rel->rd_index->indkey.values[attnum - 1] == 0))
+	{
+		ListCell   *indexpr_item = list_head(rel->rd_indexprs);
+
+		for (int i = 0; i < attnum - 1; i++)
+			if (rel->rd_index->indkey.values[i] == 0)
+				indexpr_item = lnext(rel->rd_indexprs, indexpr_item);
+
+		if (indexpr_item == NULL)	/* shouldn't happen */
+			elog(ERROR, "too few entries in indexprs list");
+
+		return (Node *) lfirst(indexpr_item);
+	}
+	return NULL;
+}
+
+/*
+ * Fetch datatype information, this is needed to derive the proper staopN
+ * and stacollN values.
+ *
+ */
+static TypeCacheEntry *
+get_attr_stat_type(Relation rel, Name attname,
+				   int16 *attnum, int32 *typmod, Oid *typcoll)
+{
+	Oid			relid = RelationGetRelid(rel);
+	Form_pg_attribute attr;
+	HeapTuple	atup;
+	Oid			typid;
+	Node	   *expr;
+
+	atup = SearchSysCache2(ATTNAME, ObjectIdGetDatum(relid),
+						   NameGetDatum(attname));
+
+	/* Attribute not found */
+	if (!HeapTupleIsValid(atup))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s has no attname %s",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+		return NULL;
+	}
+
+	attr = (Form_pg_attribute) GETSTRUCT(atup);
+	if (attr->attisdropped)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s attname %s is dropped",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+		return NULL;
+	}
+	*attnum = attr->attnum;
+
+	expr = get_attr_expr(rel, attr->attnum);
+
+	if (expr == NULL)
+	{
+		/* regular attribute */
+		typid = attr->atttypid;
+		*typmod = attr->atttypmod;
+		*typcoll = attr->attcollation;
+	}
+	else
+	{
+		typid = exprType(expr);
+		*typmod = exprTypmod(expr);
+
+		/*
+		 * If a collation has been specified for the index column, use that in
+		 * preference to anything else; but if not, fall back to whatever we
+		 * can get from the expression.
+		 */
+		if (OidIsValid(attr->attcollation))
+			*typcoll = attr->attcollation;
+		else
+			*typcoll = exprCollation(expr);
+	}
+	ReleaseSysCache(atup);
+
+	/* if it's a multirange, step down to the range type */
+	if (type_is_multirange(typid))
+		typid = get_multirange_range(typid);
+
+	return lookup_type_cache(typid,
+							 TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR);
+}
+
+/*
+ * Perform the cast of a known TextDatum into the type specified.
+ *
+ * If no errors are found, ok is set to true. Otherwise, set ok
+ * to false, capture the error found, and re-throw at warning level.
+ */
+static Datum
+cast_stavalues(FmgrInfo *flinfo, Datum d, Oid typid, int32 typmod, bool *ok)
+{
+	LOCAL_FCINFO(fcinfo, 8);
+	char	   *s;
+	Datum		result;
+	ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+	escontext.details_wanted = true;
+
+	s = TextDatumGetCString(d);
+
+	InitFunctionCallInfoData(*fcinfo, flinfo, 3, InvalidOid,
+							 (Node *) &escontext, NULL);
+
+	fcinfo->args[0].value = CStringGetDatum(s);
+	fcinfo->args[0].isnull = false;
+	fcinfo->args[1].value = ObjectIdGetDatum(typid);
+	fcinfo->args[1].isnull = false;
+	fcinfo->args[2].value = Int32GetDatum(typmod);
+	fcinfo->args[2].isnull = false;
+
+	result = FunctionCallInvoke(fcinfo);
+
+	if (SOFT_ERROR_OCCURRED(&escontext))
+	{
+		escontext.error_data->elevel = WARNING;
+		ThrowErrorData(escontext.error_data);
+		*ok = false;
+	}
+	else
+		*ok = true;
+
+	pfree(s);
+
+	return result;
+}
+
+
+/*
+ * Check array for any NULLs, and optionally for one-dimensionality.
+ *
+ * Report any failures as warnings.
+ */
+static bool
+array_check(Datum datum, int one_dim, const char *statname)
+{
+	ArrayType  *arr = DatumGetArrayTypeP(datum);
+	int16		elmlen;
+	char		elmalign;
+	bool		elembyval;
+	Datum	   *values;
+	bool	   *nulls;
+	int			nelems;
+
+	if (one_dim && (arr->ndim != 1))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be a multidimensional array", statname)));
+		return false;
+	}
+
+	get_typlenbyvalalign(ARR_ELEMTYPE(arr), &elmlen, &elembyval, &elmalign);
+
+	deconstruct_array(arr, ARR_ELEMTYPE(arr), elmlen, elembyval, elmalign,
+					  &values, &nulls, &nelems);
+
+	for (int i = 0; i < nelems; i++)
+		if (nulls[i])
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s array cannot contain NULL values", statname)));
+			return false;
+		}
+	return true;
+}
+
+/*
+ * Update the pg_statistic record.
+ */
+static void
+update_pg_statistic(Datum values[], bool nulls[])
+{
+	Relation	sd = table_open(StatisticRelationId, RowExclusiveLock);
+	CatalogIndexState indstate = CatalogOpenIndexes(sd);
+	HeapTuple	oldtup;
+	HeapTuple	stup;
+
+	/* Is there already a pg_statistic tuple for this attribute? */
+	oldtup = SearchSysCache3(STATRELATTINH,
+							 values[Anum_pg_statistic_starelid - 1],
+							 values[Anum_pg_statistic_staattnum - 1],
+							 values[Anum_pg_statistic_stainherit - 1]);
+
+	if (HeapTupleIsValid(oldtup))
+	{
+		/* Yes, replace it */
+		bool		replaces[Natts_pg_statistic];
+
+		for (int i = 0; i < Natts_pg_statistic; i++)
+			replaces[i] = true;
+
+		stup = heap_modify_tuple(oldtup, RelationGetDescr(sd),
+								 values, nulls, replaces);
+		ReleaseSysCache(oldtup);
+		CatalogTupleUpdateWithInfo(sd, &stup->t_self, stup, indstate);
+	}
+	else
+	{
+		/* No, insert new tuple */
+		stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
+		CatalogTupleInsertWithInfo(sd, stup, indstate);
+	}
+
+	heap_freetuple(stup);
+	CatalogCloseIndexes(indstate);
+	table_close(sd, NoLock);
+}
+
+/*
+ * Import statistics for a given relation attribute.
+ *
+ * This will insert/replace a row in pg_statistic for the given combinarion
+ * of relation, attribute, and inherited flag.
+ *
+ * The version parameter is for future use in the events as future versions
+ * may change them meaning of certain parameters.
+ *
+ * The variadic parameters all represent name-value pairs, with the names
+ * corresponding to attributes in pg_stats. Unkown names will generate a
+ * warning.
+ *
+ * Parameters null_frac, avg_width, and n_distinct are required because
+ * those attributes have no default value in pg_statistic.
+ *
+ * The remaining parameters all belong to a specific stakind, and all are
+ * optional. Some stakinds have multiple parameters, and in those cases
+ * both parameters must be specified if one of them is, otherwise a
+ * warning is generated but the rest of the stats may still be imported.
+ *
+ * If there is no attribute with a matching attname in the relation, the
+ * function will raise a warning and return false.
+ *
+ * Parameters corresponding to ANYARRAY columns are instead passed in as text
+ * values, which is a valid input string for an array of the type or element
+ * type of the attribute.
+ */
+Datum
+pg_set_attribute_stats(PG_FUNCTION_ARGS)
+{
+	Oid			relid = PG_GETARG_OID(0);
+	Name		attname = PG_GETARG_NAME(1);
+	bool		inherited = PG_GETARG_BOOL(2);
+	int			version = PG_GETARG_INT32(3);
+
+	/* build argument values to build the object */
+	Datum	   *args;
+	bool	   *argnulls;		/* placeholder, because strict */
+	Oid		   *types;
+	int			nargs = extract_variadic_args(fcinfo, 4, true,
+											  &args, &types, &argnulls);
+
+	Datum		values[Natts_pg_statistic] = {0};
+	bool		nulls[Natts_pg_statistic] = {false};
+
+	/*
+	 * argument indexes for each known statistic
+	 *
+	 * 0 = not found, -1 = error, n > 0 = found
+	 */
+
+	/* parameters that are required get indexes */
+	int			i_null_frac = 0;
+	int			i_avg_width = 0;
+	int			i_n_distinct = 0;
+
+	/* stakind stats are optional */
+	int			i_mc_vals = 0;
+	int			i_mc_freqs = 0;
+	int			i_hist_bounds = 0;
+	int			i_correlation = 0;
+	int			i_mc_elems = 0;
+	int			i_mc_elem_freqs = 0;
+	int			i_elem_count_hist = 0;
+	int			i_range_length_hist = 0;
+	int			i_range_empty_frac = 0;
+	int			i_range_bounds_hist = 0;
+
+	Relation	rel;
+
+	TypeCacheEntry *typcache;
+	TypeCacheEntry *elemtypcache = NULL;
+
+	int16		attnum;
+	int32		typmod;
+	Oid			typcoll;
+
+	FmgrInfo	finfo;
+
+	int			stakind_count;
+
+	int			k = 0;
+
+	if (version <= 90200)
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Cannot export statistics prior to version 9.2")));
+
+	/* if the pairs aren't pairs, something is malformed */
+	if (nargs % 2 == 1)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("stats parameters must be in name-value pairs")));
+		PG_RETURN_BOOL(false);
+	}
+
+	for (int i = 0; i < nargs; i += 2)
+	{
+		char	   *statname;
+		int			argidx = i + 1;
+
+		if (types[i] != TEXTOID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("stat names must be of type text")));
+			break;
+		}
+		statname = TextDatumGetCString(args[i]);
+
+		if (strcmp(statname, "null_frac") == 0)
+			i_null_frac = argidx;
+		else if (strcmp(statname, "avg_width") == 0)
+			i_avg_width = argidx;
+		else if (strcmp(statname, "n_distinct") == 0)
+			i_n_distinct = argidx;
+		else if (strcmp(statname, "most_common_vals") == 0)
+			i_mc_vals = argidx;
+		else if (strcmp(statname, "most_common_freqs") == 0)
+			i_mc_freqs = argidx;
+		else if (strcmp(statname, "histogram_bounds") == 0)
+			i_hist_bounds = argidx;
+		else if (strcmp(statname, "correlation") == 0)
+			i_correlation = argidx;
+		else if (strcmp(statname, "most_common_elems") == 0)
+			i_mc_elems = argidx;
+		else if (strcmp(statname, "most_common_elem_freqs") == 0)
+			i_mc_elem_freqs = argidx;
+		else if (strcmp(statname, "elem_count_histogram") == 0)
+			i_elem_count_hist = argidx;
+		else if (strcmp(statname, "range_length_histogram") == 0)
+			i_range_length_hist = argidx;
+		else if (strcmp(statname, "range_empty_frac") == 0)
+			i_range_empty_frac = argidx;
+		else if (strcmp(statname, "range_bounds_histogram") == 0)
+			i_range_bounds_hist = argidx;
+		else
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("unknown stat naame '%s', skipping", statname)));
+
+		pfree(statname);
+	}
+
+	/* check all required parameters */
+	if (i_null_frac > 0)
+	{
+		if (types[i_null_frac] != FLOAT4OID)
+		{
+			/* required param, not recoverable */
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be of type %s", "null_frac",
+							"real")));
+			PG_RETURN_BOOL(false);
+		}
+	}
+	else
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "null_frac")));
+		PG_RETURN_BOOL(false);
+	}
+
+	if (i_avg_width > 0)
+	{
+		if (types[i_avg_width] != INT4OID)
+		{
+			/* required param, not recoverable */
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be of type %s", "avg_width",
+							"integer")));
+			PG_RETURN_BOOL(false);
+		}
+	}
+	else
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "avg_width")));
+		PG_RETURN_BOOL(false);
+	}
+
+	if (i_n_distinct > 0)
+	{
+		if (types[i_n_distinct] != FLOAT4OID)
+		{
+			/* required param, not recoverable */
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be of type %s", "n_distinct",
+							"real")));
+			PG_RETURN_BOOL(false);
+		}
+	}
+	else
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "n_distinct")));
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * Look for pair mismatches, if found warn and disable.
+	 */
+	if ((i_mc_vals == 0) != (i_mc_freqs == 0))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be present if %s is missing",
+						(i_mc_vals == 0) ? "most_common_freqs" :
+						"most_common_vals",
+						(i_mc_vals == 0) ? "most_common_vals" :
+						"most_common_freqs")));
+		i_mc_vals = -1;
+		i_mc_freqs = -1;
+	}
+
+	if ((i_mc_elems == 0) != (i_mc_elem_freqs == 0))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be present if %s is missing",
+						(i_mc_elems == 0) ?
+						"most_common_elem_freqs" :
+						"most_common_elems",
+						(i_mc_elems == 0) ?
+						"most_common_elems" :
+						"most_common_elem_freqs")));
+		i_mc_elems = -1;
+		i_mc_elem_freqs = -1;
+	}
+
+	if ((i_range_length_hist == 0) != (i_range_empty_frac == 0))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be present if %s is missing",
+						(i_range_length_hist == 0) ?
+						"range_empty_frac" :
+						"range_length_histogram",
+						(i_range_length_hist == 0) ?
+						"range_length_histogram" :
+						"range_empty_frac")));
+		i_range_length_hist = -1;
+		i_range_empty_frac = -1;
+	}
+
+	rel = relation_open(relid, ShareUpdateExclusiveLock);
+
+	if (!can_modify_relation(rel))
+	{
+		relation_close(rel, NoLock);
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						RelationGetRelationName(rel))));
+		PG_RETURN_BOOL(false);
+	}
+
+	if ((!inherited) &&
+		((rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) ||
+		 (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX)))
+	{
+		relation_close(rel, NoLock);
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("%s is partitioned, can only accepted inherted stats",
+						RelationGetRelationName(rel))));
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * Many of the values that are set for a particular stakind are entirely
+	 * derived from the attribute itself, or it's expression.
+	 */
+	typcache = get_attr_stat_type(rel, attname, &attnum, &typmod, &typcoll);
+	if (typcache == NULL)
+	{
+		relation_close(rel, NoLock);
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * Derive element type if we have stat kinds that need it.
+	 *
+	 * This duplicates some type-specific logic found in various typanalyze
+	 * functions which are called from vacuum's examine_attribute().
+	 */
+	if ((i_mc_elems > 0) || (i_elem_count_hist > 0))
+	{
+		Oid			elemtypid;
+
+		if (typcache->type_id == TSVECTOROID)
+		{
+			/*
+			 * tsvector elems always have a text oid type and default
+			 * collation
+			 */
+			elemtypid = TEXTOID;
+			typcoll = DEFAULT_COLLATION_OID;
+		}
+		else if (typcache->typtype == TYPTYPE_RANGE)
+			elemtypid = get_range_subtype(typcache->type_id);
+		else
+			elemtypid = get_base_element_type(typcache->type_id);
+
+		/* not finding a basetype means we already had it */
+		if (elemtypid == InvalidOid)
+			elemtypid = typcache->type_id;
+
+		/* The stats need the eq_opr, any validation would need the lt_opr */
+		elemtypcache = lookup_type_cache(elemtypid, TYPECACHE_EQ_OPR);
+
+		if (elemtypcache == NULL)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							(i_mc_elems > 0) ?
+							"most_common_elems" :
+							"elem_count_histogram")));
+			i_mc_elems = -1;
+			i_elem_count_hist = -1;
+		}
+	}
+
+	/*
+	 * histogram_bounds and correlation must have a type < operator. WARN and
+	 * skip if this attribute doesn't have one.
+	 */
+	if (typcache->lt_opr == InvalidOid)
+	{
+		if (i_hist_bounds > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"histogram_bounds")));
+			i_hist_bounds = -1;
+		}
+		if (i_correlation > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"correlation")));
+			i_correlation = -1;
+		}
+	}
+
+	/*
+	 * Scalar types can't have most_common_elems, most_common_elem_freqs,
+	 * elem_count_histogram. WARN and skip.
+	 */
+	if (type_is_scalar(typcache->type_id))
+	{
+		if (i_mc_elems > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"most_common_elems")));
+			i_mc_elems = -1;
+		}
+		if (i_mc_elem_freqs > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"most_common_elem_freqs")));
+			i_mc_elem_freqs = -1;
+		}
+		if (i_elem_count_hist > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"elem_count_histogram")));
+			i_elem_count_hist = -1;
+		}
+	}
+
+	/*
+	 * Only range types can have range_length_histogram, range_empty_frac, and
+	 * range_bounds_histogram. WARN and skip
+	 */
+	if ((typcache->typtype != TYPTYPE_MULTIRANGE) &&
+		(typcache->typtype != TYPTYPE_RANGE))
+	{
+		if (i_range_length_hist > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"range_length_histogram")));
+			i_range_length_hist = -1;
+		}
+		if (i_range_empty_frac > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"range_empty_frac")));
+			i_range_empty_frac = -1;
+		}
+		if (i_range_bounds_hist > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"range_bounds_histogram")));
+			i_range_bounds_hist = -1;
+		}
+	}
+
+	/*
+	 * count the number of stakinds we still want to set, paired params count
+	 * as one. The count cannot exceed STATISTIC_NUM_SLOTS.
+	 */
+	stakind_count = (int) (i_mc_vals > 0) +
+		(int) (i_mc_elems > 0) +
+		(int) (i_range_length_hist > 0) +
+		(int) (i_hist_bounds > 0) +
+		(int) (i_correlation > 0) +
+		(int) (i_elem_count_hist > 0) +
+		(int) (i_range_bounds_hist > 0);
+
+	if (stakind_count > STATISTIC_NUM_SLOTS)
+	{
+		/*
+		 * This really shouldn't happen, as most datatypes exclude at least
+		 * one of these types of stats.
+		 */
+		relation_close(rel, NoLock);
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("imported statistics must have a maximum of %d slots "
+						"but %d given",
+						STATISTIC_NUM_SLOTS, stakind_count)));
+		PG_RETURN_BOOL(false);
+	}
+
+	fmgr_info(F_ARRAY_IN, &finfo);
+
+	values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(relid);
+	values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(attnum);
+	values[Anum_pg_statistic_stainherit - 1] = BoolGetDatum(inherited);
+	values[Anum_pg_statistic_stanullfrac - 1] = args[i_null_frac];
+	values[Anum_pg_statistic_stawidth - 1] = args[i_avg_width];
+	values[Anum_pg_statistic_stadistinct - 1] = args[i_n_distinct];
+
+	/*
+	 * STATISTIC_KIND_MCV
+	 *
+	 * most_common_freqs: real[]
+	 *
+	 * most_common_vals : ANYARRAY::text
+	 */
+	if (i_mc_vals > 0)
+	{
+		Oid			numberstype = types[i_mc_freqs];
+		Oid			valuestype = types[i_mc_vals];
+		Datum		stanumbers = args[i_mc_freqs];
+		Datum		strvalue = args[i_mc_vals];
+
+		if (get_element_type(numberstype) != FLOAT4OID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"most_common_freqs", "real[]")));
+		}
+		else if (valuestype != TEXTOID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"most_common_vals", "text")));
+		}
+		else
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_MCV);
+			Datum		staop = ObjectIdGetDatum(typcache->eq_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		stavalues;
+			bool		converted = false;
+
+			stavalues = cast_stavalues(&finfo, strvalue, typcache->type_id,
+									   typmod, &converted);
+
+			if (converted &&
+				array_check(stavalues, false, "most_common_vals") &&
+				array_check(stanumbers, true, "most_common_freqs"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_HISTOGRAM
+	 *
+	 * histogram_bounds: ANYARRAY::text
+	 */
+	if (i_hist_bounds > 0)
+	{
+		Oid			valuestype = types[i_hist_bounds];
+		Datum		strvalue = args[i_hist_bounds];
+
+		if (valuestype != TEXTOID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"histogram_bounds", "text")));
+		else
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_HISTOGRAM);
+			Datum		staop = ObjectIdGetDatum(typcache->lt_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		stavalues;
+			bool		converted = false;
+
+			stavalues = cast_stavalues(&finfo, strvalue, typcache->type_id,
+									   typmod, &converted);
+
+			if (converted && array_check(stavalues, false, "histogram_bounds"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_CORRELATION
+	 *
+	 * correlation: real
+	 */
+	if (i_correlation > 0)
+	{
+		if (types[i_correlation] != FLOAT4OID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be of type %s, ignored",
+							"correlation", "real")));
+		else
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_CORRELATION);
+			Datum		staop = ObjectIdGetDatum(typcache->lt_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		elems[] = {args[i_correlation]};
+			ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+			Datum		stanumbers = PointerGetDatum(arry);
+
+			values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+			values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+			values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+			values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+			nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+			k++;
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_MCELEM
+	 *
+	 * most_common_elem_freqs: real[]
+	 *
+	 * most_common_elems     : ANYARRAY::text
+	 */
+	if (i_mc_elems > 0)
+	{
+		Oid			numberstype = types[i_mc_elem_freqs];
+		Oid			valuestype = types[i_mc_elems];
+		Datum		stanumbers = args[i_mc_elem_freqs];
+
+		if (get_element_type(numberstype) != FLOAT4OID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"most_common_elem_freqs", "real[]")));
+		else if (valuestype != TEXTOID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"most_common_elems", "text")));
+		else
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_MCELEM);
+			Datum		staop = ObjectIdGetDatum(elemtypcache->eq_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		strvalue = args[i_mc_elems];
+			bool		converted = false;
+			Datum		stavalues;
+
+			stavalues = cast_stavalues(&finfo, strvalue, elemtypcache->type_id,
+									   typmod, &converted);
+
+			if (converted &&
+				array_check(stavalues, false, "most_common_elems") &&
+				array_check(stanumbers, true, "most_common_elem_freqs"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_DECHIST
+	 *
+	 * elem_count_histogram:	real[]
+	 */
+	if (i_elem_count_hist > 0)
+	{
+		Oid			numberstype = types[i_elem_count_hist];
+
+		if (get_element_type(numberstype) != FLOAT4OID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"elem_count_histogram", "real[]")));
+		else
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_DECHIST);
+			Datum		staop = ObjectIdGetDatum(elemtypcache->eq_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		stanumbers = args[i_elem_count_hist];
+
+			if (array_check(stanumbers, true, "elem_count_histogram"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+				nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+				k++;
+			}
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_BOUNDS_HISTOGRAM
+	 *
+	 * range_bounds_histogram: ANYARRAY::text
+	 *
+	 * This stakind appears before STATISTIC_KIND_BOUNDS_HISTOGRAM even though
+	 * it is numerically greater, and all other stakinds appear in numerical
+	 * order. We duplicate this quirk to make before/after tests of
+	 * pg_statistic records easier.
+	 */
+	if (i_range_bounds_hist > 0)
+	{
+		Oid			valuestype = types[i_range_bounds_hist];
+
+		if (valuestype != TEXTOID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"range_bounds_histogram", "text")));
+		else
+		{
+
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_BOUNDS_HISTOGRAM);
+			Datum		staop = ObjectIdGetDatum(InvalidOid);
+			Datum		stacoll = ObjectIdGetDatum(InvalidOid);
+			Datum		strvalue = args[i_range_bounds_hist];
+			bool		converted = false;
+			Datum		stavalues;
+
+			stavalues = cast_stavalues(&finfo, strvalue, typcache->type_id,
+									   typmod, &converted);
+
+			if (converted &&
+				array_check(stavalues, false, "range_bounds_histogram"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM
+	 *
+	 * range_empty_frac: real
+	 *
+	 * range_length_histogram:  double precision[]::text
+	 */
+	if (i_range_length_hist > 0)
+	{
+		Oid			numberstype = types[i_range_empty_frac];
+		Oid			valuestype = types[i_range_length_hist];
+
+		if (numberstype != FLOAT4OID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"range_empty_frac", "real")));
+		else if (valuestype != TEXTOID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"range_length_histogram", "text")));
+		else
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM);
+			Datum		staop = ObjectIdGetDatum(Float8LessOperator);
+			Datum		stacoll = ObjectIdGetDatum(InvalidOid);
+
+			/* The anyarray is always a float8[] for this stakind */
+			Datum		elem = args[i_range_empty_frac];
+			Datum		elems[] = {elem};
+			ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+			Datum		stanumbers = PointerGetDatum(arry);
+			Datum		strvalue = args[i_range_length_hist];
+			bool		converted = false;
+			Datum		stavalues;
+
+			stavalues = cast_stavalues(&finfo, strvalue, FLOAT8OID, 0, &converted);
+
+			if (converted &&
+				array_check(stavalues, false, "range_length_histogram"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+	}
+
+	/* fill in all remaining slots */
+	for (; k < STATISTIC_NUM_SLOTS; k++)
+	{
+		values[Anum_pg_statistic_stakind1 - 1 + k] = Int16GetDatum(0);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+	}
+
+	update_pg_statistic(values, nulls);
+
+	relation_close(rel, NoLock);
+	PG_RETURN_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..684df93993
--- /dev/null
+++ b/src/test/regress/expected/stats_export_import.out
@@ -0,0 +1,853 @@
+CREATE SCHEMA stats_export_import;
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+        0 |        -1 |             0
+(1 row)
+
+-- error: regclass not found
+SELECT pg_set_relation_stats('stats_export_import.nope'::regclass,
+                             150000::integer);
+ERROR:  relation "stats_export_import.nope" does not exist
+LINE 1: SELECT pg_set_relation_stats('stats_export_import.nope'::reg...
+                                     ^
+-- error: all three params missing
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer);
+ERROR:  function pg_set_relation_stats(regclass, integer) does not exist
+LINE 1: SELECT pg_set_relation_stats('stats_export_import.test'::reg...
+               ^
+HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+-- error: reltuples, relallvisible missing
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 4::integer);
+WARNING:  required parameter reltuples not set
+ pg_set_relation_stats 
+-----------------------
+ f
+(1 row)
+
+-- error: null value
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', NULL::real,
+                             'relallvisible', 4::integer);
+ pg_set_relation_stats 
+-----------------------
+ 
+(1 row)
+
+-- error: bad relpages type
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+WARNING:  relpages must be of type integer
+ pg_set_relation_stats 
+-----------------------
+ f
+(1 row)
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 17::integer,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+ pg_set_relation_stats 
+-----------------------
+ t
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       17 |       400 |             4
+(1 row)
+
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    '0'::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ERROR:  could not open relation with OID 0
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    NULL::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    NULL::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    NULL::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', NULL::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', NULL::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', NULL::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT stanullfrac, stawidth, stadistinct
+FROM pg_statistic
+WHERE starelid = 'stats_export_import.test'::regclass;
+ stanullfrac | stawidth | stadistinct 
+-------------+----------+-------------
+         0.1 |        2 |         0.3
+(1 row)
+
+-- warn: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_freqs', '{0.1,0.2,0.3}'::real[]
+    );
+WARNING:  most_common_freqs cannot be present if most_common_vals is missing
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{1,2,3}'::text
+    );
+WARNING:  most_common_vals cannot be present if most_common_freqs is missing
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.2,0.1}'::double precision[]
+    );
+WARNING:  most_common_freqs must be type real[], ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warning: mcv cast failure
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,four,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+WARNING:  invalid input syntax for type integer: "four"
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: histogram elements null value
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,NULL,3,4}'::text
+    );
+WARNING:  histogram_bounds array cannot contain NULL values
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,2,3,4}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'correlation', 0.5::real);
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |         0.5 |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{1,3}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+WARNING:  id cannot accept most_common_elems stats, ignored
+WARNING:  id cannot accept most_common_elem_freqs stats, ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,two}'::text
+    );
+WARNING:  most_common_elems cannot be present if most_common_elem_freqs is missing
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
+    );
+WARNING:  most_common_elem_freqs cannot be present if most_common_elems is missing
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             | {one,three}       | {0.3,0.2,0.2,0.3,0}    |                      |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+WARNING:  id cannot accept elem_count_histogram stats, ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+WARNING:  elem_count_histogram array cannot contain NULL values
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+WARNING:  id cannot accept range_length_histogram stats, ignored
+WARNING:  id cannot accept range_empty_frac stats, ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+WARNING:  range_length_histogram cannot be present if range_empty_frac is missing
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real
+    );
+WARNING:  range_empty_frac cannot be present if range_length_histogram is missing
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
+(1 row)
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+WARNING:  id cannot accept range_bounds_histogram stats, ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
+ stats_export_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      |                        |                  | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+(1 row)
+
+-- warn: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
+    'histogram_bounds', '{1,2,3,4}'::text,
+    'correlation', 1.1::real,
+    'most_common_elems', '{3,1}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    'range_empty_frac', -0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+WARNING:  imported statistics must have a maximum of 5 slots but 6 given
+ pg_set_attribute_stats 
+------------------------
+ f
+(1 row)
+
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+   relname    | num_stats 
+--------------+-----------
+ is_odd       |         1
+ is_odd_clone |         1
+ test         |         5
+ test_clone   |         5
+(4 rows)
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 675c567617..bcd7c0720e 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,8 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc stats_export_import
+
 
 # ----------
 # Load huge amounts of data
diff --git a/src/test/regress/sql/stats_export_import.sql b/src/test/regress/sql/stats_export_import.sql
new file mode 100644
index 0000000000..ebdb58aba1
--- /dev/null
+++ b/src/test/regress/sql/stats_export_import.sql
@@ -0,0 +1,671 @@
+CREATE SCHEMA stats_export_import;
+
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+-- error: regclass not found
+SELECT pg_set_relation_stats('stats_export_import.nope'::regclass,
+                             150000::integer);
+
+-- error: all three params missing
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer);
+
+-- error: reltuples, relallvisible missing
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 4::integer);
+
+-- error: null value
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', NULL::real,
+                             'relallvisible', 4::integer);
+
+-- error: bad relpages type
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 17::integer,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_export_import.test'::regclass;
+
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    '0'::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    NULL::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    NULL::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    NULL::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', NULL::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', NULL::integer,
+    'n_distinct', 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', NULL::real);
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+SELECT stanullfrac, stawidth, stadistinct
+FROM pg_statistic
+WHERE starelid = 'stats_export_import.test'::regclass;
+
+-- warn: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_freqs', '{0.1,0.2,0.3}'::real[]
+    );
+
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{1,2,3}'::text
+    );
+
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.2,0.1}'::double precision[]
+    );
+
+-- warning: mcv cast failure
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,four,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: histogram elements null value
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,NULL,3,4}'::text
+    );
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,2,3,4}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'correlation', 0.5::real);
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: scalars can't have mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{1,3}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,two}'::text
+    );
+
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
+    );
+
+-- ok: mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- warn: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real
+    );
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
+    'histogram_bounds', '{1,2,3,4}'::text,
+    'correlation', 1.1::real,
+    'most_common_elems', '{3,1}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    'range_empty_frac', -0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+
+SELECT
+    format('SELECT pg_catalog.pg_set_attribute_stats( '
+            || '%L::regclass::oid, '
+            || '%L::name, '
+            || '%L::boolean, '
+            || '%L::integer, '
+            || '%L, %L::real, '
+            || '%L, %L::integer, '
+            || '%L, %L::real %s)',
+        'stats_export_import.' || s.tablename || '_clone',
+        s.attname,
+        s.inherited,
+        150000,
+        'null_frac', s.null_frac,
+        'avg_width', s.avg_width,
+        'n_distinct', s.n_distinct,
+        CASE
+            WHEN s.most_common_vals IS NULL THEN ''
+            ELSE format(', %L, %L::text, %L, %L::real[]',
+                        'most_common_vals', s.most_common_vals,
+                        'most_common_freqs', s.most_common_freqs)
+        END ||
+        CASE
+            WHEN s.histogram_bounds IS NULL THEN ''
+            ELSE format(', %L, %L::text',
+                        'histogram_bounds', s.histogram_bounds)
+        END ||
+        CASE
+            WHEN s.correlation IS NULL THEN ''
+            ELSE format(', %L, %L::real',
+                        'correlation', s.correlation)
+        END ||
+        CASE
+            WHEN s.most_common_elems IS NULL THEN ''
+            ELSE format(', %L, %L::text, %L, %L::real[]',
+                        'most_common_elems', s.most_common_elems,
+                        'most_common_elem_freqs', s.most_common_elem_freqs)
+        END ||
+        CASE
+            WHEN s.elem_count_histogram IS NULL THEN ''
+            ELSE format(', %L, %L::real[]',
+                        'elem_count_histogram', s.elem_count_histogram)
+        END ||
+        CASE
+            WHEN s.range_bounds_histogram IS NULL THEN ''
+            ELSE format(', %L, %L::text',
+                        'range_bounds_histogram', s.range_bounds_histogram)
+        END ||
+        CASE
+            WHEN s.range_empty_frac IS NULL THEN ''
+            ELSE format(', %L, %L::real, %L, %L::text',
+                        'range_empty_frac', s.range_empty_frac,
+                        'range_length_histogram', s.range_length_histogram)
+        END)
+FROM pg_catalog.pg_stats AS s
+WHERE s.schemaname = 'stats_export_import'
+AND s.tablename IN ('test', 'is_odd')
+\gexec
+
+-- restore ECHO to original value
+\set ECHO :orig_ECHO
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 1928de5762..f7116408ab 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29638,6 +29638,273 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
     in identifying the specific disk files associated with database objects.
    </para>
 
+   <table id="functions-admin-statsimport">
+    <title>Database Object Statistics Import Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_relation_stats</primary>
+        </indexterm>
+        <function>pg_set_relation_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>version</parameter> <type>integer</type>,
+         <literal>VARIADIC</literal> <parameter>stats</parameter> <type>"any"</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Updates the <structname>pg_class</structname> row for the specified
+        <parameter>relation</parameter>, values from the variadic list of
+        name-value pairs. Each parameter name corresponds to the same-named
+        attribute in <structname>pg_class</structname>:
+       </para>
+       <variablelist>
+        <varlistentry>
+         <term><literal>relpages</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>integer</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>reltuples</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>relalltuples</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>integer</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+       </variablelist>
+       <para>
+        Any other parameter names given must also have a value pair, but will emit
+        a warning and thereafter be ignored.
+       </para>
+       <para>
+        The <parameter>version</parameter> is meant to reflect the server version
+        number of the system where this data was generated, as that may in the
+        future change how the data is imported.
+       </para>
+       <para>
+        To avoid table bloat in <structname>pg_class</structname>, this change
+        is made with an in-place update, and therefore cannot be rolled back
+        through normal transaction processing.
+       </para>
+       <para>
+        This function mimics the behavior of <command>ANALYZE</command> in its
+        effects on the values in <structname>pg_class</structname>, except that
+        the values are supplied as parameters rather than derived from table
+        sampling.
+       </para>
+       <para>
+        The caller must either be the owner of the relation, or have superuser
+        privileges.
+       </para>
+       <para>
+        The parameters given are checked for type validity and must all be
+        NOT NULL.
+       </para>
+       <para>
+        The parameters will return true if stats were updated and false if not.
+       </para>
+       </entry>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_attribute_stats</primary>
+        </indexterm>
+        <function>pg_set_attribute_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>
+         , <parameter>attname</parameter> <type>name</type>
+         , <parameter>inherited</parameter> <type>boolean</type>
+         , <parameter>version</parameter> <type>integer</type>
+         , <literal>VARIADIC</literal> <parameter>stats</parameter> <type>"any"</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Replaces the <structname>pg_statistic</structname> row for the
+        <structname>pg_attribute</structname> row specified by
+        <parameter>relation</parameter>, <parameter>attname</parameter>
+        and <parameter>inherited</parameter>. The <parameter>version</parameter>
+        should be set to the server version number of the server where
+        the variadic <parameter>stats</parameter> originated, as it may in the
+        future affect how the stats are imported. Values from the variadic
+        list form name-value pairs. Each parameter name corresponds to the
+        same-named attribute in
+        <link linkend="view-pg-stats"><structname>pg_stats</structname></link>:
+       </para>
+       <variablelist>
+        <varlistentry>
+         <term><literal>null_frac</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>avg_width</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>integer</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>n_distinct</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_vals</literal></term>
+         <listitem>
+          <para>
+          <literal>most_common_vals</literal>
+           Must be followed by a parameter of type <type>text</type>. If this
+           parameter is specified then <literal>most_common_freqs</literal>
+           must also be specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_freqs</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of <type>real[]</type>, and can only
+           be specified if <literal>most_common_vals</literal> is also specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>histogram_bounds</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>correlation</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_elems</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>. If this
+           parameter is specified then <literal>most_common_elem_freqs</literal>
+           must also be specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_elem_freqs</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real[]</type>, and can
+           only be specified if <literal>most_common_elems</literal> is also
+           specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>elem_count_histogram</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real[]</type>.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>range_length_histogram</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>. If this
+           parameter is specified then <literal>range_empty_frac</literal>
+           must also be specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>range_empty_frac</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>, and can
+           only be specified if <literal>range_length_histogram</literal>
+           is also specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>range_bounds_histogram</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>
+          </para>
+         </listitem>
+        </varlistentry>
+       </variablelist>
+       <para>
+        Any other parameter names given must also have a value pair, but will emit
+        a warning and thereafter be ignored.
+       </para>
+       <para>
+        The parameters will return true if stats were updated and false if not.
+       </para>
+       <para>
+        This function mimics the behavior of <command>ANALYZE</command> in its
+        effects on the values in <structname>pg_statistic</structname>, except
+        that the values are supplied as parameters rather than derived from
+        table sampling.
+       </para>
+       <para>
+        The purpose of this function is to apply statistics values in an
+        upgrade situation that are "good enough" for system operation until
+        they are replaced by the next <command>ANALYZE</command>, usually via
+        <command>autovacuum</command>.
+       </para>
+       <para>
+        The caller must either be the owner of the relation, or have superuser
+        privileges.
+       </para>
+       </entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
    <table id="functions-admin-dblocation">
     <title>Database Object Location Functions</title>
     <tgroup cols="1">
-- 
2.44.0

v20-0002-Enable-dumping-of-table-index-stats-in-pg_dump.patchtext/x-patch; charset=US-ASCII; name=v20-0002-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From 1c207de0c4050820044e5df0bde214e25cc50b54 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH v20 2/2] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will generate a statement that
calls pg_set_relation_stats(), and it will generate a series of
statements that call pg_set_attribute_stats(), one per attribute. These
statements will restore the statistics of the current system onto the
destination system.

Statistics are not dumped when -schema-only is specified, except during
a binary upgrade.

As is the pattern with pg_dump options, this can be disabled with
--no-statistics.
---
 src/include/catalog/pg_proc.dat      |   3 +
 src/bin/pg_dump/pg_backup.h          |   2 +
 src/bin/pg_dump/pg_backup_archiver.c |   5 +
 src/bin/pg_dump/pg_dump.c            | 362 +++++++++++++++++++++++++++
 src/bin/pg_dump/pg_dump.h            |   8 +
 src/bin/pg_dump/pg_dump_sort.c       |   5 +
 src/bin/pg_dump/pg_dumpall.c         |   5 +
 src/bin/pg_dump/pg_restore.c         |   3 +
 doc/src/sgml/ref/pg_dump.sgml        |   9 +
 doc/src/sgml/ref/pg_restore.sgml     |  10 +
 10 files changed, 412 insertions(+)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 26f62c3651..4e71dc6227 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12178,6 +12178,9 @@
   proallargtypes => '{int8,pg_lsn,pg_lsn,oid,oid,oid,int2,int8,bool}',
   proargmodes => '{i,i,i,o,o,o,o,o,o}',
   proargnames => '{tli,start_lsn,end_lsn,relfilenode,reltablespace,reldatabase,relforknumber,relblocknumber,is_limit_block}',
+  prosrc => 'pg_wal_summary_contents' },
+{ oid => '8438',
+  descr => 'WAL summarizer state',
   proname => 'pg_get_wal_summarizer_state',
   provolatile => 'v', proparallel => 's',
   prorettype => 'record', proargtypes => '',
diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index fbf5f1c515..9f9b9f8724 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -112,6 +112,7 @@ typedef struct _restoreOptions
 	int			no_publications;	/* Skip publication entries */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
@@ -181,6 +182,7 @@ typedef struct _dumpOptions
 	int			no_security_labels;
 	int			no_publications;
 	int			no_subscriptions;
+	int			no_statistics;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index c6c101c118..62d2a50117 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2955,6 +2955,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's a stats dump, maybe ignore it */
+	if (ropt->no_statistics && strcmp(te->desc, "STATISTICS") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2984,6 +2988,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 242ebe807f..9e54b4297e 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -435,6 +435,7 @@ main(int argc, char **argv)
 		{"no-comments", no_argument, &dopt.no_comments, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -1151,6 +1152,7 @@ help(const char *progname)
 	printf(_("  --no-comments                do not dump comments\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -6687,6 +6689,42 @@ getFuncs(Archive *fout, int *numFuncs)
 	return finfo;
 }
 
+/*
+ * getRelationStatistics
+ *    register the statistics object as a dependent of the relation.
+ *
+ */
+static RelStatsInfo *
+getRelationStatistics(Archive *fout, DumpableObject *rel, char relkind)
+{
+	if ((relkind == RELKIND_RELATION) ||
+		(relkind == RELKIND_PARTITIONED_TABLE) ||
+		(relkind == RELKIND_INDEX) ||
+		(relkind == RELKIND_PARTITIONED_INDEX) ||
+		(relkind == RELKIND_MATVIEW))
+	{
+		RelStatsInfo	   *info = pg_malloc0(sizeof(RelStatsInfo));
+		DumpableObject	   *dobj = &info->dobj;
+
+		dobj->objType = DO_REL_STATS;
+		dobj->catId.tableoid = 0;
+		dobj->catId.oid = 0;
+		AssignDumpId(dobj);
+		dobj->dependencies = (DumpId *) pg_malloc(sizeof(DumpId));
+		dobj->dependencies[0] = rel->dumpId;
+		dobj->nDeps = 1;
+		dobj->allocDeps = 1;
+		dobj->components |= DUMP_COMPONENT_STATISTICS;
+		dobj->dump = rel->dump;
+		dobj->name = pg_strdup(rel->name);
+		dobj->namespace = rel->namespace;
+		info->relkind = relkind;
+
+		return info;
+	}
+	return NULL;
+}
+
 /*
  * getTables
  *	  read all the tables (no indexes) in the system catalogs,
@@ -7064,6 +7102,7 @@ getTables(Archive *fout, int *numTables)
 
 		/* Tables have data */
 		tblinfo[i].dobj.components |= DUMP_COMPONENT_DATA;
+		tblinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
 
 		/* Mark whether table has an ACL */
 		if (!PQgetisnull(res, i, i_relacl))
@@ -7112,6 +7151,8 @@ getTables(Archive *fout, int *numTables)
 				}
 			}
 		}
+		if (tblinfo[i].interesting)
+			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relkind);
 	}
 
 	if (query->len != 0)
@@ -7556,11 +7597,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 		for (int c = 0; c < numinds; c++, j++)
 		{
 			char		contype;
+			char		indexkind;
+			RelStatsInfo   *relstats;
 
 			indxinfo[j].dobj.objType = DO_INDEX;
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -7583,7 +7627,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			{
 				NULL, NULL
 			};
+
+			if (indxinfo[j].parentidx == 0)
+				indexkind = RELKIND_INDEX;
+			else
+				indexkind = RELKIND_PARTITIONED_INDEX;
+
 			contype = *(PQgetvalue(res, j, i_contype));
+			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, indexkind);
 
 			if (contype == 'p' || contype == 'u' || contype == 'x')
 			{
@@ -7617,6 +7668,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				constrinfo->separate = true;
 
 				indxinfo[j].indexconstraint = constrinfo->dobj.dumpId;
+				if (relstats != NULL)
+					addObjectDependency(&relstats->dobj, constrinfo->dobj.dumpId);
 			}
 			else
 			{
@@ -10327,6 +10380,300 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * Tabular description of the parameters to pg_set_relation_stats()
+ * required, param_name, param_type
+ */
+static const char *rel_stats_arginfo[][3] = {
+	{"t", "relation", "regclass"},
+	{"t", "version", "integer"},
+	{"f", "relpages", "integer"},
+	{"f", "reltuples", "real"},
+	{"f", "relallvisible", "integer"},
+};
+
+/*
+ * Tabular description of the parameters to pg_set_attribute_stats()
+ * required, param_name, param_type
+ */
+static const char *att_stats_arginfo[][3] = {
+	{"t", "relation", "regclass"},
+	{"t", "attname", "name"},
+	{"t", "inherited", "boolean"},
+	{"t", "version", "integer"},
+	{"f", "null_frac", "float4"},
+	{"f", "avg_width", "integer"},
+	{"f", "n_distinct", "float4"},
+	{"f", "most_common_vals", "text"},
+	{"f", "most_common_freqs", "float4[]"},
+	{"f", "histogram_bounds", "text"},
+	{"f", "correlation", "float4"},
+	{"f", "most_common_elems", "text"},
+	{"f", "most_common_elem_freqs", "float4[]"},
+	{"f", "elem_count_histogram", "float4[]"},
+	{"f", "range_length_histogram", "text"},
+	{"f", "range_empty_frac", "float4"},
+	{"f", "range_bounds_histogram", "text"},
+};
+
+/*
+ * getRelStatsExportQuery --
+ *
+ * Generate a query that will fetch all relation (e.g. pg_class)
+ * stats for a given relation.
+ */
+static void
+getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "current_setting('server_version_num') AS version, "
+						 "c.relpages, c.reltuples, c.relallvisible "
+						 "FROM pg_class c "
+						 "JOIN pg_namespace n "
+						 "ON n.oid = c.relnamespace "
+						 "WHERE n.nspname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND c.relname = ");
+	appendStringLiteralAH(query, relname, fout);
+}
+
+/*
+ * getAttStatsExportQuery --
+ *
+ * Generate a query that will fetch all attribute (e.g. pg_statistic)
+ * stats for a given relation.
+ */
+static void
+getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "s.attname,"
+						 "s.inherited,"
+						 "current_setting('server_version_num') AS version, "
+						 "s.null_frac,"
+						 "s.avg_width,"
+						 "s.n_distinct,"
+						 "s.most_common_vals,"
+						 "s.most_common_freqs,"
+						 "s.histogram_bounds,"
+						 "s.correlation,"
+						 "s.most_common_elems,"
+						 "s.most_common_elem_freqs,"
+						 "s.elem_count_histogram,");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 "s.range_length_histogram,"
+							 "s.range_empty_frac,"
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(query,
+							 "NULL AS range_length_histogram,"
+							 "NULL AS range_empty_frac,"
+							 "NULL AS range_bounds_histogram ");
+
+	appendPQExpBufferStr(query,
+						 "FROM pg_stats s "
+						 "JOIN pg_namespace n "
+						 "ON n.nspname = s.schemaname "
+						 "JOIN pg_class c "
+						 "ON c.relname = s.tablename "
+						 "AND c.relnamespace = n.oid "
+						 "WHERE s.schemaname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND s.tablename = ");
+	appendStringLiteralAH(query, relname, fout);
+	appendPQExpBufferStr(query, " ORDER BY s.attname, s.inherited");
+}
+
+
+/*
+ * appendNamedArgument --
+ *
+ * Convenience routine for constructing parameters of the form:
+ * 'paraname', 'value'::type
+ */
+static void
+appendNamedArgument(PQExpBuffer out, Archive *fout,
+					const char *argname, bool positional,
+					const char *argval, const char *argtype)
+{
+	appendPQExpBufferStr(out, "\t");
+
+	if (!positional)
+	{
+		appendStringLiteralAH(out, argname, fout);
+		appendPQExpBufferStr(out, ", ");
+	}
+
+	appendStringLiteralAH(out, argval, fout);
+	appendPQExpBuffer(out, "::%s", argtype);
+}
+
+/*
+ * appendRelStatsImport --
+ *
+ * Append a formatted pg_set_relation_stats statement.
+ */
+static void
+appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	const char *sep = "";
+
+	if (PQntuples(res) == 0)
+		return;
+
+	appendPQExpBufferStr(out, "SELECT pg_catalog.pg_set_relation_stats(\n");
+
+	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
+	{
+		bool		positional = (rel_stats_arginfo[argno][0][0] == 't');
+		const char *argname = rel_stats_arginfo[argno][1];
+		const char *argtype = rel_stats_arginfo[argno][2];
+		int			fieldno = PQfnumber(res, argname);
+
+		if (fieldno < 0)
+			pg_fatal("relation stats export query missing field '%s'",
+					 argname);
+
+		if (PQgetisnull(res, 0, fieldno))
+		{
+			if (!positional)
+				pg_fatal("relation stats export query unexpected NULL in '%s'",
+						 argname);
+			else
+				continue;
+		}
+
+		appendPQExpBufferStr(out, sep);
+		appendNamedArgument(out, fout, argname, positional,
+							PQgetvalue(res, 0, fieldno), argtype);
+
+		sep = ",\n";
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+/*
+ * appendAttStatsImport --
+ *
+ * Append a series of formatted pg_set_attribute_stats statements.
+ */
+static void
+appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	{
+		const char *sep = "";
+
+		appendPQExpBufferStr(out, "SELECT pg_catalog.pg_set_attribute_stats(\n");
+		for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
+		{
+			bool		positional = (att_stats_arginfo[argno][0][0] == 't');
+			const char *argname = att_stats_arginfo[argno][1];
+			const char *argtype = att_stats_arginfo[argno][2];
+			int			fieldno = PQfnumber(res, argname);
+
+			if (fieldno < 0)
+				pg_fatal("attribute stats export query missing field '%s'",
+						 argname);
+
+			if (PQgetisnull(res, rownum, fieldno))
+			{
+				if (positional)
+					pg_fatal("attribute stats export query unexpected NULL in '%s'",
+							 argname);
+				else
+					continue;
+			}
+
+			appendPQExpBufferStr(out, sep);
+			appendNamedArgument(out, fout, argname, positional,
+								PQgetvalue(res, rownum, fieldno), argtype);
+			sep = ",\n";
+		}
+		appendPQExpBufferStr(out, "\n);\n");
+	}
+}
+
+/*
+ * get statistics dump section, which depends on the parent object type
+ *
+ * objects created in SECTION_PRE_DATA have stats in SECTION_DATA
+ * objects created in SECTION_POST_DATA have stats in SECTION_POST_DATA
+ */
+static teSection
+statisticsDumpSection(const RelStatsInfo *rsinfo)
+{
+
+	if ((rsinfo->relkind == RELKIND_MATVIEW) ||
+		(rsinfo->relkind == RELKIND_INDEX) ||
+		(rsinfo->relkind == RELKIND_PARTITIONED_INDEX))
+			return SECTION_POST_DATA;
+
+	return SECTION_DATA;
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	PGresult	   *res;
+	PQExpBuffer		query;
+	PQExpBuffer		out;
+	PQExpBuffer		tag;
+	DumpableObject *dobj = (DumpableObject *) &rsinfo->dobj;
+
+	/* do nothing, if --no-statistics is supplied */
+	if (fout->dopt->no_statistics)
+		return;
+
+	/* likewise, no stats on schema-only, unless in a binary upgrade */
+	if (fout->dopt->schemaOnly && !fout->dopt->binary_upgrade)
+		return;
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", "STATISTICS DATA", fmtId(dobj->name));
+
+	query = createPQExpBuffer();
+	out = createPQExpBuffer();
+
+	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendRelStatsImport(out, fout, res);
+	PQclear(res);
+
+	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendAttStatsImport(out, fout, res);
+	PQclear(res);
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATISTICS DATA",
+							  .section = statisticsDumpSection(rsinfo),
+							  .createStmt = out->data,
+							  .deps = dobj->dependencies,
+							  .nDeps = dobj->nDeps));
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(out);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -10775,6 +11122,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_SUBSCRIPTION_REL:
 			dumpSubscriptionTable(fout, (const SubRelInfo *) dobj);
 			break;
+		case DO_REL_STATS:
+			dumpRelationStats(fout, (const RelStatsInfo *) dobj);
+			break;
 		case DO_PRE_DATA_BOUNDARY:
 		case DO_POST_DATA_BOUNDARY:
 			/* never dumped, nothing to do */
@@ -17085,6 +17435,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 		free(indstatvalsarray);
 	}
 
+	/* Comments and stats share same .dep */
+
 	/* Dump Index Comments */
 	if (indxinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
 		dumpComment(fout, "INDEX", qindxname,
@@ -18782,6 +19134,16 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 				/* must come after the pre-data boundary */
 				addObjectDependency(dobj, preDataBound->dumpId);
 				break;
+			case DO_REL_STATS:
+				/* stats section varies by parent object type, DATA or POST */
+				if (statisticsDumpSection((RelStatsInfo *) dobj) == SECTION_DATA)
+				{
+					addObjectDependency(dobj, preDataBound->dumpId);
+					addObjectDependency(postDataBound, dobj->dumpId);
+				}
+				else
+					addObjectDependency(dobj, postDataBound->dumpId);
+				break;
 		}
 	}
 }
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 2a7c5873a0..86f984d579 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -84,6 +84,7 @@ typedef enum
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION,
 	DO_SUBSCRIPTION_REL,		/* see note for SubRelInfo */
+	DO_REL_STATS,
 } DumpableObjectType;
 
 /*
@@ -101,6 +102,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
@@ -421,6 +423,12 @@ typedef struct _indexAttachInfo
 	IndxInfo   *partitionIdx;	/* link to index on partition */
 } IndexAttachInfo;
 
+typedef struct _relStatsInfo
+{
+	DumpableObject	dobj;
+	char			relkind;		/* 'r', 'v', 'c', etc */
+} RelStatsInfo;
+
 typedef struct _statsExtInfo
 {
 	DumpableObject dobj;
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 4cb754caa5..cfe277f5ce 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -1490,6 +1490,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "POST-DATA BOUNDARY  (ID %d)",
 					 obj->dumpId);
 			return;
+		case DO_REL_STATS:
+			snprintf(buf, bufsize,
+					 "RELATION STATISTICS FOR %s  (ID %d OID %u)",
+					 obj->name, obj->dumpId, obj->catId.oid);
+			return;
 	}
 	/* shouldn't get here */
 	snprintf(buf, bufsize,
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 73337f3392..c4eaff3063 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -103,6 +103,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -172,6 +173,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -451,6 +453,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -666,6 +670,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 5ea78cf7cc..33ae4f92bc 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -75,6 +75,7 @@ main(int argc, char **argv)
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
 	static int	no_subscriptions = 0;
+	static int  no_statistics = 0;
 	static int	strict_names = 0;
 
 	struct option cmdopts[] = {
@@ -127,6 +128,7 @@ main(int argc, char **argv)
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
+		{"no-statistics", no_argument, &no_statistics, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -370,6 +372,7 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index 08d775379f..c86793b7db 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -1081,6 +1081,15 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index 2e3ba80258..845f739a45 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -723,6 +723,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore statistics, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
-- 
2.44.0

#165Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#164)
3 attachment(s)
Re: Statistics Import and Export

Next up for question is how to handle --statistics-only or an equivalent.
The option would be mutually exclusive with --schema-only and --data-only,
and it would be mildly incongruous if it didn't have a short option like
the others, so I'm suggested -P for Probablity / Percentile / ρ:
correlation / etc.

One wrinkle with having three mutually exclusive options instead of two is
that the existing code was able to assume that one of the options being
true meant that we could bail out of certain dumpXYZ() functions, and now
those tests have to compare against two, which makes me think we should add
three new DumpOptions that are the non-exclusive positives (yesSchema,
yesData, yesStats) and set those in addition to the schemaOnly, dataOnly,
and statsOnly flags. Thoughts?

v21 attached.

0001 is the same.

0002 is a preparatory change to pg_dump introducing
DumpOption/RestoreOption variables dumpSchema and dumpData. The current
code makes heavy use of the fact that schemaOnly and dataOnly are mutually
exclusive and logically opposite. That will not be the case when
statisticsOnly is introduced, so I decided to add the new variables whose
value is entirely derivative of the existing command flags, but resolves
the complexities of those interactions in one spot, as those complexities
are about to jump with the new options.

0003 is the statistics changes to pg_dump, adding the options -X /
--statistics-only, and the derivative boolean statisticsOnly. The -P option
is already used by pg_restore, so instead I chose -X because of the passing
resemblance to Chi as in the chi-square statistics test makes it vaguely
statistics-ish. If someone has a better letter, I'm listening.

With that change, people should be able to use pg_dump -X --table=foo to
dump existing stats for a table and its dependent indexes, and then tweak
those calls to do tuning work. Have fun with it. If this becomes a common
use-case then it may make sense to get functions to fetch
relation/attribute stats for a given relation, either as a formed SQL
statement or as the parameter values.

Attachments:

v21-0001-Create-pg_set_relation_stats-pg_set_attribute_st.patchtext/x-patch; charset=US-ASCII; name=v21-0001-Create-pg_set_relation_stats-pg_set_attribute_st.patchDownload
From efa23849aeb385467aa10cff060149b7a62975f2 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 21 Mar 2024 18:04:54 -0400
Subject: [PATCH v21 1/3] Create pg_set_relation_stats, pg_set_attribute_stats.

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

Both functions take an oid to identify the target relation that will
receive the statistics. There is nothing requiring that relation to be
the same one as the one exported, though the statistics supplied have
to make sense in the context of the new relation.

Both functions take a variadic list of statistic parameters name-value
pairs.

Each name given for pg_set_relation_stats must correspond to a
statistics attribute in pg_class, and the paired value must match the
datatype of said attribute in pg_class.

Each name given for pg_set_attribute_stats must correspond to a
statistics attribute in pg_stats, and the paird value must match the
datatype of said attribute in pg_stats, except for attributes of type
ANYARRAY, in which case a TEXT value is required.

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

These functions 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               |   19 +
 src/include/statistics/statistics.h           |    2 +
 src/backend/statistics/Makefile               |    3 +-
 src/backend/statistics/meson.build            |    1 +
 src/backend/statistics/statistics.c           | 1271 +++++++++++++++++
 .../regress/expected/stats_export_import.out  |  853 +++++++++++
 src/test/regress/parallel_schedule            |    3 +-
 src/test/regress/sql/stats_export_import.sql  |  671 +++++++++
 doc/src/sgml/func.sgml                        |  267 ++++
 9 files changed, 3088 insertions(+), 2 deletions(-)
 create mode 100644 src/backend/statistics/statistics.c
 create mode 100644 src/test/regress/expected/stats_export_import.out
 create mode 100644 src/test/regress/sql/stats_export_import.sql

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 134e3b22fd..4e71dc6227 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12195,4 +12195,23 @@
   proargtypes => 'int2',
   prosrc => 'gist_stratnum_identity' },
 
+# Statistics Import
+{ oid => '8048',
+  descr => 'set statistics on relation',
+  proname => 'pg_set_relation_stats', provariadic => 'any',
+  proisstrict => 't', provolatile => 'v',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'regclass int4 any',
+  proargnames => '{relation,version,stats}',
+  proargmodes => '{i,i,v}',
+  prosrc => 'pg_set_relation_stats' },
+{ oid => '8049',
+  descr => 'set statistics on attribute',
+  proname => 'pg_set_attribute_stats', provariadic => 'any',
+  proisstrict => 't', provolatile => 'v',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'regclass name bool int4 any',
+  proargnames => '{relation,attname,inherited,version,stats}',
+  proargmodes => '{i,i,i,i,v}',
+  prosrc => 'pg_set_attribute_stats' },
 ]
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 7f2bf18716..1dddf96576 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -127,4 +127,6 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												int nclauses);
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
+extern Datum pg_set_relation_stats(PG_FUNCTION_ARGS);
+extern Datum pg_set_attribute_stats(PG_FUNCTION_ARGS);
 #endif							/* STATISTICS_H */
diff --git a/src/backend/statistics/Makefile b/src/backend/statistics/Makefile
index 89cf8c2797..e4f8ab7c4f 100644
--- a/src/backend/statistics/Makefile
+++ b/src/backend/statistics/Makefile
@@ -16,6 +16,7 @@ OBJS = \
 	dependencies.o \
 	extended_stats.o \
 	mcv.o \
-	mvdistinct.o
+	mvdistinct.o \
+	statistics.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/statistics/meson.build b/src/backend/statistics/meson.build
index 73b29a3d50..331e82c776 100644
--- a/src/backend/statistics/meson.build
+++ b/src/backend/statistics/meson.build
@@ -5,4 +5,5 @@ backend_sources += files(
   'extended_stats.c',
   'mcv.c',
   'mvdistinct.c',
+  'statistics.c',
 )
diff --git a/src/backend/statistics/statistics.c b/src/backend/statistics/statistics.c
new file mode 100644
index 0000000000..e092eb3dc3
--- /dev/null
+++ b/src/backend/statistics/statistics.c
@@ -0,0 +1,1271 @@
+/*-------------------------------------------------------------------------
+ * statistics.c
+ *
+ *	  POSTGRES statistics import
+ *
+ * Code supporting the direct importation of relation statistics, similar to
+ * what is done by the ANALYZE command.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *       src/backend/statistics/statistics.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_collation.h"
+#include "catalog/pg_database.h"
+#include "catalog/pg_operator.h"
+#include "catalog/pg_type.h"
+#include "fmgr.h"
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "nodes/nodeFuncs.h"
+#include "statistics/statistics.h"
+#include "utils/acl.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/float.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/rangetypes.h"
+#include "utils/syscache.h"
+#include "utils/typcache.h"
+
+/*
+ * A role has privileges to set statistics on the relation if any of the
+ * following are true:
+ *   - the role owns the current database and the relation is not shared
+ *   - the role has the MAINTAIN privilege on the relation
+ *
+ */
+static bool
+can_modify_relation(Relation rel)
+{
+	Oid			relid = RelationGetRelid(rel);
+	Form_pg_class reltuple = rel->rd_rel;
+
+	return ((object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId())
+			 && !reltuple->relisshared) ||
+			pg_class_aclcheck(relid, GetUserId(), ACL_MAINTAIN) == ACLCHECK_OK);
+}
+
+/*
+ * Set statistics for a given pg_class entry.
+ *
+ * This does an in-place (i.e. non-transactional) update of pg_class, just as
+ * is done in ANALYZE.
+ *
+ */
+Datum
+pg_set_relation_stats(PG_FUNCTION_ARGS)
+{
+	Oid			relid = PG_GETARG_OID(0);
+	int			version = PG_GETARG_INT32(1);
+
+	/* indexes of where we found required stats */
+	int			i_relpages = 0;
+	int			i_reltuples = 0;
+	int			i_relallvisible = 0;
+
+	/* build argument values to build the object */
+	Datum	   *args;
+	bool	   *nulls;			/* placeholder, because strict */
+	Oid		   *types;
+	int			nargs;
+
+	/* Minimum version supported */
+	if (version <= 90200)
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Cannot export statistics prior to version 9.2")));
+
+	nargs = extract_variadic_args(fcinfo, 2, true, &args, &types, &nulls);
+
+	/* if the pairs aren't pairs, something is malformed */
+	if (nargs % 2 == 1)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("stats parameters must be in name-value pairs")));
+		PG_RETURN_BOOL(false);
+	}
+
+	/* loop through args, matching params to their arg indexes */
+	for (int i = 0; i < nargs; i += 2)
+	{
+		char	   *statname;
+		int			argidx = i + 1;
+
+		if (types[i] != TEXTOID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("stat names must be of type text")));
+			continue;
+		}
+		statname = TextDatumGetCString(args[i]);
+
+		/*
+		 * match each named parameter to the index of the value that follows
+		 * it
+		 */
+		if (strcmp(statname, "relpages") == 0)
+			i_relpages = argidx;
+		else if (strcmp(statname, "reltuples") == 0)
+			i_reltuples = argidx;
+		else if (strcmp(statname, "relallvisible") == 0)
+			i_relallvisible = argidx;
+		else
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("unknown stat naame '%s', skipping", statname)));
+
+		pfree(statname);
+	}
+
+	/*
+	 * Ensure that we got all required parameters, and they are of the correct
+	 * type.
+	 */
+	if (i_relpages == 0)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "relpages")));
+		PG_RETURN_BOOL(false);
+	}
+	else if (types[i_relpages] != INT4OID)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relpages must be of type integer")));
+		PG_RETURN_BOOL(false);
+	}
+	else if (i_reltuples == 0)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "reltuples")));
+		PG_RETURN_BOOL(false);
+	}
+	else if (types[i_reltuples] != FLOAT4OID)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("reltuples must be of type real")));
+		PG_RETURN_BOOL(false);
+	}
+	else if (types[i_relallvisible] != INT4OID)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "relallvisible")));
+		PG_RETURN_BOOL(false);
+	}
+	else if (i_relallvisible == -1)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relallvisible must be of type integer")));
+		PG_RETURN_BOOL(false);
+	}
+	else
+	{
+		/*
+		 * Open the relation, getting ShareUpdateExclusiveLock to ensure that
+		 * no other stat-setting operation can run on it concurrently.
+		 */
+		Relation	rel;
+		HeapTuple	ctup;
+		Form_pg_class pgcform;
+		int			relpages;
+		float4		reltuples;
+		int			relallvisible;
+
+		rel = table_open(RelationRelationId, ShareUpdateExclusiveLock);
+
+		if (!can_modify_relation(rel))
+		{
+			table_close(rel, NoLock);
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("must be owner to modify relation stats")));
+			PG_RETURN_BOOL(false);
+		}
+
+		relpages = DatumGetInt32(args[i_relpages]);
+		reltuples = DatumGetFloat4(args[i_reltuples]);
+		relallvisible = DatumGetInt32(args[i_relallvisible]);
+
+		ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+		if (!HeapTupleIsValid(ctup))
+			ereport(ERROR,
+					(errcode(ERRCODE_OBJECT_IN_USE),
+					 errmsg("pg_class entry for relid %u not found", relid)));
+
+		pgcform = (Form_pg_class) GETSTRUCT(ctup);
+		/* Only update pg_class if there is a meaningful change */
+		if ((pgcform->reltuples != reltuples)
+			|| (pgcform->relpages != relpages)
+			|| (pgcform->relallvisible != relallvisible))
+		{
+			pgcform->relpages = relpages;
+			pgcform->reltuples = reltuples;
+			pgcform->relallvisible = relallvisible;
+
+			heap_inplace_update(rel, ctup);
+		}
+
+		table_close(rel, NoLock);
+		PG_RETURN_BOOL(true);
+	}
+
+	PG_RETURN_BOOL(false);
+}
+
+/*
+ * Test if the type is a scalar for MCELEM purposes
+ */
+static bool
+type_is_scalar(Oid typid)
+{
+	HeapTuple	tp;
+	bool		result = false;
+
+	tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+	if (HeapTupleIsValid(tp))
+	{
+		Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+
+		result = (!OidIsValid(typtup->typanalyze));
+		ReleaseSysCache(tp);
+	}
+	return result;
+}
+
+/*
+ * If this relation is an index and that index has expressions in it, and
+ * the attnum specified is known to be an expression, then we must walk
+ * the list attributes up to the specified attnum to get the right
+ * expression.
+ */
+static Node *
+get_attr_expr(Relation rel, int attnum)
+{
+	if ((rel->rd_rel->relkind == RELKIND_INDEX
+		 || (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+		&& (rel->rd_indexprs != NIL)
+		&& (rel->rd_index->indkey.values[attnum - 1] == 0))
+	{
+		ListCell   *indexpr_item = list_head(rel->rd_indexprs);
+
+		for (int i = 0; i < attnum - 1; i++)
+			if (rel->rd_index->indkey.values[i] == 0)
+				indexpr_item = lnext(rel->rd_indexprs, indexpr_item);
+
+		if (indexpr_item == NULL)	/* shouldn't happen */
+			elog(ERROR, "too few entries in indexprs list");
+
+		return (Node *) lfirst(indexpr_item);
+	}
+	return NULL;
+}
+
+/*
+ * Fetch datatype information, this is needed to derive the proper staopN
+ * and stacollN values.
+ *
+ */
+static TypeCacheEntry *
+get_attr_stat_type(Relation rel, Name attname,
+				   int16 *attnum, int32 *typmod, Oid *typcoll)
+{
+	Oid			relid = RelationGetRelid(rel);
+	Form_pg_attribute attr;
+	HeapTuple	atup;
+	Oid			typid;
+	Node	   *expr;
+
+	atup = SearchSysCache2(ATTNAME, ObjectIdGetDatum(relid),
+						   NameGetDatum(attname));
+
+	/* Attribute not found */
+	if (!HeapTupleIsValid(atup))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s has no attname %s",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+		return NULL;
+	}
+
+	attr = (Form_pg_attribute) GETSTRUCT(atup);
+	if (attr->attisdropped)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s attname %s is dropped",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+		return NULL;
+	}
+	*attnum = attr->attnum;
+
+	expr = get_attr_expr(rel, attr->attnum);
+
+	if (expr == NULL)
+	{
+		/* regular attribute */
+		typid = attr->atttypid;
+		*typmod = attr->atttypmod;
+		*typcoll = attr->attcollation;
+	}
+	else
+	{
+		typid = exprType(expr);
+		*typmod = exprTypmod(expr);
+
+		/*
+		 * If a collation has been specified for the index column, use that in
+		 * preference to anything else; but if not, fall back to whatever we
+		 * can get from the expression.
+		 */
+		if (OidIsValid(attr->attcollation))
+			*typcoll = attr->attcollation;
+		else
+			*typcoll = exprCollation(expr);
+	}
+	ReleaseSysCache(atup);
+
+	/* if it's a multirange, step down to the range type */
+	if (type_is_multirange(typid))
+		typid = get_multirange_range(typid);
+
+	return lookup_type_cache(typid,
+							 TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR);
+}
+
+/*
+ * Perform the cast of a known TextDatum into the type specified.
+ *
+ * If no errors are found, ok is set to true. Otherwise, set ok
+ * to false, capture the error found, and re-throw at warning level.
+ */
+static Datum
+cast_stavalues(FmgrInfo *flinfo, Datum d, Oid typid, int32 typmod, bool *ok)
+{
+	LOCAL_FCINFO(fcinfo, 8);
+	char	   *s;
+	Datum		result;
+	ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+	escontext.details_wanted = true;
+
+	s = TextDatumGetCString(d);
+
+	InitFunctionCallInfoData(*fcinfo, flinfo, 3, InvalidOid,
+							 (Node *) &escontext, NULL);
+
+	fcinfo->args[0].value = CStringGetDatum(s);
+	fcinfo->args[0].isnull = false;
+	fcinfo->args[1].value = ObjectIdGetDatum(typid);
+	fcinfo->args[1].isnull = false;
+	fcinfo->args[2].value = Int32GetDatum(typmod);
+	fcinfo->args[2].isnull = false;
+
+	result = FunctionCallInvoke(fcinfo);
+
+	if (SOFT_ERROR_OCCURRED(&escontext))
+	{
+		escontext.error_data->elevel = WARNING;
+		ThrowErrorData(escontext.error_data);
+		*ok = false;
+	}
+	else
+		*ok = true;
+
+	pfree(s);
+
+	return result;
+}
+
+
+/*
+ * Check array for any NULLs, and optionally for one-dimensionality.
+ *
+ * Report any failures as warnings.
+ */
+static bool
+array_check(Datum datum, int one_dim, const char *statname)
+{
+	ArrayType  *arr = DatumGetArrayTypeP(datum);
+	int16		elmlen;
+	char		elmalign;
+	bool		elembyval;
+	Datum	   *values;
+	bool	   *nulls;
+	int			nelems;
+
+	if (one_dim && (arr->ndim != 1))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be a multidimensional array", statname)));
+		return false;
+	}
+
+	get_typlenbyvalalign(ARR_ELEMTYPE(arr), &elmlen, &elembyval, &elmalign);
+
+	deconstruct_array(arr, ARR_ELEMTYPE(arr), elmlen, elembyval, elmalign,
+					  &values, &nulls, &nelems);
+
+	for (int i = 0; i < nelems; i++)
+		if (nulls[i])
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s array cannot contain NULL values", statname)));
+			return false;
+		}
+	return true;
+}
+
+/*
+ * Update the pg_statistic record.
+ */
+static void
+update_pg_statistic(Datum values[], bool nulls[])
+{
+	Relation	sd = table_open(StatisticRelationId, RowExclusiveLock);
+	CatalogIndexState indstate = CatalogOpenIndexes(sd);
+	HeapTuple	oldtup;
+	HeapTuple	stup;
+
+	/* Is there already a pg_statistic tuple for this attribute? */
+	oldtup = SearchSysCache3(STATRELATTINH,
+							 values[Anum_pg_statistic_starelid - 1],
+							 values[Anum_pg_statistic_staattnum - 1],
+							 values[Anum_pg_statistic_stainherit - 1]);
+
+	if (HeapTupleIsValid(oldtup))
+	{
+		/* Yes, replace it */
+		bool		replaces[Natts_pg_statistic];
+
+		for (int i = 0; i < Natts_pg_statistic; i++)
+			replaces[i] = true;
+
+		stup = heap_modify_tuple(oldtup, RelationGetDescr(sd),
+								 values, nulls, replaces);
+		ReleaseSysCache(oldtup);
+		CatalogTupleUpdateWithInfo(sd, &stup->t_self, stup, indstate);
+	}
+	else
+	{
+		/* No, insert new tuple */
+		stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
+		CatalogTupleInsertWithInfo(sd, stup, indstate);
+	}
+
+	heap_freetuple(stup);
+	CatalogCloseIndexes(indstate);
+	table_close(sd, NoLock);
+}
+
+/*
+ * Import statistics for a given relation attribute.
+ *
+ * This will insert/replace a row in pg_statistic for the given combinarion
+ * of relation, attribute, and inherited flag.
+ *
+ * The version parameter is for future use in the events as future versions
+ * may change them meaning of certain parameters.
+ *
+ * The variadic parameters all represent name-value pairs, with the names
+ * corresponding to attributes in pg_stats. Unkown names will generate a
+ * warning.
+ *
+ * Parameters null_frac, avg_width, and n_distinct are required because
+ * those attributes have no default value in pg_statistic.
+ *
+ * The remaining parameters all belong to a specific stakind, and all are
+ * optional. Some stakinds have multiple parameters, and in those cases
+ * both parameters must be specified if one of them is, otherwise a
+ * warning is generated but the rest of the stats may still be imported.
+ *
+ * If there is no attribute with a matching attname in the relation, the
+ * function will raise a warning and return false.
+ *
+ * Parameters corresponding to ANYARRAY columns are instead passed in as text
+ * values, which is a valid input string for an array of the type or element
+ * type of the attribute.
+ */
+Datum
+pg_set_attribute_stats(PG_FUNCTION_ARGS)
+{
+	Oid			relid = PG_GETARG_OID(0);
+	Name		attname = PG_GETARG_NAME(1);
+	bool		inherited = PG_GETARG_BOOL(2);
+	int			version = PG_GETARG_INT32(3);
+
+	/* build argument values to build the object */
+	Datum	   *args;
+	bool	   *argnulls;		/* placeholder, because strict */
+	Oid		   *types;
+	int			nargs = extract_variadic_args(fcinfo, 4, true,
+											  &args, &types, &argnulls);
+
+	Datum		values[Natts_pg_statistic] = {0};
+	bool		nulls[Natts_pg_statistic] = {false};
+
+	/*
+	 * argument indexes for each known statistic
+	 *
+	 * 0 = not found, -1 = error, n > 0 = found
+	 */
+
+	/* parameters that are required get indexes */
+	int			i_null_frac = 0;
+	int			i_avg_width = 0;
+	int			i_n_distinct = 0;
+
+	/* stakind stats are optional */
+	int			i_mc_vals = 0;
+	int			i_mc_freqs = 0;
+	int			i_hist_bounds = 0;
+	int			i_correlation = 0;
+	int			i_mc_elems = 0;
+	int			i_mc_elem_freqs = 0;
+	int			i_elem_count_hist = 0;
+	int			i_range_length_hist = 0;
+	int			i_range_empty_frac = 0;
+	int			i_range_bounds_hist = 0;
+
+	Relation	rel;
+
+	TypeCacheEntry *typcache;
+	TypeCacheEntry *elemtypcache = NULL;
+
+	int16		attnum;
+	int32		typmod;
+	Oid			typcoll;
+
+	FmgrInfo	finfo;
+
+	int			stakind_count;
+
+	int			k = 0;
+
+	if (version <= 90200)
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Cannot export statistics prior to version 9.2")));
+
+	/* if the pairs aren't pairs, something is malformed */
+	if (nargs % 2 == 1)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("stats parameters must be in name-value pairs")));
+		PG_RETURN_BOOL(false);
+	}
+
+	for (int i = 0; i < nargs; i += 2)
+	{
+		char	   *statname;
+		int			argidx = i + 1;
+
+		if (types[i] != TEXTOID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("stat names must be of type text")));
+			break;
+		}
+		statname = TextDatumGetCString(args[i]);
+
+		if (strcmp(statname, "null_frac") == 0)
+			i_null_frac = argidx;
+		else if (strcmp(statname, "avg_width") == 0)
+			i_avg_width = argidx;
+		else if (strcmp(statname, "n_distinct") == 0)
+			i_n_distinct = argidx;
+		else if (strcmp(statname, "most_common_vals") == 0)
+			i_mc_vals = argidx;
+		else if (strcmp(statname, "most_common_freqs") == 0)
+			i_mc_freqs = argidx;
+		else if (strcmp(statname, "histogram_bounds") == 0)
+			i_hist_bounds = argidx;
+		else if (strcmp(statname, "correlation") == 0)
+			i_correlation = argidx;
+		else if (strcmp(statname, "most_common_elems") == 0)
+			i_mc_elems = argidx;
+		else if (strcmp(statname, "most_common_elem_freqs") == 0)
+			i_mc_elem_freqs = argidx;
+		else if (strcmp(statname, "elem_count_histogram") == 0)
+			i_elem_count_hist = argidx;
+		else if (strcmp(statname, "range_length_histogram") == 0)
+			i_range_length_hist = argidx;
+		else if (strcmp(statname, "range_empty_frac") == 0)
+			i_range_empty_frac = argidx;
+		else if (strcmp(statname, "range_bounds_histogram") == 0)
+			i_range_bounds_hist = argidx;
+		else
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("unknown stat naame '%s', skipping", statname)));
+
+		pfree(statname);
+	}
+
+	/* check all required parameters */
+	if (i_null_frac > 0)
+	{
+		if (types[i_null_frac] != FLOAT4OID)
+		{
+			/* required param, not recoverable */
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be of type %s", "null_frac",
+							"real")));
+			PG_RETURN_BOOL(false);
+		}
+	}
+	else
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "null_frac")));
+		PG_RETURN_BOOL(false);
+	}
+
+	if (i_avg_width > 0)
+	{
+		if (types[i_avg_width] != INT4OID)
+		{
+			/* required param, not recoverable */
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be of type %s", "avg_width",
+							"integer")));
+			PG_RETURN_BOOL(false);
+		}
+	}
+	else
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "avg_width")));
+		PG_RETURN_BOOL(false);
+	}
+
+	if (i_n_distinct > 0)
+	{
+		if (types[i_n_distinct] != FLOAT4OID)
+		{
+			/* required param, not recoverable */
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be of type %s", "n_distinct",
+							"real")));
+			PG_RETURN_BOOL(false);
+		}
+	}
+	else
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "n_distinct")));
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * Look for pair mismatches, if found warn and disable.
+	 */
+	if ((i_mc_vals == 0) != (i_mc_freqs == 0))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be present if %s is missing",
+						(i_mc_vals == 0) ? "most_common_freqs" :
+						"most_common_vals",
+						(i_mc_vals == 0) ? "most_common_vals" :
+						"most_common_freqs")));
+		i_mc_vals = -1;
+		i_mc_freqs = -1;
+	}
+
+	if ((i_mc_elems == 0) != (i_mc_elem_freqs == 0))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be present if %s is missing",
+						(i_mc_elems == 0) ?
+						"most_common_elem_freqs" :
+						"most_common_elems",
+						(i_mc_elems == 0) ?
+						"most_common_elems" :
+						"most_common_elem_freqs")));
+		i_mc_elems = -1;
+		i_mc_elem_freqs = -1;
+	}
+
+	if ((i_range_length_hist == 0) != (i_range_empty_frac == 0))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be present if %s is missing",
+						(i_range_length_hist == 0) ?
+						"range_empty_frac" :
+						"range_length_histogram",
+						(i_range_length_hist == 0) ?
+						"range_length_histogram" :
+						"range_empty_frac")));
+		i_range_length_hist = -1;
+		i_range_empty_frac = -1;
+	}
+
+	rel = relation_open(relid, ShareUpdateExclusiveLock);
+
+	if (!can_modify_relation(rel))
+	{
+		relation_close(rel, NoLock);
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						RelationGetRelationName(rel))));
+		PG_RETURN_BOOL(false);
+	}
+
+	if ((!inherited) &&
+		((rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) ||
+		 (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX)))
+	{
+		relation_close(rel, NoLock);
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("%s is partitioned, can only accepted inherted stats",
+						RelationGetRelationName(rel))));
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * Many of the values that are set for a particular stakind are entirely
+	 * derived from the attribute itself, or it's expression.
+	 */
+	typcache = get_attr_stat_type(rel, attname, &attnum, &typmod, &typcoll);
+	if (typcache == NULL)
+	{
+		relation_close(rel, NoLock);
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * Derive element type if we have stat kinds that need it.
+	 *
+	 * This duplicates some type-specific logic found in various typanalyze
+	 * functions which are called from vacuum's examine_attribute().
+	 */
+	if ((i_mc_elems > 0) || (i_elem_count_hist > 0))
+	{
+		Oid			elemtypid;
+
+		if (typcache->type_id == TSVECTOROID)
+		{
+			/*
+			 * tsvector elems always have a text oid type and default
+			 * collation
+			 */
+			elemtypid = TEXTOID;
+			typcoll = DEFAULT_COLLATION_OID;
+		}
+		else if (typcache->typtype == TYPTYPE_RANGE)
+			elemtypid = get_range_subtype(typcache->type_id);
+		else
+			elemtypid = get_base_element_type(typcache->type_id);
+
+		/* not finding a basetype means we already had it */
+		if (elemtypid == InvalidOid)
+			elemtypid = typcache->type_id;
+
+		/* The stats need the eq_opr, any validation would need the lt_opr */
+		elemtypcache = lookup_type_cache(elemtypid, TYPECACHE_EQ_OPR);
+
+		if (elemtypcache == NULL)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							(i_mc_elems > 0) ?
+							"most_common_elems" :
+							"elem_count_histogram")));
+			i_mc_elems = -1;
+			i_elem_count_hist = -1;
+		}
+	}
+
+	/*
+	 * histogram_bounds and correlation must have a type < operator. WARN and
+	 * skip if this attribute doesn't have one.
+	 */
+	if (typcache->lt_opr == InvalidOid)
+	{
+		if (i_hist_bounds > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"histogram_bounds")));
+			i_hist_bounds = -1;
+		}
+		if (i_correlation > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"correlation")));
+			i_correlation = -1;
+		}
+	}
+
+	/*
+	 * Scalar types can't have most_common_elems, most_common_elem_freqs,
+	 * elem_count_histogram. WARN and skip.
+	 */
+	if (type_is_scalar(typcache->type_id))
+	{
+		if (i_mc_elems > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"most_common_elems")));
+			i_mc_elems = -1;
+		}
+		if (i_mc_elem_freqs > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"most_common_elem_freqs")));
+			i_mc_elem_freqs = -1;
+		}
+		if (i_elem_count_hist > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"elem_count_histogram")));
+			i_elem_count_hist = -1;
+		}
+	}
+
+	/*
+	 * Only range types can have range_length_histogram, range_empty_frac, and
+	 * range_bounds_histogram. WARN and skip
+	 */
+	if ((typcache->typtype != TYPTYPE_MULTIRANGE) &&
+		(typcache->typtype != TYPTYPE_RANGE))
+	{
+		if (i_range_length_hist > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"range_length_histogram")));
+			i_range_length_hist = -1;
+		}
+		if (i_range_empty_frac > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"range_empty_frac")));
+			i_range_empty_frac = -1;
+		}
+		if (i_range_bounds_hist > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"range_bounds_histogram")));
+			i_range_bounds_hist = -1;
+		}
+	}
+
+	/*
+	 * count the number of stakinds we still want to set, paired params count
+	 * as one. The count cannot exceed STATISTIC_NUM_SLOTS.
+	 */
+	stakind_count = (int) (i_mc_vals > 0) +
+		(int) (i_mc_elems > 0) +
+		(int) (i_range_length_hist > 0) +
+		(int) (i_hist_bounds > 0) +
+		(int) (i_correlation > 0) +
+		(int) (i_elem_count_hist > 0) +
+		(int) (i_range_bounds_hist > 0);
+
+	if (stakind_count > STATISTIC_NUM_SLOTS)
+	{
+		/*
+		 * This really shouldn't happen, as most datatypes exclude at least
+		 * one of these types of stats.
+		 */
+		relation_close(rel, NoLock);
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("imported statistics must have a maximum of %d slots "
+						"but %d given",
+						STATISTIC_NUM_SLOTS, stakind_count)));
+		PG_RETURN_BOOL(false);
+	}
+
+	fmgr_info(F_ARRAY_IN, &finfo);
+
+	values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(relid);
+	values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(attnum);
+	values[Anum_pg_statistic_stainherit - 1] = BoolGetDatum(inherited);
+	values[Anum_pg_statistic_stanullfrac - 1] = args[i_null_frac];
+	values[Anum_pg_statistic_stawidth - 1] = args[i_avg_width];
+	values[Anum_pg_statistic_stadistinct - 1] = args[i_n_distinct];
+
+	/*
+	 * STATISTIC_KIND_MCV
+	 *
+	 * most_common_freqs: real[]
+	 *
+	 * most_common_vals : ANYARRAY::text
+	 */
+	if (i_mc_vals > 0)
+	{
+		Oid			numberstype = types[i_mc_freqs];
+		Oid			valuestype = types[i_mc_vals];
+		Datum		stanumbers = args[i_mc_freqs];
+		Datum		strvalue = args[i_mc_vals];
+
+		if (get_element_type(numberstype) != FLOAT4OID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"most_common_freqs", "real[]")));
+		}
+		else if (valuestype != TEXTOID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"most_common_vals", "text")));
+		}
+		else
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_MCV);
+			Datum		staop = ObjectIdGetDatum(typcache->eq_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		stavalues;
+			bool		converted = false;
+
+			stavalues = cast_stavalues(&finfo, strvalue, typcache->type_id,
+									   typmod, &converted);
+
+			if (converted &&
+				array_check(stavalues, false, "most_common_vals") &&
+				array_check(stanumbers, true, "most_common_freqs"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_HISTOGRAM
+	 *
+	 * histogram_bounds: ANYARRAY::text
+	 */
+	if (i_hist_bounds > 0)
+	{
+		Oid			valuestype = types[i_hist_bounds];
+		Datum		strvalue = args[i_hist_bounds];
+
+		if (valuestype != TEXTOID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"histogram_bounds", "text")));
+		else
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_HISTOGRAM);
+			Datum		staop = ObjectIdGetDatum(typcache->lt_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		stavalues;
+			bool		converted = false;
+
+			stavalues = cast_stavalues(&finfo, strvalue, typcache->type_id,
+									   typmod, &converted);
+
+			if (converted && array_check(stavalues, false, "histogram_bounds"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_CORRELATION
+	 *
+	 * correlation: real
+	 */
+	if (i_correlation > 0)
+	{
+		if (types[i_correlation] != FLOAT4OID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be of type %s, ignored",
+							"correlation", "real")));
+		else
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_CORRELATION);
+			Datum		staop = ObjectIdGetDatum(typcache->lt_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		elems[] = {args[i_correlation]};
+			ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+			Datum		stanumbers = PointerGetDatum(arry);
+
+			values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+			values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+			values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+			values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+			nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+			k++;
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_MCELEM
+	 *
+	 * most_common_elem_freqs: real[]
+	 *
+	 * most_common_elems     : ANYARRAY::text
+	 */
+	if (i_mc_elems > 0)
+	{
+		Oid			numberstype = types[i_mc_elem_freqs];
+		Oid			valuestype = types[i_mc_elems];
+		Datum		stanumbers = args[i_mc_elem_freqs];
+
+		if (get_element_type(numberstype) != FLOAT4OID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"most_common_elem_freqs", "real[]")));
+		else if (valuestype != TEXTOID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"most_common_elems", "text")));
+		else
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_MCELEM);
+			Datum		staop = ObjectIdGetDatum(elemtypcache->eq_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		strvalue = args[i_mc_elems];
+			bool		converted = false;
+			Datum		stavalues;
+
+			stavalues = cast_stavalues(&finfo, strvalue, elemtypcache->type_id,
+									   typmod, &converted);
+
+			if (converted &&
+				array_check(stavalues, false, "most_common_elems") &&
+				array_check(stanumbers, true, "most_common_elem_freqs"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_DECHIST
+	 *
+	 * elem_count_histogram:	real[]
+	 */
+	if (i_elem_count_hist > 0)
+	{
+		Oid			numberstype = types[i_elem_count_hist];
+
+		if (get_element_type(numberstype) != FLOAT4OID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"elem_count_histogram", "real[]")));
+		else
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_DECHIST);
+			Datum		staop = ObjectIdGetDatum(elemtypcache->eq_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		stanumbers = args[i_elem_count_hist];
+
+			if (array_check(stanumbers, true, "elem_count_histogram"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+				nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+				k++;
+			}
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_BOUNDS_HISTOGRAM
+	 *
+	 * range_bounds_histogram: ANYARRAY::text
+	 *
+	 * This stakind appears before STATISTIC_KIND_BOUNDS_HISTOGRAM even though
+	 * it is numerically greater, and all other stakinds appear in numerical
+	 * order. We duplicate this quirk to make before/after tests of
+	 * pg_statistic records easier.
+	 */
+	if (i_range_bounds_hist > 0)
+	{
+		Oid			valuestype = types[i_range_bounds_hist];
+
+		if (valuestype != TEXTOID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"range_bounds_histogram", "text")));
+		else
+		{
+
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_BOUNDS_HISTOGRAM);
+			Datum		staop = ObjectIdGetDatum(InvalidOid);
+			Datum		stacoll = ObjectIdGetDatum(InvalidOid);
+			Datum		strvalue = args[i_range_bounds_hist];
+			bool		converted = false;
+			Datum		stavalues;
+
+			stavalues = cast_stavalues(&finfo, strvalue, typcache->type_id,
+									   typmod, &converted);
+
+			if (converted &&
+				array_check(stavalues, false, "range_bounds_histogram"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM
+	 *
+	 * range_empty_frac: real
+	 *
+	 * range_length_histogram:  double precision[]::text
+	 */
+	if (i_range_length_hist > 0)
+	{
+		Oid			numberstype = types[i_range_empty_frac];
+		Oid			valuestype = types[i_range_length_hist];
+
+		if (numberstype != FLOAT4OID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"range_empty_frac", "real")));
+		else if (valuestype != TEXTOID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"range_length_histogram", "text")));
+		else
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM);
+			Datum		staop = ObjectIdGetDatum(Float8LessOperator);
+			Datum		stacoll = ObjectIdGetDatum(InvalidOid);
+
+			/* The anyarray is always a float8[] for this stakind */
+			Datum		elem = args[i_range_empty_frac];
+			Datum		elems[] = {elem};
+			ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+			Datum		stanumbers = PointerGetDatum(arry);
+			Datum		strvalue = args[i_range_length_hist];
+			bool		converted = false;
+			Datum		stavalues;
+
+			stavalues = cast_stavalues(&finfo, strvalue, FLOAT8OID, 0, &converted);
+
+			if (converted &&
+				array_check(stavalues, false, "range_length_histogram"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+	}
+
+	/* fill in all remaining slots */
+	for (; k < STATISTIC_NUM_SLOTS; k++)
+	{
+		values[Anum_pg_statistic_stakind1 - 1 + k] = Int16GetDatum(0);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+	}
+
+	update_pg_statistic(values, nulls);
+
+	relation_close(rel, NoLock);
+	PG_RETURN_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..684df93993
--- /dev/null
+++ b/src/test/regress/expected/stats_export_import.out
@@ -0,0 +1,853 @@
+CREATE SCHEMA stats_export_import;
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+        0 |        -1 |             0
+(1 row)
+
+-- error: regclass not found
+SELECT pg_set_relation_stats('stats_export_import.nope'::regclass,
+                             150000::integer);
+ERROR:  relation "stats_export_import.nope" does not exist
+LINE 1: SELECT pg_set_relation_stats('stats_export_import.nope'::reg...
+                                     ^
+-- error: all three params missing
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer);
+ERROR:  function pg_set_relation_stats(regclass, integer) does not exist
+LINE 1: SELECT pg_set_relation_stats('stats_export_import.test'::reg...
+               ^
+HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+-- error: reltuples, relallvisible missing
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 4::integer);
+WARNING:  required parameter reltuples not set
+ pg_set_relation_stats 
+-----------------------
+ f
+(1 row)
+
+-- error: null value
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', NULL::real,
+                             'relallvisible', 4::integer);
+ pg_set_relation_stats 
+-----------------------
+ 
+(1 row)
+
+-- error: bad relpages type
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+WARNING:  relpages must be of type integer
+ pg_set_relation_stats 
+-----------------------
+ f
+(1 row)
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 17::integer,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+ pg_set_relation_stats 
+-----------------------
+ t
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       17 |       400 |             4
+(1 row)
+
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    '0'::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ERROR:  could not open relation with OID 0
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    NULL::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    NULL::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    NULL::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', NULL::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', NULL::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', NULL::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT stanullfrac, stawidth, stadistinct
+FROM pg_statistic
+WHERE starelid = 'stats_export_import.test'::regclass;
+ stanullfrac | stawidth | stadistinct 
+-------------+----------+-------------
+         0.1 |        2 |         0.3
+(1 row)
+
+-- warn: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_freqs', '{0.1,0.2,0.3}'::real[]
+    );
+WARNING:  most_common_freqs cannot be present if most_common_vals is missing
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{1,2,3}'::text
+    );
+WARNING:  most_common_vals cannot be present if most_common_freqs is missing
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.2,0.1}'::double precision[]
+    );
+WARNING:  most_common_freqs must be type real[], ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warning: mcv cast failure
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,four,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+WARNING:  invalid input syntax for type integer: "four"
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: histogram elements null value
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,NULL,3,4}'::text
+    );
+WARNING:  histogram_bounds array cannot contain NULL values
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,2,3,4}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'correlation', 0.5::real);
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |         0.5 |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{1,3}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+WARNING:  id cannot accept most_common_elems stats, ignored
+WARNING:  id cannot accept most_common_elem_freqs stats, ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,two}'::text
+    );
+WARNING:  most_common_elems cannot be present if most_common_elem_freqs is missing
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
+    );
+WARNING:  most_common_elem_freqs cannot be present if most_common_elems is missing
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             | {one,three}       | {0.3,0.2,0.2,0.3,0}    |                      |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+WARNING:  id cannot accept elem_count_histogram stats, ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+WARNING:  elem_count_histogram array cannot contain NULL values
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+WARNING:  id cannot accept range_length_histogram stats, ignored
+WARNING:  id cannot accept range_empty_frac stats, ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+WARNING:  range_length_histogram cannot be present if range_empty_frac is missing
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real
+    );
+WARNING:  range_empty_frac cannot be present if range_length_histogram is missing
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
+(1 row)
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+WARNING:  id cannot accept range_bounds_histogram stats, ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
+ stats_export_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      |                        |                  | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+(1 row)
+
+-- warn: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
+    'histogram_bounds', '{1,2,3,4}'::text,
+    'correlation', 1.1::real,
+    'most_common_elems', '{3,1}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    'range_empty_frac', -0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+WARNING:  imported statistics must have a maximum of 5 slots but 6 given
+ pg_set_attribute_stats 
+------------------------
+ f
+(1 row)
+
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+   relname    | num_stats 
+--------------+-----------
+ is_odd       |         1
+ is_odd_clone |         1
+ test         |         5
+ test_clone   |         5
+(4 rows)
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 675c567617..bcd7c0720e 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,8 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc stats_export_import
+
 
 # ----------
 # Load huge amounts of data
diff --git a/src/test/regress/sql/stats_export_import.sql b/src/test/regress/sql/stats_export_import.sql
new file mode 100644
index 0000000000..ebdb58aba1
--- /dev/null
+++ b/src/test/regress/sql/stats_export_import.sql
@@ -0,0 +1,671 @@
+CREATE SCHEMA stats_export_import;
+
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+-- error: regclass not found
+SELECT pg_set_relation_stats('stats_export_import.nope'::regclass,
+                             150000::integer);
+
+-- error: all three params missing
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer);
+
+-- error: reltuples, relallvisible missing
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 4::integer);
+
+-- error: null value
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', NULL::real,
+                             'relallvisible', 4::integer);
+
+-- error: bad relpages type
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 17::integer,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_export_import.test'::regclass;
+
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    '0'::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    NULL::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    NULL::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    NULL::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', NULL::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', NULL::integer,
+    'n_distinct', 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', NULL::real);
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+SELECT stanullfrac, stawidth, stadistinct
+FROM pg_statistic
+WHERE starelid = 'stats_export_import.test'::regclass;
+
+-- warn: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_freqs', '{0.1,0.2,0.3}'::real[]
+    );
+
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{1,2,3}'::text
+    );
+
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.2,0.1}'::double precision[]
+    );
+
+-- warning: mcv cast failure
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,four,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: histogram elements null value
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,NULL,3,4}'::text
+    );
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,2,3,4}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'correlation', 0.5::real);
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: scalars can't have mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{1,3}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,two}'::text
+    );
+
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
+    );
+
+-- ok: mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- warn: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real
+    );
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
+    'histogram_bounds', '{1,2,3,4}'::text,
+    'correlation', 1.1::real,
+    'most_common_elems', '{3,1}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    'range_empty_frac', -0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+
+SELECT
+    format('SELECT pg_catalog.pg_set_attribute_stats( '
+            || '%L::regclass::oid, '
+            || '%L::name, '
+            || '%L::boolean, '
+            || '%L::integer, '
+            || '%L, %L::real, '
+            || '%L, %L::integer, '
+            || '%L, %L::real %s)',
+        'stats_export_import.' || s.tablename || '_clone',
+        s.attname,
+        s.inherited,
+        150000,
+        'null_frac', s.null_frac,
+        'avg_width', s.avg_width,
+        'n_distinct', s.n_distinct,
+        CASE
+            WHEN s.most_common_vals IS NULL THEN ''
+            ELSE format(', %L, %L::text, %L, %L::real[]',
+                        'most_common_vals', s.most_common_vals,
+                        'most_common_freqs', s.most_common_freqs)
+        END ||
+        CASE
+            WHEN s.histogram_bounds IS NULL THEN ''
+            ELSE format(', %L, %L::text',
+                        'histogram_bounds', s.histogram_bounds)
+        END ||
+        CASE
+            WHEN s.correlation IS NULL THEN ''
+            ELSE format(', %L, %L::real',
+                        'correlation', s.correlation)
+        END ||
+        CASE
+            WHEN s.most_common_elems IS NULL THEN ''
+            ELSE format(', %L, %L::text, %L, %L::real[]',
+                        'most_common_elems', s.most_common_elems,
+                        'most_common_elem_freqs', s.most_common_elem_freqs)
+        END ||
+        CASE
+            WHEN s.elem_count_histogram IS NULL THEN ''
+            ELSE format(', %L, %L::real[]',
+                        'elem_count_histogram', s.elem_count_histogram)
+        END ||
+        CASE
+            WHEN s.range_bounds_histogram IS NULL THEN ''
+            ELSE format(', %L, %L::text',
+                        'range_bounds_histogram', s.range_bounds_histogram)
+        END ||
+        CASE
+            WHEN s.range_empty_frac IS NULL THEN ''
+            ELSE format(', %L, %L::real, %L, %L::text',
+                        'range_empty_frac', s.range_empty_frac,
+                        'range_length_histogram', s.range_length_histogram)
+        END)
+FROM pg_catalog.pg_stats AS s
+WHERE s.schemaname = 'stats_export_import'
+AND s.tablename IN ('test', 'is_odd')
+\gexec
+
+-- restore ECHO to original value
+\set ECHO :orig_ECHO
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 36b2c5427a..5772f1ae23 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29629,6 +29629,273 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
     in identifying the specific disk files associated with database objects.
    </para>
 
+   <table id="functions-admin-statsimport">
+    <title>Database Object Statistics Import Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_relation_stats</primary>
+        </indexterm>
+        <function>pg_set_relation_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>version</parameter> <type>integer</type>,
+         <literal>VARIADIC</literal> <parameter>stats</parameter> <type>"any"</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Updates the <structname>pg_class</structname> row for the specified
+        <parameter>relation</parameter>, values from the variadic list of
+        name-value pairs. Each parameter name corresponds to the same-named
+        attribute in <structname>pg_class</structname>:
+       </para>
+       <variablelist>
+        <varlistentry>
+         <term><literal>relpages</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>integer</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>reltuples</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>relalltuples</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>integer</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+       </variablelist>
+       <para>
+        Any other parameter names given must also have a value pair, but will emit
+        a warning and thereafter be ignored.
+       </para>
+       <para>
+        The <parameter>version</parameter> is meant to reflect the server version
+        number of the system where this data was generated, as that may in the
+        future change how the data is imported.
+       </para>
+       <para>
+        To avoid table bloat in <structname>pg_class</structname>, this change
+        is made with an in-place update, and therefore cannot be rolled back
+        through normal transaction processing.
+       </para>
+       <para>
+        This function mimics the behavior of <command>ANALYZE</command> in its
+        effects on the values in <structname>pg_class</structname>, except that
+        the values are supplied as parameters rather than derived from table
+        sampling.
+       </para>
+       <para>
+        The caller must either be the owner of the relation, or have superuser
+        privileges.
+       </para>
+       <para>
+        The parameters given are checked for type validity and must all be
+        NOT NULL.
+       </para>
+       <para>
+        The parameters will return true if stats were updated and false if not.
+       </para>
+       </entry>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_attribute_stats</primary>
+        </indexterm>
+        <function>pg_set_attribute_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>
+         , <parameter>attname</parameter> <type>name</type>
+         , <parameter>inherited</parameter> <type>boolean</type>
+         , <parameter>version</parameter> <type>integer</type>
+         , <literal>VARIADIC</literal> <parameter>stats</parameter> <type>"any"</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Replaces the <structname>pg_statistic</structname> row for the
+        <structname>pg_attribute</structname> row specified by
+        <parameter>relation</parameter>, <parameter>attname</parameter>
+        and <parameter>inherited</parameter>. The <parameter>version</parameter>
+        should be set to the server version number of the server where
+        the variadic <parameter>stats</parameter> originated, as it may in the
+        future affect how the stats are imported. Values from the variadic
+        list form name-value pairs. Each parameter name corresponds to the
+        same-named attribute in
+        <link linkend="view-pg-stats"><structname>pg_stats</structname></link>:
+       </para>
+       <variablelist>
+        <varlistentry>
+         <term><literal>null_frac</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>avg_width</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>integer</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>n_distinct</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_vals</literal></term>
+         <listitem>
+          <para>
+          <literal>most_common_vals</literal>
+           Must be followed by a parameter of type <type>text</type>. If this
+           parameter is specified then <literal>most_common_freqs</literal>
+           must also be specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_freqs</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of <type>real[]</type>, and can only
+           be specified if <literal>most_common_vals</literal> is also specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>histogram_bounds</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>correlation</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_elems</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>. If this
+           parameter is specified then <literal>most_common_elem_freqs</literal>
+           must also be specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_elem_freqs</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real[]</type>, and can
+           only be specified if <literal>most_common_elems</literal> is also
+           specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>elem_count_histogram</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real[]</type>.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>range_length_histogram</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>. If this
+           parameter is specified then <literal>range_empty_frac</literal>
+           must also be specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>range_empty_frac</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>, and can
+           only be specified if <literal>range_length_histogram</literal>
+           is also specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>range_bounds_histogram</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>
+          </para>
+         </listitem>
+        </varlistentry>
+       </variablelist>
+       <para>
+        Any other parameter names given must also have a value pair, but will emit
+        a warning and thereafter be ignored.
+       </para>
+       <para>
+        The parameters will return true if stats were updated and false if not.
+       </para>
+       <para>
+        This function mimics the behavior of <command>ANALYZE</command> in its
+        effects on the values in <structname>pg_statistic</structname>, except
+        that the values are supplied as parameters rather than derived from
+        table sampling.
+       </para>
+       <para>
+        The purpose of this function is to apply statistics values in an
+        upgrade situation that are "good enough" for system operation until
+        they are replaced by the next <command>ANALYZE</command>, usually via
+        <command>autovacuum</command>.
+       </para>
+       <para>
+        The caller must either be the owner of the relation, or have superuser
+        privileges.
+       </para>
+       </entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
    <table id="functions-admin-dblocation">
     <title>Database Object Location Functions</title>
     <tgroup cols="1">
-- 
2.45.0

v21-0002-Add-derivative-flags-dumpSchema-dumpData.patchtext/x-patch; charset=US-ASCII; name=v21-0002-Add-derivative-flags-dumpSchema-dumpData.patchDownload
From 7236ba9a4405da3d67ae76ea1084c4f1ee9bc0cf Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 4 May 2024 04:52:38 -0400
Subject: [PATCH v21 2/3] Add derivative flags dumpSchema, dumpData.

User-set flags --schema-only and --data-only are often consulted by
various operations to determine if they should be skipped or not. While
this logic works when there are only two mutually-exclusive -only
options, it will get progressively more confusing when more are added.

In anticipation of this, create the flags dumpSchema and dumpData which
are derivative of the existing options schemaOnly and dataOnly. This
allows us to restate current skip-this-section tests in terms of what is
enabled, rather than checking if the other -only mode is turned off.
---
 src/bin/pg_dump/pg_backup.h                  |   8 +
 src/bin/pg_dump/pg_dump.c                    | 186 ++++++++++---------
 src/bin/pg_dump/pg_restore.c                 |   4 +
 src/test/regress/sql/stats_export_import.sql |   4 +-
 4 files changed, 109 insertions(+), 93 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index fbf5f1c515..d942a6a256 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -157,6 +157,10 @@ typedef struct _restoreOptions
 	int			enable_row_security;
 	int			sequence_data;	/* dump sequence data even in schema-only mode */
 	int			binary_upgrade;
+
+	/* flags derived entirely from the user-settable flags */
+	bool		dumpSchema;
+	bool		dumpData;
 } RestoreOptions;
 
 typedef struct _dumpOptions
@@ -203,6 +207,10 @@ typedef struct _dumpOptions
 
 	int			sequence_data;	/* dump sequence data even in schema-only mode */
 	int			do_nothing;
+
+	/* flags derived entirely from the user-settable flags */
+	bool		dumpSchema;
+	bool		dumpData;
 } DumpOptions;
 
 /*
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 379debac24..a6ee93e765 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -738,6 +738,10 @@ main(int argc, char **argv)
 	if (dopt.if_exists && !dopt.outputClean)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
+	/* set derivative flags */
+	dopt.dumpSchema = (!dopt.dataOnly);
+	dopt.dumpData = (!dopt.schemaOnly);
+
 	/*
 	 * --inserts are already implied above if --column-inserts or
 	 * --rows-per-insert were specified.
@@ -920,7 +924,7 @@ main(int argc, char **argv)
 	 * -s means "schema only" and LOs are data, not schema, so we never
 	 * include LOs when -s is used.
 	 */
-	if (dopt.include_everything && !dopt.schemaOnly && !dopt.dontOutputLOs)
+	if (dopt.include_everything && dopt.dumpData && !dopt.dontOutputLOs)
 		dopt.outputLOs = true;
 
 	/*
@@ -934,15 +938,15 @@ main(int argc, char **argv)
 	 */
 	tblinfo = getSchemaData(fout, &numTables);
 
-	if (!dopt.schemaOnly)
+	if (dopt.dumpData)
 	{
 		getTableData(&dopt, tblinfo, numTables, 0);
 		buildMatViewRefreshDependencies(fout);
-		if (dopt.dataOnly)
+		if (!dopt.dumpSchema)
 			getTableDataFKConstraints();
 	}
 
-	if (dopt.schemaOnly && dopt.sequence_data)
+	if (!dopt.dumpData && dopt.sequence_data)
 		getTableData(&dopt, tblinfo, numTables, RELKIND_SEQUENCE);
 
 	/*
@@ -4067,8 +4071,8 @@ dumpPolicy(Archive *fout, const PolicyInfo *polinfo)
 	const char *cmd;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
@@ -4286,8 +4290,8 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 	char	   *qpubname;
 	bool		first = true;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	delq = createPQExpBuffer();
@@ -4601,8 +4605,8 @@ dumpPublicationNamespace(Archive *fout, const PublicationSchemaInfo *pubsinfo)
 	PQExpBuffer query;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	tag = psprintf("%s %s", pubinfo->dobj.name, schemainfo->dobj.name);
@@ -4644,8 +4648,8 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 	PQExpBuffer query;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	tag = psprintf("%s %s", pubinfo->dobj.name, tbinfo->dobj.name);
@@ -5008,8 +5012,8 @@ dumpSubscriptionTable(Archive *fout, const SubRelInfo *subrinfo)
 	PQExpBuffer query;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	Assert(fout->dopt->binary_upgrade && fout->remoteVersion >= 170000);
@@ -5082,8 +5086,8 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo)
 	int			i;
 	char		two_phase_disabled[] = {LOGICALREP_TWOPHASE_STATE_DISABLED, '\0'};
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	delq = createPQExpBuffer();
@@ -7269,8 +7273,8 @@ getPartitioningInfo(Archive *fout)
 	/* hash partitioning didn't exist before v11 */
 	if (fout->remoteVersion < 110000)
 		return;
-	/* needn't bother if schema-only dump */
-	if (fout->dopt->schemaOnly)
+	/* needn't bother if not dumping data */
+	if (!fout->dopt->dumpData)
 		return;
 
 	query = createPQExpBuffer();
@@ -9165,7 +9169,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * Now get info about column defaults.  This is skipped for a data-only
 	 * dump, as it is only needed for table schemas.
 	 */
-	if (!dopt->dataOnly && tbloids->len > 1)
+	if (dopt->dumpSchema && tbloids->len > 1)
 	{
 		AttrDefInfo *attrdefs;
 		int			numDefaults;
@@ -9295,7 +9299,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * Get info about table CHECK constraints.  This is skipped for a
 	 * data-only dump, as it is only needed for table schemas.
 	 */
-	if (!dopt->dataOnly && checkoids->len > 2)
+	if (dopt->dumpSchema && checkoids->len > 2)
 	{
 		ConstraintInfo *constrs;
 		int			numConstrs;
@@ -10247,13 +10251,13 @@ dumpCommentExtended(Archive *fout, const char *type,
 	/* Comments are schema not data ... except LO comments are data */
 	if (strcmp(type, "LARGE OBJECT") != 0)
 	{
-		if (dopt->dataOnly)
+		if (!dopt->dumpSchema)
 			return;
 	}
 	else
 	{
 		/* We do dump LO comments in binary-upgrade mode */
-		if (dopt->schemaOnly && !dopt->binary_upgrade)
+		if (!dopt->dumpData && !dopt->binary_upgrade)
 			return;
 	}
 
@@ -10360,7 +10364,7 @@ dumpTableComment(Archive *fout, const TableInfo *tbinfo,
 		return;
 
 	/* Comments are SCHEMA not data */
-	if (dopt->dataOnly)
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Search for comments associated with relation, using table */
@@ -10806,8 +10810,8 @@ dumpNamespace(Archive *fout, const NamespaceInfo *nspinfo)
 	PQExpBuffer delq;
 	char	   *qnspname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -10883,8 +10887,8 @@ dumpExtension(Archive *fout, const ExtensionInfo *extinfo)
 	PQExpBuffer delq;
 	char	   *qextname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -11008,8 +11012,8 @@ dumpType(Archive *fout, const TypeInfo *tyinfo)
 {
 	DumpOptions *dopt = fout->dopt;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Dump out in proper style */
@@ -12119,8 +12123,8 @@ dumpShellType(Archive *fout, const ShellTypeInfo *stinfo)
 	DumpOptions *dopt = fout->dopt;
 	PQExpBuffer q;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -12171,8 +12175,8 @@ dumpProcLang(Archive *fout, const ProcLangInfo *plang)
 	FuncInfo   *inlineInfo = NULL;
 	FuncInfo   *validatorInfo = NULL;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
@@ -12379,8 +12383,8 @@ dumpFunc(Archive *fout, const FuncInfo *finfo)
 	int			nconfigitems = 0;
 	const char *keyword;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -12771,8 +12775,8 @@ dumpCast(Archive *fout, const CastInfo *cast)
 	const char *sourceType;
 	const char *targetType;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Cannot dump if we don't have the cast function's info */
@@ -12877,8 +12881,8 @@ dumpTransform(Archive *fout, const TransformInfo *transform)
 	char	   *lanname;
 	const char *transformType;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Cannot dump if we don't have the transform functions' info */
@@ -13026,8 +13030,8 @@ dumpOpr(Archive *fout, const OprInfo *oprinfo)
 	char	   *oprregproc;
 	char	   *oprref;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
@@ -13313,8 +13317,8 @@ dumpAccessMethod(Archive *fout, const AccessMethodInfo *aminfo)
 	PQExpBuffer delq;
 	char	   *qamname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -13416,8 +13420,8 @@ dumpOpclass(Archive *fout, const OpclassInfo *opcinfo)
 	bool		needComma;
 	int			i;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -13687,8 +13691,8 @@ dumpOpfamily(Archive *fout, const OpfamilyInfo *opfinfo)
 	bool		needComma;
 	int			i;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -13894,8 +13898,8 @@ dumpCollation(Archive *fout, const CollInfo *collinfo)
 	const char *colllocale;
 	const char *collicurules;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -14148,8 +14152,8 @@ dumpConversion(Archive *fout, const ConvInfo *convinfo)
 	const char *conproc;
 	bool		condefault;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -14296,8 +14300,8 @@ dumpAgg(Archive *fout, const AggInfo *agginfo)
 	const char *proparallel;
 	char		defaultfinalmodify;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -14626,8 +14630,8 @@ dumpTSParser(Archive *fout, const TSParserInfo *prsinfo)
 	PQExpBuffer delq;
 	char	   *qprsname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14694,8 +14698,8 @@ dumpTSDictionary(Archive *fout, const TSDictInfo *dictinfo)
 	char	   *nspname;
 	char	   *tmplname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14770,8 +14774,8 @@ dumpTSTemplate(Archive *fout, const TSTemplateInfo *tmplinfo)
 	PQExpBuffer delq;
 	char	   *qtmplname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14836,8 +14840,8 @@ dumpTSConfig(Archive *fout, const TSConfigInfo *cfginfo)
 	int			i_tokenname;
 	int			i_dictname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14948,8 +14952,8 @@ dumpForeignDataWrapper(Archive *fout, const FdwInfo *fdwinfo)
 	PQExpBuffer delq;
 	char	   *qfdwname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -15021,8 +15025,8 @@ dumpForeignServer(Archive *fout, const ForeignServerInfo *srvinfo)
 	char	   *qsrvname;
 	char	   *fdwname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -15212,8 +15216,8 @@ dumpDefaultACL(Archive *fout, const DefaultACLInfo *daclinfo)
 	PQExpBuffer tag;
 	const char *type;
 
-	/* Do nothing in data-only dump, or if we're skipping ACLs */
-	if (dopt->dataOnly || dopt->aclsSkip)
+	/* Do nothing if not dumping schema, or if we're skipping ACLs */
+	if (!dopt->dumpSchema || dopt->aclsSkip)
 		return;
 
 	q = createPQExpBuffer();
@@ -15313,7 +15317,7 @@ dumpACL(Archive *fout, DumpId objDumpId, DumpId altDumpId,
 		return InvalidDumpId;
 
 	/* --data-only skips ACLs *except* large object ACLs */
-	if (dopt->dataOnly && strcmp(type, "LARGE OBJECT") != 0)
+	if (!dopt->dumpSchema && strcmp(type, "LARGE OBJECT") != 0)
 		return InvalidDumpId;
 
 	sql = createPQExpBuffer();
@@ -15442,13 +15446,13 @@ dumpSecLabel(Archive *fout, const char *type, const char *name,
 	 */
 	if (strcmp(type, "LARGE OBJECT") != 0)
 	{
-		if (dopt->dataOnly)
+		if (!dopt->dumpSchema)
 			return;
 	}
 	else
 	{
 		/* We do dump large object security labels in binary-upgrade mode */
-		if (dopt->schemaOnly && !dopt->binary_upgrade)
+		if (!dopt->dumpData && !dopt->binary_upgrade)
 			return;
 	}
 
@@ -15516,7 +15520,7 @@ dumpTableSecLabel(Archive *fout, const TableInfo *tbinfo, const char *reltypenam
 		return;
 
 	/* SecLabel are SCHEMA not data */
-	if (dopt->dataOnly)
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Search for comments associated with relation, using table */
@@ -15755,8 +15759,8 @@ dumpTable(Archive *fout, const TableInfo *tbinfo)
 	DumpId		tableAclDumpId = InvalidDumpId;
 	char	   *namecopy;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	if (tbinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
@@ -16825,8 +16829,8 @@ dumpTableAttach(Archive *fout, const TableAttachInfo *attachinfo)
 	PGresult   *res;
 	char	   *partbound;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -16897,8 +16901,8 @@ dumpAttrDef(Archive *fout, const AttrDefInfo *adinfo)
 	char	   *tag;
 	char	   *foreign;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Skip if not "separate"; it was dumped in the table's definition */
@@ -16986,8 +16990,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 	char	   *qindxname;
 	char	   *qqindxname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -17119,8 +17123,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 static void
 dumpIndexAttach(Archive *fout, const IndexAttachInfo *attachinfo)
 {
-	/* Do nothing in data-only dump */
-	if (fout->dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!fout->dopt->dumpSchema)
 		return;
 
 	if (attachinfo->partitionIdx->dobj.dump & DUMP_COMPONENT_DEFINITION)
@@ -17166,8 +17170,8 @@ dumpStatisticsExt(Archive *fout, const StatsExtInfo *statsextinfo)
 	PGresult   *res;
 	char	   *stxdef;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -17242,8 +17246,8 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
 	char	   *tag = NULL;
 	char	   *foreign;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -17889,8 +17893,8 @@ dumpTrigger(Archive *fout, const TriggerInfo *tginfo)
 	char	   *qtabname;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -18011,8 +18015,8 @@ dumpEventTrigger(Archive *fout, const EventTriggerInfo *evtinfo)
 	PQExpBuffer delqry;
 	char	   *qevtname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -18102,8 +18106,8 @@ dumpRule(Archive *fout, const RuleInfo *rinfo)
 	PGresult   *res;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 5ea78cf7cc..62821cbee4 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -360,6 +360,10 @@ main(int argc, char **argv)
 	if (opts->single_txn && numWorkers > 1)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
+	/* set derivative flags */
+	opts->dumpSchema = (opts->dataOnly != 1);
+	opts->dumpData = (opts->schemaOnly != 1);
+
 	opts->disable_triggers = disable_triggers;
 	opts->enable_row_security = enable_row_security;
 	opts->noDataForFailedTables = no_data_for_failed_tables;
diff --git a/src/test/regress/sql/stats_export_import.sql b/src/test/regress/sql/stats_export_import.sql
index ebdb58aba1..39d96c34ef 100644
--- a/src/test/regress/sql/stats_export_import.sql
+++ b/src/test/regress/sql/stats_export_import.sql
@@ -257,7 +257,7 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: scalars can't have mcelem 
+-- warn: scalars can't have mcelem
 SELECT pg_catalog.pg_set_attribute_stats(
     'stats_export_import.test'::regclass,
     'id'::name,
@@ -294,7 +294,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
     );
 
--- ok: mcelem 
+-- ok: mcelem
 SELECT pg_catalog.pg_set_attribute_stats(
     'stats_export_import.test'::regclass,
     'tags'::name,
-- 
2.45.0

v21-0003-Enable-dumping-of-table-index-stats-in-pg_dump.patchtext/x-patch; charset=US-ASCII; name=v21-0003-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From 741bec76e528a60e61034ae8ab00ee81a9d466a7 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH v21 3/3] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will generate a statement that
calls pg_set_relation_stats(), and it will generate a series of
statements that call pg_set_attribute_stats(), one per attribute. These
statements will restore the statistics of the current system onto the
destination system.

Adds the command-line options -X / --statistics-only, which are mutually
exclusive to --schema-only and --data-only.

Statistics are not dumped when --schema-only is specified, except during
a binary upgrade.

As is the pattern with pg_dump options, staistics can be disabled using
--no-statistics.

Table statistics are dumped in the data section. This is true even if
dumping stats in a binary upgrade. Index and Materialized View
statistics are dumped in the post-data section.
---
 src/bin/pg_dump/pg_backup.h          |   6 +
 src/bin/pg_dump/pg_backup_archiver.c |   5 +
 src/bin/pg_dump/pg_dump.c            | 397 ++++++++++++++++++++++++++-
 src/bin/pg_dump/pg_dump.h            |   8 +
 src/bin/pg_dump/pg_dump_sort.c       |   5 +
 src/bin/pg_dump/pg_dumpall.c         |   5 +
 src/bin/pg_dump/pg_restore.c         |   8 +
 src/bin/pg_dump/t/001_basic.pl       |  10 +-
 doc/src/sgml/ref/pg_dump.sgml        |  36 ++-
 doc/src/sgml/ref/pg_restore.sgml     |  31 ++-
 10 files changed, 493 insertions(+), 18 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index d942a6a256..25d386f5bb 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -112,11 +112,13 @@ typedef struct _restoreOptions
 	int			no_publications;	/* Skip publication entries */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
 	int			dataOnly;
 	int			schemaOnly;
+	int			statisticsOnly;
 	int			dumpSections;
 	int			verbose;
 	int			aclsSkip;
@@ -161,6 +163,7 @@ typedef struct _restoreOptions
 	/* flags derived entirely from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } RestoreOptions;
 
 typedef struct _dumpOptions
@@ -172,6 +175,7 @@ typedef struct _dumpOptions
 	/* various user-settable parameters */
 	bool		schemaOnly;
 	bool		dataOnly;
+	bool		statisticsOnly;
 	int			dumpSections;	/* bitmask of chosen sections */
 	bool		aclsSkip;
 	const char *lockWaitTimeout;
@@ -185,6 +189,7 @@ typedef struct _dumpOptions
 	int			no_security_labels;
 	int			no_publications;
 	int			no_subscriptions;
+	int			no_statistics;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
@@ -211,6 +216,7 @@ typedef struct _dumpOptions
 	/* flags derived entirely from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } DumpOptions;
 
 /*
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index c6c101c118..62d2a50117 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2955,6 +2955,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's a stats dump, maybe ignore it */
+	if (ropt->no_statistics && strcmp(te->desc, "STATISTICS") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2984,6 +2988,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a6ee93e765..28abfe18d7 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -407,6 +407,7 @@ main(int argc, char **argv)
 		{"encoding", required_argument, NULL, 'E'},
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
+		{"statistics-only", no_argument, NULL, 'X'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -435,6 +436,7 @@ main(int argc, char **argv)
 		{"no-comments", no_argument, &dopt.no_comments, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -480,7 +482,7 @@ main(int argc, char **argv)
 
 	InitDumpOptions(&dopt);
 
-	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:RsS:t:T:U:vwWxZ:",
+	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:PRsS:t:T:U:vwWxXZ:",
 							long_options, &optindex)) != -1)
 	{
 		switch (c)
@@ -554,6 +556,10 @@ main(int argc, char **argv)
 				dopt.cparams.pgport = pg_strdup(optarg);
 				break;
 
+			case 'X':			/* Dump statistics only */
+				dopt.statisticsOnly = true;
+				break;
+
 			case 'R':
 				/* no-op, still accepted for backwards compatibility */
 				break;
@@ -723,8 +729,11 @@ main(int argc, char **argv)
 	if (dopt.binary_upgrade)
 		dopt.sequence_data = 1;
 
-	if (dopt.dataOnly && dopt.schemaOnly)
-		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (((int)dopt.dataOnly + (int) dopt.schemaOnly + (int) dopt.statisticsOnly) > 1)
+		pg_fatal("options -s/--schema-only, -a/--data-only, and -X/--statistics-only cannot be used together");
+
+	if (dopt.statisticsOnly && dopt.no_statistics)
+		pg_fatal("options -X/--statistics-only and --no-statistics cannot be used together");
 
 	if (dopt.schemaOnly && foreign_servers_include_patterns.head != NULL)
 		pg_fatal("options -s/--schema-only and --include-foreign-data cannot be used together");
@@ -739,8 +748,23 @@ main(int argc, char **argv)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
 	/* set derivative flags */
-	dopt.dumpSchema = (!dopt.dataOnly);
-	dopt.dumpData = (!dopt.schemaOnly);
+	dopt.dumpSchema = dopt.schemaOnly ||
+		!(dopt.dataOnly || dopt.statisticsOnly);
+
+	dopt.dumpData = dopt.dataOnly ||
+		!(dopt.schemaOnly || dopt.statisticsOnly);
+
+	if (dopt.statisticsOnly)
+		/* stats are only thing wanted */
+		dopt.dumpStatistics = true;
+	else if (dopt.no_statistics)
+		/* stats specifically excluded */
+		dopt.dumpStatistics = false;
+	else if (dopt.binary_upgrade)
+		/* binary upgrade and not specifically excluded */
+		dopt.dumpStatistics = true;
+	else
+		dopt.dumpStatistics = !(dopt.schemaOnly || dopt.dataOnly);
 
 	/*
 	 * --inserts are already implied above if --column-inserts or
@@ -1033,6 +1057,7 @@ main(int argc, char **argv)
 	ropt->dropSchema = dopt.outputClean;
 	ropt->dataOnly = dopt.dataOnly;
 	ropt->schemaOnly = dopt.schemaOnly;
+	ropt->statisticsOnly = dopt.statisticsOnly;
 	ropt->if_exists = dopt.if_exists;
 	ropt->column_inserts = dopt.column_inserts;
 	ropt->dumpSections = dopt.dumpSections;
@@ -1111,7 +1136,7 @@ help(const char *progname)
 	printf(_("  -?, --help                   show this help, then exit\n"));
 
 	printf(_("\nOptions controlling the output content:\n"));
-	printf(_("  -a, --data-only              dump only the data, not the schema\n"));
+	printf(_("  -a, --data-only              dump only the data, not the schema or statistics\n"));
 	printf(_("  -b, --large-objects          include large objects in dump\n"));
 	printf(_("  --blobs                      (same as --large-objects, deprecated)\n"));
 	printf(_("  -B, --no-large-objects       exclude large objects in dump\n"));
@@ -1124,11 +1149,12 @@ help(const char *progname)
 	printf(_("  -N, --exclude-schema=PATTERN do NOT dump the specified schema(s)\n"));
 	printf(_("  -O, --no-owner               skip restoration of object ownership in\n"
 			 "                               plain-text format\n"));
-	printf(_("  -s, --schema-only            dump only the schema, no data\n"));
+	printf(_("  -s, --schema-only            dump only the schema, no data or statistics\n"));
 	printf(_("  -S, --superuser=NAME         superuser user name to use in plain-text format\n"));
 	printf(_("  -t, --table=PATTERN          dump only the specified table(s)\n"));
 	printf(_("  -T, --exclude-table=PATTERN  do NOT dump the specified table(s)\n"));
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
+	printf(_("  -X, --statistics-only        dump only the statistics, not schema or data\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
@@ -1155,6 +1181,7 @@ help(const char *progname)
 	printf(_("  --no-comments                do not dump comments\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -6691,6 +6718,42 @@ getFuncs(Archive *fout, int *numFuncs)
 	return finfo;
 }
 
+/*
+ * getRelationStatistics
+ *    register the statistics object as a dependent of the relation.
+ *
+ */
+static RelStatsInfo *
+getRelationStatistics(Archive *fout, DumpableObject *rel, char relkind)
+{
+	if ((relkind == RELKIND_RELATION) ||
+		(relkind == RELKIND_PARTITIONED_TABLE) ||
+		(relkind == RELKIND_INDEX) ||
+		(relkind == RELKIND_PARTITIONED_INDEX) ||
+		(relkind == RELKIND_MATVIEW))
+	{
+		RelStatsInfo	   *info = pg_malloc0(sizeof(RelStatsInfo));
+		DumpableObject	   *dobj = &info->dobj;
+
+		dobj->objType = DO_REL_STATS;
+		dobj->catId.tableoid = 0;
+		dobj->catId.oid = 0;
+		AssignDumpId(dobj);
+		dobj->dependencies = (DumpId *) pg_malloc(sizeof(DumpId));
+		dobj->dependencies[0] = rel->dumpId;
+		dobj->nDeps = 1;
+		dobj->allocDeps = 1;
+		dobj->components |= DUMP_COMPONENT_STATISTICS;
+		dobj->dump = rel->dump;
+		dobj->name = pg_strdup(rel->name);
+		dobj->namespace = rel->namespace;
+		info->relkind = relkind;
+
+		return info;
+	}
+	return NULL;
+}
+
 /*
  * getTables
  *	  read all the tables (no indexes) in the system catalogs,
@@ -7068,6 +7131,7 @@ getTables(Archive *fout, int *numTables)
 
 		/* Tables have data */
 		tblinfo[i].dobj.components |= DUMP_COMPONENT_DATA;
+		tblinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
 
 		/* Mark whether table has an ACL */
 		if (!PQgetisnull(res, i, i_relacl))
@@ -7116,6 +7180,8 @@ getTables(Archive *fout, int *numTables)
 				}
 			}
 		}
+		if (tblinfo[i].interesting)
+			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relkind);
 	}
 
 	if (query->len != 0)
@@ -7560,11 +7626,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 		for (int c = 0; c < numinds; c++, j++)
 		{
 			char		contype;
+			char		indexkind;
+			RelStatsInfo   *relstats;
 
 			indxinfo[j].dobj.objType = DO_INDEX;
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -7587,7 +7656,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			{
 				NULL, NULL
 			};
+
+			if (indxinfo[j].parentidx == 0)
+				indexkind = RELKIND_INDEX;
+			else
+				indexkind = RELKIND_PARTITIONED_INDEX;
+
 			contype = *(PQgetvalue(res, j, i_contype));
+			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, indexkind);
 
 			if (contype == 'p' || contype == 'u' || contype == 'x')
 			{
@@ -7621,6 +7697,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				constrinfo->separate = true;
 
 				indxinfo[j].indexconstraint = constrinfo->dobj.dumpId;
+				if (relstats != NULL)
+					addObjectDependency(&relstats->dobj, constrinfo->dobj.dumpId);
 			}
 			else
 			{
@@ -10343,6 +10421,296 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * Tabular description of the parameters to pg_set_relation_stats()
+ * required, param_name, param_type
+ */
+static const char *rel_stats_arginfo[][3] = {
+	{"t", "relation", "regclass"},
+	{"t", "version", "integer"},
+	{"f", "relpages", "integer"},
+	{"f", "reltuples", "real"},
+	{"f", "relallvisible", "integer"},
+};
+
+/*
+ * Tabular description of the parameters to pg_set_attribute_stats()
+ * required, param_name, param_type
+ */
+static const char *att_stats_arginfo[][3] = {
+	{"t", "relation", "regclass"},
+	{"t", "attname", "name"},
+	{"t", "inherited", "boolean"},
+	{"t", "version", "integer"},
+	{"f", "null_frac", "float4"},
+	{"f", "avg_width", "integer"},
+	{"f", "n_distinct", "float4"},
+	{"f", "most_common_vals", "text"},
+	{"f", "most_common_freqs", "float4[]"},
+	{"f", "histogram_bounds", "text"},
+	{"f", "correlation", "float4"},
+	{"f", "most_common_elems", "text"},
+	{"f", "most_common_elem_freqs", "float4[]"},
+	{"f", "elem_count_histogram", "float4[]"},
+	{"f", "range_length_histogram", "text"},
+	{"f", "range_empty_frac", "float4"},
+	{"f", "range_bounds_histogram", "text"},
+};
+
+/*
+ * getRelStatsExportQuery --
+ *
+ * Generate a query that will fetch all relation (e.g. pg_class)
+ * stats for a given relation.
+ */
+static void
+getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "current_setting('server_version_num') AS version, "
+						 "c.relpages, c.reltuples, c.relallvisible "
+						 "FROM pg_class c "
+						 "JOIN pg_namespace n "
+						 "ON n.oid = c.relnamespace "
+						 "WHERE n.nspname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND c.relname = ");
+	appendStringLiteralAH(query, relname, fout);
+}
+
+/*
+ * getAttStatsExportQuery --
+ *
+ * Generate a query that will fetch all attribute (e.g. pg_statistic)
+ * stats for a given relation.
+ */
+static void
+getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "s.attname,"
+						 "s.inherited,"
+						 "current_setting('server_version_num') AS version, "
+						 "s.null_frac,"
+						 "s.avg_width,"
+						 "s.n_distinct,"
+						 "s.most_common_vals,"
+						 "s.most_common_freqs,"
+						 "s.histogram_bounds,"
+						 "s.correlation,"
+						 "s.most_common_elems,"
+						 "s.most_common_elem_freqs,"
+						 "s.elem_count_histogram,");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 "s.range_length_histogram,"
+							 "s.range_empty_frac,"
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(query,
+							 "NULL AS range_length_histogram,"
+							 "NULL AS range_empty_frac,"
+							 "NULL AS range_bounds_histogram ");
+
+	appendPQExpBufferStr(query,
+						 "FROM pg_stats s "
+						 "JOIN pg_namespace n "
+						 "ON n.nspname = s.schemaname "
+						 "JOIN pg_class c "
+						 "ON c.relname = s.tablename "
+						 "AND c.relnamespace = n.oid "
+						 "WHERE s.schemaname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND s.tablename = ");
+	appendStringLiteralAH(query, relname, fout);
+	appendPQExpBufferStr(query, " ORDER BY s.attname, s.inherited");
+}
+
+
+/*
+ * appendNamedArgument --
+ *
+ * Convenience routine for constructing parameters of the form:
+ * 'paraname', 'value'::type
+ */
+static void
+appendNamedArgument(PQExpBuffer out, Archive *fout,
+					const char *argname, bool positional,
+					const char *argval, const char *argtype)
+{
+	appendPQExpBufferStr(out, "\t");
+
+	if (!positional)
+	{
+		appendStringLiteralAH(out, argname, fout);
+		appendPQExpBufferStr(out, ", ");
+	}
+
+	appendStringLiteralAH(out, argval, fout);
+	appendPQExpBuffer(out, "::%s", argtype);
+}
+
+/*
+ * appendRelStatsImport --
+ *
+ * Append a formatted pg_set_relation_stats statement.
+ */
+static void
+appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	const char *sep = "";
+
+	if (PQntuples(res) == 0)
+		return;
+
+	appendPQExpBufferStr(out, "SELECT pg_catalog.pg_set_relation_stats(\n");
+
+	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
+	{
+		bool		positional = (rel_stats_arginfo[argno][0][0] == 't');
+		const char *argname = rel_stats_arginfo[argno][1];
+		const char *argtype = rel_stats_arginfo[argno][2];
+		int			fieldno = PQfnumber(res, argname);
+
+		if (fieldno < 0)
+			pg_fatal("relation stats export query missing field '%s'",
+					 argname);
+
+		if (PQgetisnull(res, 0, fieldno))
+		{
+			if (!positional)
+				pg_fatal("relation stats export query unexpected NULL in '%s'",
+						 argname);
+			else
+				continue;
+		}
+
+		appendPQExpBufferStr(out, sep);
+		appendNamedArgument(out, fout, argname, positional,
+							PQgetvalue(res, 0, fieldno), argtype);
+
+		sep = ",\n";
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+/*
+ * appendAttStatsImport --
+ *
+ * Append a series of formatted pg_set_attribute_stats statements.
+ */
+static void
+appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	{
+		const char *sep = "";
+
+		appendPQExpBufferStr(out, "SELECT pg_catalog.pg_set_attribute_stats(\n");
+		for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
+		{
+			bool		positional = (att_stats_arginfo[argno][0][0] == 't');
+			const char *argname = att_stats_arginfo[argno][1];
+			const char *argtype = att_stats_arginfo[argno][2];
+			int			fieldno = PQfnumber(res, argname);
+
+			if (fieldno < 0)
+				pg_fatal("attribute stats export query missing field '%s'",
+						 argname);
+
+			if (PQgetisnull(res, rownum, fieldno))
+			{
+				if (positional)
+					pg_fatal("attribute stats export query unexpected NULL in '%s'",
+							 argname);
+				else
+					continue;
+			}
+
+			appendPQExpBufferStr(out, sep);
+			appendNamedArgument(out, fout, argname, positional,
+								PQgetvalue(res, rownum, fieldno), argtype);
+			sep = ",\n";
+		}
+		appendPQExpBufferStr(out, "\n);\n");
+	}
+}
+
+/*
+ * get statistics dump section, which depends on the parent object type
+ *
+ * objects created in SECTION_PRE_DATA have stats in SECTION_DATA
+ * objects created in SECTION_POST_DATA have stats in SECTION_POST_DATA
+ */
+static teSection
+statisticsDumpSection(const RelStatsInfo *rsinfo)
+{
+
+	if ((rsinfo->relkind == RELKIND_MATVIEW) ||
+		(rsinfo->relkind == RELKIND_INDEX) ||
+		(rsinfo->relkind == RELKIND_PARTITIONED_INDEX))
+			return SECTION_POST_DATA;
+
+	return SECTION_DATA;
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	PGresult	   *res;
+	PQExpBuffer		query;
+	PQExpBuffer		out;
+	PQExpBuffer		tag;
+	DumpableObject *dobj = (DumpableObject *) &rsinfo->dobj;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", "STATISTICS DATA", fmtId(dobj->name));
+
+	query = createPQExpBuffer();
+	out = createPQExpBuffer();
+
+	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendRelStatsImport(out, fout, res);
+	PQclear(res);
+
+	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendAttStatsImport(out, fout, res);
+	PQclear(res);
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATISTICS DATA",
+							  .section = statisticsDumpSection(rsinfo),
+							  .createStmt = out->data,
+							  .deps = dobj->dependencies,
+							  .nDeps = dobj->nDeps));
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(out);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -10791,6 +11159,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_SUBSCRIPTION_REL:
 			dumpSubscriptionTable(fout, (const SubRelInfo *) dobj);
 			break;
+		case DO_REL_STATS:
+			dumpRelationStats(fout, (const RelStatsInfo *) dobj);
+			break;
 		case DO_PRE_DATA_BOUNDARY:
 		case DO_POST_DATA_BOUNDARY:
 			/* never dumped, nothing to do */
@@ -17101,6 +17472,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 		free(indstatvalsarray);
 	}
 
+	/* Comments and stats share same .dep */
+
 	/* Dump Index Comments */
 	if (indxinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
 		dumpComment(fout, "INDEX", qindxname,
@@ -18798,6 +19171,16 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 				/* must come after the pre-data boundary */
 				addObjectDependency(dobj, preDataBound->dumpId);
 				break;
+			case DO_REL_STATS:
+				/* stats section varies by parent object type, DATA or POST */
+				if (statisticsDumpSection((RelStatsInfo *) dobj) == SECTION_DATA)
+				{
+					addObjectDependency(dobj, preDataBound->dumpId);
+					addObjectDependency(postDataBound, dobj->dumpId);
+				}
+				else
+					addObjectDependency(dobj, postDataBound->dumpId);
+				break;
 		}
 	}
 }
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 2a7c5873a0..86f984d579 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -84,6 +84,7 @@ typedef enum
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION,
 	DO_SUBSCRIPTION_REL,		/* see note for SubRelInfo */
+	DO_REL_STATS,
 } DumpableObjectType;
 
 /*
@@ -101,6 +102,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
@@ -421,6 +423,12 @@ typedef struct _indexAttachInfo
 	IndxInfo   *partitionIdx;	/* link to index on partition */
 } IndexAttachInfo;
 
+typedef struct _relStatsInfo
+{
+	DumpableObject	dobj;
+	char			relkind;		/* 'r', 'v', 'c', etc */
+} RelStatsInfo;
+
 typedef struct _statsExtInfo
 {
 	DumpableObject dobj;
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 4cb754caa5..cfe277f5ce 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -1490,6 +1490,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "POST-DATA BOUNDARY  (ID %d)",
 					 obj->dumpId);
 			return;
+		case DO_REL_STATS:
+			snprintf(buf, bufsize,
+					 "RELATION STATISTICS FOR %s  (ID %d OID %u)",
+					 obj->name, obj->dumpId, obj->catId.oid);
+			return;
 	}
 	/* shouldn't get here */
 	snprintf(buf, bufsize,
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 73337f3392..c4eaff3063 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -103,6 +103,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -172,6 +173,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -451,6 +453,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -666,6 +670,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 62821cbee4..984c113560 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -75,6 +75,7 @@ main(int argc, char **argv)
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
 	static int	no_subscriptions = 0;
+	static int  no_statistics = 0;
 	static int	strict_names = 0;
 
 	struct option cmdopts[] = {
@@ -107,6 +108,7 @@ main(int argc, char **argv)
 		{"username", 1, NULL, 'U'},
 		{"verbose", 0, NULL, 'v'},
 		{"single-transaction", 0, NULL, '1'},
+		{"statistics-only", no_argument, NULL, 'P'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -127,6 +129,7 @@ main(int argc, char **argv)
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
+		{"no-statistics", no_argument, &no_statistics, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -270,6 +273,10 @@ main(int argc, char **argv)
 				opts->aclsSkip = 1;
 				break;
 
+			case 'X':			/* Restore statistics only */
+				opts->statisticsOnly = 1;
+				break;
+
 			case '1':			/* Restore data in a single transaction */
 				opts->single_txn = true;
 				opts->exit_on_error = true;
@@ -374,6 +381,7 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
diff --git a/src/bin/pg_dump/t/001_basic.pl b/src/bin/pg_dump/t/001_basic.pl
index b9d13a0e1d..b1c7a10919 100644
--- a/src/bin/pg_dump/t/001_basic.pl
+++ b/src/bin/pg_dump/t/001_basic.pl
@@ -46,8 +46,8 @@ command_fails_like(
 
 command_fails_like(
 	[ 'pg_dump', '-s', '-a' ],
-	qr/\Qpg_dump: error: options -s\/--schema-only and -a\/--data-only cannot be used together\E/,
-	'pg_dump: options -s/--schema-only and -a/--data-only cannot be used together'
+	qr/\Qpg_dump: error: options -s\/--schema-only, -a\/--data-only, and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: options -s/--schema-only, -a/--data-only, and -X/--statistics-only cannot be used together'
 );
 
 command_fails_like(
@@ -56,6 +56,12 @@ command_fails_like(
 	'pg_dump: options -s/--schema-only and --include-foreign-data cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '--statistics-only', '--no-statistics' ],
+	qr/\Qpg_dump: error: options -X\/--statistics-only and --no-statistics cannot be used together\E/,
+	'pg_dump: options -X\/--statistics-only and --no-statistics cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-j2', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: option --include-foreign-data is not supported with parallel backup\E/,
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index 08d775379f..037273701b 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -119,7 +119,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Dump only the data, not the schema (data definitions).
+        Dump only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are dumped.
        </para>
 
@@ -137,8 +137,9 @@ PostgreSQL documentation
       <listitem>
        <para>
         Include large objects in the dump.  This is the default behavior
-        except when <option>--schema</option>, <option>--table</option>, or
-        <option>--schema-only</option> is specified.  The <option>-b</option>
+        except when <option>--schema</option>, <option>--table</option>,
+        <option>--schema-only</option>, or
+        <option>--statistics-only</option> is specified.  The <option>-b</option>
         switch is therefore only useful to add large objects to dumps
         where a specific schema or table has been requested.  Note that
         large objects are considered data and therefore will be included when
@@ -512,10 +513,11 @@ PostgreSQL documentation
       <term><option>--schema-only</option></term>
       <listitem>
        <para>
-        Dump only the object definitions (schema), not data.
+        Dump only the object definitions (schema), not data or statistics.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive to <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
         <option>--section=pre-data --section=post-data</option>.
@@ -648,6 +650,18 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Dump only the statistics, not the schema (data definitions) or data.
+        Statistics for tables, materialized views, and indexes are dumped.
+       </para>
+
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-Z <replaceable class="parameter">level</replaceable></option></term>
       <term><option>-Z <replaceable class="parameter">method</replaceable></option>[:<replaceable>detail</replaceable>]</term>
@@ -829,7 +843,8 @@ PostgreSQL documentation
         though you do not need the data in it.
        </para>
        <para>
-        To exclude data for all tables in the database, see <option>--schema-only</option>.
+        To exclude data for all tables in the database, see <option>--schema-only</option>
+        or <option>--statistics-only</option>.
        </para>
       </listitem>
      </varlistentry>
@@ -1081,6 +1096,15 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index 2e3ba80258..8b1658e648 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -94,7 +94,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Restore only the data, not the schema (data definitions).
+        Restore only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are restored,
         if present in the archive.
        </para>
@@ -483,10 +483,11 @@ PostgreSQL documentation
         to the extent that schema entries are present in the archive.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive of <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
-        <option>--section=pre-data --section=post-data</option>.
+        <option>--section=pre-data --section=post-data --no-statistics</option>.
        </para>
        <para>
         (Do not confuse this with the <option>--schema</option> option, which
@@ -599,6 +600,20 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Restore only the statistics, not schema or data.
+       </para>
+       <para>
+        (Do not confuse this with the <option>--schema</option> option, which
+        uses the word <quote>schema</quote> in a different meaning.)
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-1</option></term>
       <term><option>--single-transaction</option></term>
@@ -723,6 +738,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore statistics, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
-- 
2.45.0

#166Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#165)
Re: Statistics Import and Export

On Mon, 2024-05-06 at 23:43 -0400, Corey Huinker wrote:

v21 attached.

0003 is the statistics changes to pg_dump, adding the options -X / --
statistics-only, and the derivative boolean statisticsOnly. The -P
option is already used by pg_restore, so instead I chose -X because
of the passing resemblance to Chi as in the chi-square statistics
test makes it vaguely statistics-ish. If someone has a better letter,
I'm listening.

With that change, people should be able to use pg_dump -X --table=foo
to dump existing stats for a table and its dependent indexes, and
then tweak those calls to do tuning work. Have fun with it. If this
becomes a common use-case then it may make sense to get functions to
fetch relation/attribute stats for a given relation, either as a
formed SQL statement or as the parameter values.

Can you explain what you did with the
SECTION_NONE/SECTION_DATA/SECTION_POST_DATA over v19-v21 and why?

Regards,
Jeff Davis

#167Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#166)
Re: Statistics Import and Export

Can you explain what you did with the
SECTION_NONE/SECTION_DATA/SECTION_POST_DATA over v19-v21 and why?

Initially, I got things to work by having statistics import behave like
COMMENTs, which meant that they were run immediately after the
table/matview/index/constraint that created the pg_class/pg_attribute
entries, but they could be suppressed with a --noX flag

Per previous comments, it was suggested by others that:

- having them in SECTION_NONE was a grave mistake
- Everything that could belong in SECTION_DATA should, and the rest should
be in SECTION_POST_DATA
- This would almost certainly require the statistics import commands to be
TOC objects (one object per pg_class entry, not one object per function
call)

Turning them into TOC objects was a multi-phase process.

1. the TOC entries are generated with dependencies (the parent pg_class
object as well as the potential unique/pk constraint in the case of
indexes), but no statements are generated (in case the stats are filtered
out or the parent object is filtered out). This TOC entry must have
everything we'll need to later generate the function calls. So far, that
information is the parent name, parent schema, and relkind of the parent
object.

2. The TOC entries get sorted by dependencies, and additional dependencies
are added which enforce the PRE/DATA/POST boundaries. This is where knowing
the parent object's relkind is required, as that determines the DATA/POST
section.

3. Now the TOC entry is able to stand on its own, and generate the
statements if they survive the dump/restore filters. Most of the later
versions of the patch were efforts to get the objects to fall into the
right PRE/DATA/POST sections, and the central bug was that the dependencies
passed into ARCHIVE_OPTS were incorrect, as the dependent object passed in
was now the new TOC object, not the parent TOC object. Once that was
resolved, things fell into place.

#168Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#167)
Re: Statistics Import and Export

On Thu, 2024-05-16 at 05:25 -0400, Corey Huinker wrote:

Per previous comments, it was suggested by others that:

- having them in SECTION_NONE was a grave mistake
- Everything that could belong in SECTION_DATA should, and the rest
should be in SECTION_POST_DATA

I don't understand the gravity of the choice here: what am I missing?

To be clear: I'm not arguing against it, but I'd like to understand it
better. Perhaps it has to do with the relationship between the sections
and the dependencies?

Regards,
Jeff Davis

#169Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#168)
3 attachment(s)
Re: Statistics Import and Export

On Thu, May 16, 2024 at 2:26 PM Jeff Davis <pgsql@j-davis.com> wrote:

On Thu, 2024-05-16 at 05:25 -0400, Corey Huinker wrote:

Per previous comments, it was suggested by others that:

- having them in SECTION_NONE was a grave mistake
- Everything that could belong in SECTION_DATA should, and the rest
should be in SECTION_POST_DATA

I don't understand the gravity of the choice here: what am I missing?

To be clear: I'm not arguing against it, but I'd like to understand it
better. Perhaps it has to do with the relationship between the sections
and the dependencies?

I'm with you, I don't understand the choice and would like to, but at the
same time it now works in the way others strongly suggested that it should,
so I'm still curious about the why.

There were several people expressing interest in this patch at pgconf.dev,
so I thought I'd post a rebase and give a summary of things to date.

THE INITIAL GOAL

The initial goal of this effort was to reduce upgrade downtimes by
eliminating the need for the vacuumdb --analyze-in-stages call that is
recommended (but often not done) after a pg_upgrade. The analyze-in-stages
steps is usually by far the longest part of a binary upgrade and is a
significant part of a restore from dump, so eliminating this step will save
users time, and eliminate or greatly reduce a potential pitfall to
upgrade...and thus reduce upgrade friction (read: excuses to not upgrade).

THE FUNCTIONS

These patches introduce two functions, pg_set_relation_stats() and
pg_set_attribute_stats(), which allow the caller to modify the statistics
of any relation, provided that they own that relation or have maintainer
privilege.

The function pg_set_relation_stats looks like this:

SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
150000::integer,
'relpages', 17::integer,
'reltuples', 400.0::real,
'relallvisible', 4::integer);

The function takes an oid of the relation to have stats imported, a version
number (SERVER_VERSION_NUM) for the source of the statistics, and then a
series of varargs organized as name-value pairs. Currently, three arg pairs
are required to properly set (relpages, reltuples, and relallvisible). If
all three are not present, the function will issue a warning, and the row
will not be updated.

The choice of varargs is a defensive one, basically ensuring that a
pgdump that includes statistics import calls will not fail on a future
version that does not have one or more of these values. The call itself
would fail to modify the relation row, but it wouldn't cause the whole
restore to fail. I'm personally not against having a fixed arg version of
this function, nor am I against having both at the same time, the varargs
version basically teeing up the fixed-param call appropriate for the
destination server version.

This function does an in-place update of the pg_class row to avoid bloat
pg_class, just like ANALYZE does. This means that this function call is
NON-transactional.

The function pg_set_attribute_stats looks like this:

SELECT pg_catalog.pg_set_attribute_stats(
'stats_export_import.test'::regclass,
'id'::name,
false::boolean,
150000::integer,
'null_frac', 0.5::real,
'avg_width', 2::integer,
'n_distinct', -0.1::real,
'most_common_vals', '{2,1,3}'::text,
'most_common_freqs', '{0.3,0.25,0.05}'::real[]
);

Like the first function, it takes a relation oid and a source server
version though that is in the 4th position. It also takes the name of an
attribute, and a boolean as to whether these stats are for inherited
statistics (true) or regular (false). Again what follows is a vararg list
of name-value pairs, each name corresponding to an attribute of pg_stats,
and expecting a value appropriate for said attribute of pg_stats. Note that
ANYARRAY values are passed in as text. This is done for a few reasons.
First, if the attribute is an array type, then the most_common_elements
value will be an array of that array type, and there is no way to represent
that in SQL (it instead gives a higher order array of the same base type).
Second, it allows us to import the values with a simple array_in() call.
Last, it allows for situations where the type name changed from source
system to destination (example: a schema-qualified extension type gets
moved to core).

There are lots of ways that this function call can go wrong. An invalid
attribute name, an invalid parameter name in a name-value pair, invalid
data type of parameter being passed in the value of a name-value pair, or
type coercion errors in array_in() to name just a few. All of these errors
result in a warning and the import failing, but the function completes
normally. Internal typecasting and array_in are all done with the _safe()
equivalents, and any such errors are re-emitted as warnings. The central
goal here is to not make a restore fail just because the statistics are
wonky.

Calls to pg_set_attribute_stats() are transactional. This wouldn't warrant
mentioning if not for pg_set_relation_stats() being non-transactional.

DUMP / RESTORE / UPGRADE

The code for pg_dump/restore/upgrade has been modified to allow for
statistics to be exported/imported by default. There are flags to prevent
this (--no-statistics) and there are flags to ONLY do statistics
(--statistics-only) the utility of which will be discussed later.

pg_dump will make queries of the source database, adjusting the syntax to
reflect the version of the source system. There is very little variance in
those queries, so it should be possible to query as far back as 9.2 and get
usable stats. The output of these calls will be a series of SELECT
statements, each one making a call to either pg_set_relation_stats (one per
table/index/matview) or pg_set_attribute_stats (one per attribute that had
a matching pg_statistic row).

The positioning of these calls in the restore sequence was originally set
up as SECTION_NONE, but it was strongly suggested that SECTION_DATA /
SECTION_POST_DATA was the right spot instead, and that's where they
currently reside.

The end result will be that the new database now has the stats identical
(or at least close to) the source system. Those statistics might be good or
bad, but they're almost certainly better than no stats at all. Even if they
are bad, they will be overwritten by the next ANALYZE or autovacuum.

WHAT IS NOT DONE

1. Extended Statistics, which are considerably more complex than regular
stats (stxdexprs is itself an array of pg_statistic rows) and thus more
difficult to express in a simple function call. They are also used fairly
rarely in customer installations, so leaving them out of the v1 patch
seemed like an easy trade-off.

2. Any sort of validity checking beyond data-types. This was initially
provided, verifying that arrays values representing frequencies must be
between 0.0 and 1.0, arrays that represent most common value frequencies
must be in monotonically non-increasing order, etc. but these were rejected
as being overly complex, potentially rejecting valid stats, and getting in
the way of an other use I hadn't considered.

3. Export functions. Strictly speaking we don't need them, but some
use-cases described below may make the case for including them.

OTHER USES

Usage of these functions is not restricted to upgrade/restore situations.
The most obvious use was to experiment with how the planner behaves when
one or more tables grow and/or skew. It is difficult to create a table with
10 billion rows in it, but it's now trivial to create a table that says it
has 10 billion rows in it.

This can be taken a step further, and in a way I had not anticipated -
actively stress-testing the planner by inserting wildly incorrect and/or
nonsensical stats. In that sense, these functions are a fuzzing tool that
happens to make upgrades go faster.

FUTURE PLANS

Integration with postgres_fdw is an obvious next step, allowing an ANALYZE
on a foreign table to, instead of asking for a remote row sample, to simply
export the stats of the remote table and import them into the foreign table.

Extended Statistics.

CURRENT PROGRESS

I believe that all outstanding questions/request were addressed, and the
patch is now back to needing a review.

FOR YOUR CONSIDERATION

Rebase (current as of f04d1c1db01199f02b0914a7ca2962c531935717) attached.

Attachments:

v22-0001-Create-pg_set_relation_stats-pg_set_attribute_st.patchtext/x-patch; charset=US-ASCII; name=v22-0001-Create-pg_set_relation_stats-pg_set_attribute_st.patchDownload
From fee18b3f6dcf781f91871cb54448e9e06675f5b9 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 21 Mar 2024 18:04:54 -0400
Subject: [PATCH v22 1/3] Create pg_set_relation_stats, pg_set_attribute_stats.

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

Both functions take an oid to identify the target relation that will
receive the statistics. There is nothing requiring that relation to be
the same one as the one exported, though the statistics supplied have
to make sense in the context of the new relation.

Both functions take a variadic list of statistic parameters name-value
pairs.

Each name given for pg_set_relation_stats must correspond to a
statistics attribute in pg_class, and the paired value must match the
datatype of said attribute in pg_class.

Each name given for pg_set_attribute_stats must correspond to a
statistics attribute in pg_stats, and the paired value must match the
datatype of said attribute in pg_stats, except for attributes of type
ANYARRAY, in which case a TEXT value is required.

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

These functions 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               |   19 +
 src/include/statistics/statistics.h           |    2 +
 src/backend/statistics/Makefile               |    3 +-
 src/backend/statistics/meson.build            |    1 +
 src/backend/statistics/statistics.c           | 1271 +++++++++++++++++
 .../regress/expected/stats_export_import.out  |  853 +++++++++++
 src/test/regress/parallel_schedule            |    3 +-
 src/test/regress/sql/stats_export_import.sql  |  671 +++++++++
 doc/src/sgml/func.sgml                        |  267 ++++
 9 files changed, 3088 insertions(+), 2 deletions(-)
 create mode 100644 src/backend/statistics/statistics.c
 create mode 100644 src/test/regress/expected/stats_export_import.out
 create mode 100644 src/test/regress/sql/stats_export_import.sql

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 6a5476d3c4..09abb515f4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12185,4 +12185,23 @@
   proargnames => '{summarized_tli,summarized_lsn,pending_lsn,summarizer_pid}',
   prosrc => 'pg_get_wal_summarizer_state' },
 
+# Statistics Import
+{ oid => '8048',
+  descr => 'set statistics on relation',
+  proname => 'pg_set_relation_stats', provariadic => 'any',
+  proisstrict => 't', provolatile => 'v',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'regclass int4 any',
+  proargnames => '{relation,version,stats}',
+  proargmodes => '{i,i,v}',
+  prosrc => 'pg_set_relation_stats' },
+{ oid => '8049',
+  descr => 'set statistics on attribute',
+  proname => 'pg_set_attribute_stats', provariadic => 'any',
+  proisstrict => 't', provolatile => 'v',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'regclass name bool int4 any',
+  proargnames => '{relation,attname,inherited,version,stats}',
+  proargmodes => '{i,i,i,i,v}',
+  prosrc => 'pg_set_attribute_stats' },
 ]
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 7f2bf18716..1dddf96576 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -127,4 +127,6 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												int nclauses);
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
+extern Datum pg_set_relation_stats(PG_FUNCTION_ARGS);
+extern Datum pg_set_attribute_stats(PG_FUNCTION_ARGS);
 #endif							/* STATISTICS_H */
diff --git a/src/backend/statistics/Makefile b/src/backend/statistics/Makefile
index 89cf8c2797..e4f8ab7c4f 100644
--- a/src/backend/statistics/Makefile
+++ b/src/backend/statistics/Makefile
@@ -16,6 +16,7 @@ OBJS = \
 	dependencies.o \
 	extended_stats.o \
 	mcv.o \
-	mvdistinct.o
+	mvdistinct.o \
+	statistics.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/statistics/meson.build b/src/backend/statistics/meson.build
index 73b29a3d50..331e82c776 100644
--- a/src/backend/statistics/meson.build
+++ b/src/backend/statistics/meson.build
@@ -5,4 +5,5 @@ backend_sources += files(
   'extended_stats.c',
   'mcv.c',
   'mvdistinct.c',
+  'statistics.c',
 )
diff --git a/src/backend/statistics/statistics.c b/src/backend/statistics/statistics.c
new file mode 100644
index 0000000000..e092eb3dc3
--- /dev/null
+++ b/src/backend/statistics/statistics.c
@@ -0,0 +1,1271 @@
+/*-------------------------------------------------------------------------
+ * statistics.c
+ *
+ *	  POSTGRES statistics import
+ *
+ * Code supporting the direct importation of relation statistics, similar to
+ * what is done by the ANALYZE command.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *       src/backend/statistics/statistics.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_collation.h"
+#include "catalog/pg_database.h"
+#include "catalog/pg_operator.h"
+#include "catalog/pg_type.h"
+#include "fmgr.h"
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "nodes/nodeFuncs.h"
+#include "statistics/statistics.h"
+#include "utils/acl.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/float.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/rangetypes.h"
+#include "utils/syscache.h"
+#include "utils/typcache.h"
+
+/*
+ * A role has privileges to set statistics on the relation if any of the
+ * following are true:
+ *   - the role owns the current database and the relation is not shared
+ *   - the role has the MAINTAIN privilege on the relation
+ *
+ */
+static bool
+can_modify_relation(Relation rel)
+{
+	Oid			relid = RelationGetRelid(rel);
+	Form_pg_class reltuple = rel->rd_rel;
+
+	return ((object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId())
+			 && !reltuple->relisshared) ||
+			pg_class_aclcheck(relid, GetUserId(), ACL_MAINTAIN) == ACLCHECK_OK);
+}
+
+/*
+ * Set statistics for a given pg_class entry.
+ *
+ * This does an in-place (i.e. non-transactional) update of pg_class, just as
+ * is done in ANALYZE.
+ *
+ */
+Datum
+pg_set_relation_stats(PG_FUNCTION_ARGS)
+{
+	Oid			relid = PG_GETARG_OID(0);
+	int			version = PG_GETARG_INT32(1);
+
+	/* indexes of where we found required stats */
+	int			i_relpages = 0;
+	int			i_reltuples = 0;
+	int			i_relallvisible = 0;
+
+	/* build argument values to build the object */
+	Datum	   *args;
+	bool	   *nulls;			/* placeholder, because strict */
+	Oid		   *types;
+	int			nargs;
+
+	/* Minimum version supported */
+	if (version <= 90200)
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Cannot export statistics prior to version 9.2")));
+
+	nargs = extract_variadic_args(fcinfo, 2, true, &args, &types, &nulls);
+
+	/* if the pairs aren't pairs, something is malformed */
+	if (nargs % 2 == 1)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("stats parameters must be in name-value pairs")));
+		PG_RETURN_BOOL(false);
+	}
+
+	/* loop through args, matching params to their arg indexes */
+	for (int i = 0; i < nargs; i += 2)
+	{
+		char	   *statname;
+		int			argidx = i + 1;
+
+		if (types[i] != TEXTOID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("stat names must be of type text")));
+			continue;
+		}
+		statname = TextDatumGetCString(args[i]);
+
+		/*
+		 * match each named parameter to the index of the value that follows
+		 * it
+		 */
+		if (strcmp(statname, "relpages") == 0)
+			i_relpages = argidx;
+		else if (strcmp(statname, "reltuples") == 0)
+			i_reltuples = argidx;
+		else if (strcmp(statname, "relallvisible") == 0)
+			i_relallvisible = argidx;
+		else
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("unknown stat naame '%s', skipping", statname)));
+
+		pfree(statname);
+	}
+
+	/*
+	 * Ensure that we got all required parameters, and they are of the correct
+	 * type.
+	 */
+	if (i_relpages == 0)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "relpages")));
+		PG_RETURN_BOOL(false);
+	}
+	else if (types[i_relpages] != INT4OID)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relpages must be of type integer")));
+		PG_RETURN_BOOL(false);
+	}
+	else if (i_reltuples == 0)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "reltuples")));
+		PG_RETURN_BOOL(false);
+	}
+	else if (types[i_reltuples] != FLOAT4OID)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("reltuples must be of type real")));
+		PG_RETURN_BOOL(false);
+	}
+	else if (types[i_relallvisible] != INT4OID)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "relallvisible")));
+		PG_RETURN_BOOL(false);
+	}
+	else if (i_relallvisible == -1)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relallvisible must be of type integer")));
+		PG_RETURN_BOOL(false);
+	}
+	else
+	{
+		/*
+		 * Open the relation, getting ShareUpdateExclusiveLock to ensure that
+		 * no other stat-setting operation can run on it concurrently.
+		 */
+		Relation	rel;
+		HeapTuple	ctup;
+		Form_pg_class pgcform;
+		int			relpages;
+		float4		reltuples;
+		int			relallvisible;
+
+		rel = table_open(RelationRelationId, ShareUpdateExclusiveLock);
+
+		if (!can_modify_relation(rel))
+		{
+			table_close(rel, NoLock);
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("must be owner to modify relation stats")));
+			PG_RETURN_BOOL(false);
+		}
+
+		relpages = DatumGetInt32(args[i_relpages]);
+		reltuples = DatumGetFloat4(args[i_reltuples]);
+		relallvisible = DatumGetInt32(args[i_relallvisible]);
+
+		ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+		if (!HeapTupleIsValid(ctup))
+			ereport(ERROR,
+					(errcode(ERRCODE_OBJECT_IN_USE),
+					 errmsg("pg_class entry for relid %u not found", relid)));
+
+		pgcform = (Form_pg_class) GETSTRUCT(ctup);
+		/* Only update pg_class if there is a meaningful change */
+		if ((pgcform->reltuples != reltuples)
+			|| (pgcform->relpages != relpages)
+			|| (pgcform->relallvisible != relallvisible))
+		{
+			pgcform->relpages = relpages;
+			pgcform->reltuples = reltuples;
+			pgcform->relallvisible = relallvisible;
+
+			heap_inplace_update(rel, ctup);
+		}
+
+		table_close(rel, NoLock);
+		PG_RETURN_BOOL(true);
+	}
+
+	PG_RETURN_BOOL(false);
+}
+
+/*
+ * Test if the type is a scalar for MCELEM purposes
+ */
+static bool
+type_is_scalar(Oid typid)
+{
+	HeapTuple	tp;
+	bool		result = false;
+
+	tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+	if (HeapTupleIsValid(tp))
+	{
+		Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+
+		result = (!OidIsValid(typtup->typanalyze));
+		ReleaseSysCache(tp);
+	}
+	return result;
+}
+
+/*
+ * If this relation is an index and that index has expressions in it, and
+ * the attnum specified is known to be an expression, then we must walk
+ * the list attributes up to the specified attnum to get the right
+ * expression.
+ */
+static Node *
+get_attr_expr(Relation rel, int attnum)
+{
+	if ((rel->rd_rel->relkind == RELKIND_INDEX
+		 || (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+		&& (rel->rd_indexprs != NIL)
+		&& (rel->rd_index->indkey.values[attnum - 1] == 0))
+	{
+		ListCell   *indexpr_item = list_head(rel->rd_indexprs);
+
+		for (int i = 0; i < attnum - 1; i++)
+			if (rel->rd_index->indkey.values[i] == 0)
+				indexpr_item = lnext(rel->rd_indexprs, indexpr_item);
+
+		if (indexpr_item == NULL)	/* shouldn't happen */
+			elog(ERROR, "too few entries in indexprs list");
+
+		return (Node *) lfirst(indexpr_item);
+	}
+	return NULL;
+}
+
+/*
+ * Fetch datatype information, this is needed to derive the proper staopN
+ * and stacollN values.
+ *
+ */
+static TypeCacheEntry *
+get_attr_stat_type(Relation rel, Name attname,
+				   int16 *attnum, int32 *typmod, Oid *typcoll)
+{
+	Oid			relid = RelationGetRelid(rel);
+	Form_pg_attribute attr;
+	HeapTuple	atup;
+	Oid			typid;
+	Node	   *expr;
+
+	atup = SearchSysCache2(ATTNAME, ObjectIdGetDatum(relid),
+						   NameGetDatum(attname));
+
+	/* Attribute not found */
+	if (!HeapTupleIsValid(atup))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s has no attname %s",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+		return NULL;
+	}
+
+	attr = (Form_pg_attribute) GETSTRUCT(atup);
+	if (attr->attisdropped)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s attname %s is dropped",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+		return NULL;
+	}
+	*attnum = attr->attnum;
+
+	expr = get_attr_expr(rel, attr->attnum);
+
+	if (expr == NULL)
+	{
+		/* regular attribute */
+		typid = attr->atttypid;
+		*typmod = attr->atttypmod;
+		*typcoll = attr->attcollation;
+	}
+	else
+	{
+		typid = exprType(expr);
+		*typmod = exprTypmod(expr);
+
+		/*
+		 * If a collation has been specified for the index column, use that in
+		 * preference to anything else; but if not, fall back to whatever we
+		 * can get from the expression.
+		 */
+		if (OidIsValid(attr->attcollation))
+			*typcoll = attr->attcollation;
+		else
+			*typcoll = exprCollation(expr);
+	}
+	ReleaseSysCache(atup);
+
+	/* if it's a multirange, step down to the range type */
+	if (type_is_multirange(typid))
+		typid = get_multirange_range(typid);
+
+	return lookup_type_cache(typid,
+							 TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR);
+}
+
+/*
+ * Perform the cast of a known TextDatum into the type specified.
+ *
+ * If no errors are found, ok is set to true. Otherwise, set ok
+ * to false, capture the error found, and re-throw at warning level.
+ */
+static Datum
+cast_stavalues(FmgrInfo *flinfo, Datum d, Oid typid, int32 typmod, bool *ok)
+{
+	LOCAL_FCINFO(fcinfo, 8);
+	char	   *s;
+	Datum		result;
+	ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+	escontext.details_wanted = true;
+
+	s = TextDatumGetCString(d);
+
+	InitFunctionCallInfoData(*fcinfo, flinfo, 3, InvalidOid,
+							 (Node *) &escontext, NULL);
+
+	fcinfo->args[0].value = CStringGetDatum(s);
+	fcinfo->args[0].isnull = false;
+	fcinfo->args[1].value = ObjectIdGetDatum(typid);
+	fcinfo->args[1].isnull = false;
+	fcinfo->args[2].value = Int32GetDatum(typmod);
+	fcinfo->args[2].isnull = false;
+
+	result = FunctionCallInvoke(fcinfo);
+
+	if (SOFT_ERROR_OCCURRED(&escontext))
+	{
+		escontext.error_data->elevel = WARNING;
+		ThrowErrorData(escontext.error_data);
+		*ok = false;
+	}
+	else
+		*ok = true;
+
+	pfree(s);
+
+	return result;
+}
+
+
+/*
+ * Check array for any NULLs, and optionally for one-dimensionality.
+ *
+ * Report any failures as warnings.
+ */
+static bool
+array_check(Datum datum, int one_dim, const char *statname)
+{
+	ArrayType  *arr = DatumGetArrayTypeP(datum);
+	int16		elmlen;
+	char		elmalign;
+	bool		elembyval;
+	Datum	   *values;
+	bool	   *nulls;
+	int			nelems;
+
+	if (one_dim && (arr->ndim != 1))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be a multidimensional array", statname)));
+		return false;
+	}
+
+	get_typlenbyvalalign(ARR_ELEMTYPE(arr), &elmlen, &elembyval, &elmalign);
+
+	deconstruct_array(arr, ARR_ELEMTYPE(arr), elmlen, elembyval, elmalign,
+					  &values, &nulls, &nelems);
+
+	for (int i = 0; i < nelems; i++)
+		if (nulls[i])
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s array cannot contain NULL values", statname)));
+			return false;
+		}
+	return true;
+}
+
+/*
+ * Update the pg_statistic record.
+ */
+static void
+update_pg_statistic(Datum values[], bool nulls[])
+{
+	Relation	sd = table_open(StatisticRelationId, RowExclusiveLock);
+	CatalogIndexState indstate = CatalogOpenIndexes(sd);
+	HeapTuple	oldtup;
+	HeapTuple	stup;
+
+	/* Is there already a pg_statistic tuple for this attribute? */
+	oldtup = SearchSysCache3(STATRELATTINH,
+							 values[Anum_pg_statistic_starelid - 1],
+							 values[Anum_pg_statistic_staattnum - 1],
+							 values[Anum_pg_statistic_stainherit - 1]);
+
+	if (HeapTupleIsValid(oldtup))
+	{
+		/* Yes, replace it */
+		bool		replaces[Natts_pg_statistic];
+
+		for (int i = 0; i < Natts_pg_statistic; i++)
+			replaces[i] = true;
+
+		stup = heap_modify_tuple(oldtup, RelationGetDescr(sd),
+								 values, nulls, replaces);
+		ReleaseSysCache(oldtup);
+		CatalogTupleUpdateWithInfo(sd, &stup->t_self, stup, indstate);
+	}
+	else
+	{
+		/* No, insert new tuple */
+		stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
+		CatalogTupleInsertWithInfo(sd, stup, indstate);
+	}
+
+	heap_freetuple(stup);
+	CatalogCloseIndexes(indstate);
+	table_close(sd, NoLock);
+}
+
+/*
+ * Import statistics for a given relation attribute.
+ *
+ * This will insert/replace a row in pg_statistic for the given combinarion
+ * of relation, attribute, and inherited flag.
+ *
+ * The version parameter is for future use in the events as future versions
+ * may change them meaning of certain parameters.
+ *
+ * The variadic parameters all represent name-value pairs, with the names
+ * corresponding to attributes in pg_stats. Unkown names will generate a
+ * warning.
+ *
+ * Parameters null_frac, avg_width, and n_distinct are required because
+ * those attributes have no default value in pg_statistic.
+ *
+ * The remaining parameters all belong to a specific stakind, and all are
+ * optional. Some stakinds have multiple parameters, and in those cases
+ * both parameters must be specified if one of them is, otherwise a
+ * warning is generated but the rest of the stats may still be imported.
+ *
+ * If there is no attribute with a matching attname in the relation, the
+ * function will raise a warning and return false.
+ *
+ * Parameters corresponding to ANYARRAY columns are instead passed in as text
+ * values, which is a valid input string for an array of the type or element
+ * type of the attribute.
+ */
+Datum
+pg_set_attribute_stats(PG_FUNCTION_ARGS)
+{
+	Oid			relid = PG_GETARG_OID(0);
+	Name		attname = PG_GETARG_NAME(1);
+	bool		inherited = PG_GETARG_BOOL(2);
+	int			version = PG_GETARG_INT32(3);
+
+	/* build argument values to build the object */
+	Datum	   *args;
+	bool	   *argnulls;		/* placeholder, because strict */
+	Oid		   *types;
+	int			nargs = extract_variadic_args(fcinfo, 4, true,
+											  &args, &types, &argnulls);
+
+	Datum		values[Natts_pg_statistic] = {0};
+	bool		nulls[Natts_pg_statistic] = {false};
+
+	/*
+	 * argument indexes for each known statistic
+	 *
+	 * 0 = not found, -1 = error, n > 0 = found
+	 */
+
+	/* parameters that are required get indexes */
+	int			i_null_frac = 0;
+	int			i_avg_width = 0;
+	int			i_n_distinct = 0;
+
+	/* stakind stats are optional */
+	int			i_mc_vals = 0;
+	int			i_mc_freqs = 0;
+	int			i_hist_bounds = 0;
+	int			i_correlation = 0;
+	int			i_mc_elems = 0;
+	int			i_mc_elem_freqs = 0;
+	int			i_elem_count_hist = 0;
+	int			i_range_length_hist = 0;
+	int			i_range_empty_frac = 0;
+	int			i_range_bounds_hist = 0;
+
+	Relation	rel;
+
+	TypeCacheEntry *typcache;
+	TypeCacheEntry *elemtypcache = NULL;
+
+	int16		attnum;
+	int32		typmod;
+	Oid			typcoll;
+
+	FmgrInfo	finfo;
+
+	int			stakind_count;
+
+	int			k = 0;
+
+	if (version <= 90200)
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Cannot export statistics prior to version 9.2")));
+
+	/* if the pairs aren't pairs, something is malformed */
+	if (nargs % 2 == 1)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("stats parameters must be in name-value pairs")));
+		PG_RETURN_BOOL(false);
+	}
+
+	for (int i = 0; i < nargs; i += 2)
+	{
+		char	   *statname;
+		int			argidx = i + 1;
+
+		if (types[i] != TEXTOID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("stat names must be of type text")));
+			break;
+		}
+		statname = TextDatumGetCString(args[i]);
+
+		if (strcmp(statname, "null_frac") == 0)
+			i_null_frac = argidx;
+		else if (strcmp(statname, "avg_width") == 0)
+			i_avg_width = argidx;
+		else if (strcmp(statname, "n_distinct") == 0)
+			i_n_distinct = argidx;
+		else if (strcmp(statname, "most_common_vals") == 0)
+			i_mc_vals = argidx;
+		else if (strcmp(statname, "most_common_freqs") == 0)
+			i_mc_freqs = argidx;
+		else if (strcmp(statname, "histogram_bounds") == 0)
+			i_hist_bounds = argidx;
+		else if (strcmp(statname, "correlation") == 0)
+			i_correlation = argidx;
+		else if (strcmp(statname, "most_common_elems") == 0)
+			i_mc_elems = argidx;
+		else if (strcmp(statname, "most_common_elem_freqs") == 0)
+			i_mc_elem_freqs = argidx;
+		else if (strcmp(statname, "elem_count_histogram") == 0)
+			i_elem_count_hist = argidx;
+		else if (strcmp(statname, "range_length_histogram") == 0)
+			i_range_length_hist = argidx;
+		else if (strcmp(statname, "range_empty_frac") == 0)
+			i_range_empty_frac = argidx;
+		else if (strcmp(statname, "range_bounds_histogram") == 0)
+			i_range_bounds_hist = argidx;
+		else
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("unknown stat naame '%s', skipping", statname)));
+
+		pfree(statname);
+	}
+
+	/* check all required parameters */
+	if (i_null_frac > 0)
+	{
+		if (types[i_null_frac] != FLOAT4OID)
+		{
+			/* required param, not recoverable */
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be of type %s", "null_frac",
+							"real")));
+			PG_RETURN_BOOL(false);
+		}
+	}
+	else
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "null_frac")));
+		PG_RETURN_BOOL(false);
+	}
+
+	if (i_avg_width > 0)
+	{
+		if (types[i_avg_width] != INT4OID)
+		{
+			/* required param, not recoverable */
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be of type %s", "avg_width",
+							"integer")));
+			PG_RETURN_BOOL(false);
+		}
+	}
+	else
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "avg_width")));
+		PG_RETURN_BOOL(false);
+	}
+
+	if (i_n_distinct > 0)
+	{
+		if (types[i_n_distinct] != FLOAT4OID)
+		{
+			/* required param, not recoverable */
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be of type %s", "n_distinct",
+							"real")));
+			PG_RETURN_BOOL(false);
+		}
+	}
+	else
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("required parameter %s not set", "n_distinct")));
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * Look for pair mismatches, if found warn and disable.
+	 */
+	if ((i_mc_vals == 0) != (i_mc_freqs == 0))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be present if %s is missing",
+						(i_mc_vals == 0) ? "most_common_freqs" :
+						"most_common_vals",
+						(i_mc_vals == 0) ? "most_common_vals" :
+						"most_common_freqs")));
+		i_mc_vals = -1;
+		i_mc_freqs = -1;
+	}
+
+	if ((i_mc_elems == 0) != (i_mc_elem_freqs == 0))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be present if %s is missing",
+						(i_mc_elems == 0) ?
+						"most_common_elem_freqs" :
+						"most_common_elems",
+						(i_mc_elems == 0) ?
+						"most_common_elems" :
+						"most_common_elem_freqs")));
+		i_mc_elems = -1;
+		i_mc_elem_freqs = -1;
+	}
+
+	if ((i_range_length_hist == 0) != (i_range_empty_frac == 0))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be present if %s is missing",
+						(i_range_length_hist == 0) ?
+						"range_empty_frac" :
+						"range_length_histogram",
+						(i_range_length_hist == 0) ?
+						"range_length_histogram" :
+						"range_empty_frac")));
+		i_range_length_hist = -1;
+		i_range_empty_frac = -1;
+	}
+
+	rel = relation_open(relid, ShareUpdateExclusiveLock);
+
+	if (!can_modify_relation(rel))
+	{
+		relation_close(rel, NoLock);
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						RelationGetRelationName(rel))));
+		PG_RETURN_BOOL(false);
+	}
+
+	if ((!inherited) &&
+		((rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) ||
+		 (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX)))
+	{
+		relation_close(rel, NoLock);
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("%s is partitioned, can only accepted inherted stats",
+						RelationGetRelationName(rel))));
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * Many of the values that are set for a particular stakind are entirely
+	 * derived from the attribute itself, or it's expression.
+	 */
+	typcache = get_attr_stat_type(rel, attname, &attnum, &typmod, &typcoll);
+	if (typcache == NULL)
+	{
+		relation_close(rel, NoLock);
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * Derive element type if we have stat kinds that need it.
+	 *
+	 * This duplicates some type-specific logic found in various typanalyze
+	 * functions which are called from vacuum's examine_attribute().
+	 */
+	if ((i_mc_elems > 0) || (i_elem_count_hist > 0))
+	{
+		Oid			elemtypid;
+
+		if (typcache->type_id == TSVECTOROID)
+		{
+			/*
+			 * tsvector elems always have a text oid type and default
+			 * collation
+			 */
+			elemtypid = TEXTOID;
+			typcoll = DEFAULT_COLLATION_OID;
+		}
+		else if (typcache->typtype == TYPTYPE_RANGE)
+			elemtypid = get_range_subtype(typcache->type_id);
+		else
+			elemtypid = get_base_element_type(typcache->type_id);
+
+		/* not finding a basetype means we already had it */
+		if (elemtypid == InvalidOid)
+			elemtypid = typcache->type_id;
+
+		/* The stats need the eq_opr, any validation would need the lt_opr */
+		elemtypcache = lookup_type_cache(elemtypid, TYPECACHE_EQ_OPR);
+
+		if (elemtypcache == NULL)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							(i_mc_elems > 0) ?
+							"most_common_elems" :
+							"elem_count_histogram")));
+			i_mc_elems = -1;
+			i_elem_count_hist = -1;
+		}
+	}
+
+	/*
+	 * histogram_bounds and correlation must have a type < operator. WARN and
+	 * skip if this attribute doesn't have one.
+	 */
+	if (typcache->lt_opr == InvalidOid)
+	{
+		if (i_hist_bounds > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"histogram_bounds")));
+			i_hist_bounds = -1;
+		}
+		if (i_correlation > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"correlation")));
+			i_correlation = -1;
+		}
+	}
+
+	/*
+	 * Scalar types can't have most_common_elems, most_common_elem_freqs,
+	 * elem_count_histogram. WARN and skip.
+	 */
+	if (type_is_scalar(typcache->type_id))
+	{
+		if (i_mc_elems > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"most_common_elems")));
+			i_mc_elems = -1;
+		}
+		if (i_mc_elem_freqs > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"most_common_elem_freqs")));
+			i_mc_elem_freqs = -1;
+		}
+		if (i_elem_count_hist > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"elem_count_histogram")));
+			i_elem_count_hist = -1;
+		}
+	}
+
+	/*
+	 * Only range types can have range_length_histogram, range_empty_frac, and
+	 * range_bounds_histogram. WARN and skip
+	 */
+	if ((typcache->typtype != TYPTYPE_MULTIRANGE) &&
+		(typcache->typtype != TYPTYPE_RANGE))
+	{
+		if (i_range_length_hist > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"range_length_histogram")));
+			i_range_length_hist = -1;
+		}
+		if (i_range_empty_frac > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"range_empty_frac")));
+			i_range_empty_frac = -1;
+		}
+		if (i_range_bounds_hist > 0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot accept %s stats, ignored",
+							NameStr(*attname),
+							"range_bounds_histogram")));
+			i_range_bounds_hist = -1;
+		}
+	}
+
+	/*
+	 * count the number of stakinds we still want to set, paired params count
+	 * as one. The count cannot exceed STATISTIC_NUM_SLOTS.
+	 */
+	stakind_count = (int) (i_mc_vals > 0) +
+		(int) (i_mc_elems > 0) +
+		(int) (i_range_length_hist > 0) +
+		(int) (i_hist_bounds > 0) +
+		(int) (i_correlation > 0) +
+		(int) (i_elem_count_hist > 0) +
+		(int) (i_range_bounds_hist > 0);
+
+	if (stakind_count > STATISTIC_NUM_SLOTS)
+	{
+		/*
+		 * This really shouldn't happen, as most datatypes exclude at least
+		 * one of these types of stats.
+		 */
+		relation_close(rel, NoLock);
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("imported statistics must have a maximum of %d slots "
+						"but %d given",
+						STATISTIC_NUM_SLOTS, stakind_count)));
+		PG_RETURN_BOOL(false);
+	}
+
+	fmgr_info(F_ARRAY_IN, &finfo);
+
+	values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(relid);
+	values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(attnum);
+	values[Anum_pg_statistic_stainherit - 1] = BoolGetDatum(inherited);
+	values[Anum_pg_statistic_stanullfrac - 1] = args[i_null_frac];
+	values[Anum_pg_statistic_stawidth - 1] = args[i_avg_width];
+	values[Anum_pg_statistic_stadistinct - 1] = args[i_n_distinct];
+
+	/*
+	 * STATISTIC_KIND_MCV
+	 *
+	 * most_common_freqs: real[]
+	 *
+	 * most_common_vals : ANYARRAY::text
+	 */
+	if (i_mc_vals > 0)
+	{
+		Oid			numberstype = types[i_mc_freqs];
+		Oid			valuestype = types[i_mc_vals];
+		Datum		stanumbers = args[i_mc_freqs];
+		Datum		strvalue = args[i_mc_vals];
+
+		if (get_element_type(numberstype) != FLOAT4OID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"most_common_freqs", "real[]")));
+		}
+		else if (valuestype != TEXTOID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"most_common_vals", "text")));
+		}
+		else
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_MCV);
+			Datum		staop = ObjectIdGetDatum(typcache->eq_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		stavalues;
+			bool		converted = false;
+
+			stavalues = cast_stavalues(&finfo, strvalue, typcache->type_id,
+									   typmod, &converted);
+
+			if (converted &&
+				array_check(stavalues, false, "most_common_vals") &&
+				array_check(stanumbers, true, "most_common_freqs"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_HISTOGRAM
+	 *
+	 * histogram_bounds: ANYARRAY::text
+	 */
+	if (i_hist_bounds > 0)
+	{
+		Oid			valuestype = types[i_hist_bounds];
+		Datum		strvalue = args[i_hist_bounds];
+
+		if (valuestype != TEXTOID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"histogram_bounds", "text")));
+		else
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_HISTOGRAM);
+			Datum		staop = ObjectIdGetDatum(typcache->lt_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		stavalues;
+			bool		converted = false;
+
+			stavalues = cast_stavalues(&finfo, strvalue, typcache->type_id,
+									   typmod, &converted);
+
+			if (converted && array_check(stavalues, false, "histogram_bounds"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_CORRELATION
+	 *
+	 * correlation: real
+	 */
+	if (i_correlation > 0)
+	{
+		if (types[i_correlation] != FLOAT4OID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be of type %s, ignored",
+							"correlation", "real")));
+		else
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_CORRELATION);
+			Datum		staop = ObjectIdGetDatum(typcache->lt_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		elems[] = {args[i_correlation]};
+			ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+			Datum		stanumbers = PointerGetDatum(arry);
+
+			values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+			values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+			values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+			values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+			nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+			k++;
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_MCELEM
+	 *
+	 * most_common_elem_freqs: real[]
+	 *
+	 * most_common_elems     : ANYARRAY::text
+	 */
+	if (i_mc_elems > 0)
+	{
+		Oid			numberstype = types[i_mc_elem_freqs];
+		Oid			valuestype = types[i_mc_elems];
+		Datum		stanumbers = args[i_mc_elem_freqs];
+
+		if (get_element_type(numberstype) != FLOAT4OID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"most_common_elem_freqs", "real[]")));
+		else if (valuestype != TEXTOID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"most_common_elems", "text")));
+		else
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_MCELEM);
+			Datum		staop = ObjectIdGetDatum(elemtypcache->eq_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		strvalue = args[i_mc_elems];
+			bool		converted = false;
+			Datum		stavalues;
+
+			stavalues = cast_stavalues(&finfo, strvalue, elemtypcache->type_id,
+									   typmod, &converted);
+
+			if (converted &&
+				array_check(stavalues, false, "most_common_elems") &&
+				array_check(stanumbers, true, "most_common_elem_freqs"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_DECHIST
+	 *
+	 * elem_count_histogram:	real[]
+	 */
+	if (i_elem_count_hist > 0)
+	{
+		Oid			numberstype = types[i_elem_count_hist];
+
+		if (get_element_type(numberstype) != FLOAT4OID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"elem_count_histogram", "real[]")));
+		else
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_DECHIST);
+			Datum		staop = ObjectIdGetDatum(elemtypcache->eq_opr);
+			Datum		stacoll = ObjectIdGetDatum(typcoll);
+			Datum		stanumbers = args[i_elem_count_hist];
+
+			if (array_check(stanumbers, true, "elem_count_histogram"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+				nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+				k++;
+			}
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_BOUNDS_HISTOGRAM
+	 *
+	 * range_bounds_histogram: ANYARRAY::text
+	 *
+	 * This stakind appears before STATISTIC_KIND_BOUNDS_HISTOGRAM even though
+	 * it is numerically greater, and all other stakinds appear in numerical
+	 * order. We duplicate this quirk to make before/after tests of
+	 * pg_statistic records easier.
+	 */
+	if (i_range_bounds_hist > 0)
+	{
+		Oid			valuestype = types[i_range_bounds_hist];
+
+		if (valuestype != TEXTOID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"range_bounds_histogram", "text")));
+		else
+		{
+
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_BOUNDS_HISTOGRAM);
+			Datum		staop = ObjectIdGetDatum(InvalidOid);
+			Datum		stacoll = ObjectIdGetDatum(InvalidOid);
+			Datum		strvalue = args[i_range_bounds_hist];
+			bool		converted = false;
+			Datum		stavalues;
+
+			stavalues = cast_stavalues(&finfo, strvalue, typcache->type_id,
+									   typmod, &converted);
+
+			if (converted &&
+				array_check(stavalues, false, "range_bounds_histogram"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM
+	 *
+	 * range_empty_frac: real
+	 *
+	 * range_length_histogram:  double precision[]::text
+	 */
+	if (i_range_length_hist > 0)
+	{
+		Oid			numberstype = types[i_range_empty_frac];
+		Oid			valuestype = types[i_range_length_hist];
+
+		if (numberstype != FLOAT4OID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"range_empty_frac", "real")));
+		else if (valuestype != TEXTOID)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s must be type %s, ignored",
+							"range_length_histogram", "text")));
+		else
+		{
+			Datum		stakind = Int16GetDatum(STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM);
+			Datum		staop = ObjectIdGetDatum(Float8LessOperator);
+			Datum		stacoll = ObjectIdGetDatum(InvalidOid);
+
+			/* The anyarray is always a float8[] for this stakind */
+			Datum		elem = args[i_range_empty_frac];
+			Datum		elems[] = {elem};
+			ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+			Datum		stanumbers = PointerGetDatum(arry);
+			Datum		strvalue = args[i_range_length_hist];
+			bool		converted = false;
+			Datum		stavalues;
+
+			stavalues = cast_stavalues(&finfo, strvalue, FLOAT8OID, 0, &converted);
+
+			if (converted &&
+				array_check(stavalues, false, "range_length_histogram"))
+			{
+				values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+				values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+				values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+				values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+				k++;
+			}
+		}
+	}
+
+	/* fill in all remaining slots */
+	for (; k < STATISTIC_NUM_SLOTS; k++)
+	{
+		values[Anum_pg_statistic_stakind1 - 1 + k] = Int16GetDatum(0);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+	}
+
+	update_pg_statistic(values, nulls);
+
+	relation_close(rel, NoLock);
+	PG_RETURN_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..684df93993
--- /dev/null
+++ b/src/test/regress/expected/stats_export_import.out
@@ -0,0 +1,853 @@
+CREATE SCHEMA stats_export_import;
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+        0 |        -1 |             0
+(1 row)
+
+-- error: regclass not found
+SELECT pg_set_relation_stats('stats_export_import.nope'::regclass,
+                             150000::integer);
+ERROR:  relation "stats_export_import.nope" does not exist
+LINE 1: SELECT pg_set_relation_stats('stats_export_import.nope'::reg...
+                                     ^
+-- error: all three params missing
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer);
+ERROR:  function pg_set_relation_stats(regclass, integer) does not exist
+LINE 1: SELECT pg_set_relation_stats('stats_export_import.test'::reg...
+               ^
+HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+-- error: reltuples, relallvisible missing
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 4::integer);
+WARNING:  required parameter reltuples not set
+ pg_set_relation_stats 
+-----------------------
+ f
+(1 row)
+
+-- error: null value
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', NULL::real,
+                             'relallvisible', 4::integer);
+ pg_set_relation_stats 
+-----------------------
+ 
+(1 row)
+
+-- error: bad relpages type
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+WARNING:  relpages must be of type integer
+ pg_set_relation_stats 
+-----------------------
+ f
+(1 row)
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 17::integer,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+ pg_set_relation_stats 
+-----------------------
+ t
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       17 |       400 |             4
+(1 row)
+
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    '0'::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ERROR:  could not open relation with OID 0
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    NULL::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    NULL::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    NULL::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', NULL::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', NULL::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', NULL::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT stanullfrac, stawidth, stadistinct
+FROM pg_statistic
+WHERE starelid = 'stats_export_import.test'::regclass;
+ stanullfrac | stawidth | stadistinct 
+-------------+----------+-------------
+         0.1 |        2 |         0.3
+(1 row)
+
+-- warn: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_freqs', '{0.1,0.2,0.3}'::real[]
+    );
+WARNING:  most_common_freqs cannot be present if most_common_vals is missing
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{1,2,3}'::text
+    );
+WARNING:  most_common_vals cannot be present if most_common_freqs is missing
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.2,0.1}'::double precision[]
+    );
+WARNING:  most_common_freqs must be type real[], ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warning: mcv cast failure
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,four,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+WARNING:  invalid input syntax for type integer: "four"
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: histogram elements null value
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,NULL,3,4}'::text
+    );
+WARNING:  histogram_bounds array cannot contain NULL values
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,2,3,4}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'correlation', 0.5::real);
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |         0.5 |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{1,3}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+WARNING:  id cannot accept most_common_elems stats, ignored
+WARNING:  id cannot accept most_common_elem_freqs stats, ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,two}'::text
+    );
+WARNING:  most_common_elems cannot be present if most_common_elem_freqs is missing
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
+    );
+WARNING:  most_common_elem_freqs cannot be present if most_common_elems is missing
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             | {one,three}       | {0.3,0.2,0.2,0.3,0}    |                      |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+WARNING:  id cannot accept elem_count_histogram stats, ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+WARNING:  elem_count_histogram array cannot contain NULL values
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+WARNING:  id cannot accept range_length_histogram stats, ignored
+WARNING:  id cannot accept range_empty_frac stats, ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+WARNING:  range_length_histogram cannot be present if range_empty_frac is missing
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real
+    );
+WARNING:  range_empty_frac cannot be present if range_length_histogram is missing
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
+(1 row)
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+WARNING:  id cannot accept range_bounds_histogram stats, ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
+ stats_export_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      |                        |                  | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+(1 row)
+
+-- warn: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
+    'histogram_bounds', '{1,2,3,4}'::text,
+    'correlation', 1.1::real,
+    'most_common_elems', '{3,1}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    'range_empty_frac', -0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+WARNING:  imported statistics must have a maximum of 5 slots but 6 given
+ pg_set_attribute_stats 
+------------------------
+ f
+(1 row)
+
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+   relname    | num_stats 
+--------------+-----------
+ is_odd       |         1
+ is_odd_clone |         1
+ test         |         5
+ test_clone   |         5
+(4 rows)
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 969ced994f..099175d931 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,8 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc stats_export_import
+
 
 # ----------
 # Load huge amounts of data
diff --git a/src/test/regress/sql/stats_export_import.sql b/src/test/regress/sql/stats_export_import.sql
new file mode 100644
index 0000000000..ebdb58aba1
--- /dev/null
+++ b/src/test/regress/sql/stats_export_import.sql
@@ -0,0 +1,671 @@
+CREATE SCHEMA stats_export_import;
+
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+-- error: regclass not found
+SELECT pg_set_relation_stats('stats_export_import.nope'::regclass,
+                             150000::integer);
+
+-- error: all three params missing
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer);
+
+-- error: reltuples, relallvisible missing
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 4::integer);
+
+-- error: null value
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', NULL::real,
+                             'relallvisible', 4::integer);
+
+-- error: bad relpages type
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 17::integer,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_export_import.test'::regclass;
+
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    '0'::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    NULL::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    NULL::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    NULL::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', NULL::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', NULL::integer,
+    'n_distinct', 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', NULL::real);
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+SELECT stanullfrac, stawidth, stadistinct
+FROM pg_statistic
+WHERE starelid = 'stats_export_import.test'::regclass;
+
+-- warn: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_freqs', '{0.1,0.2,0.3}'::real[]
+    );
+
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{1,2,3}'::text
+    );
+
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.2,0.1}'::double precision[]
+    );
+
+-- warning: mcv cast failure
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,four,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: histogram elements null value
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,NULL,3,4}'::text
+    );
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,2,3,4}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'correlation', 0.5::real);
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: scalars can't have mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{1,3}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,two}'::text
+    );
+
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
+    );
+
+-- ok: mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- warn: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real
+    );
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
+    'histogram_bounds', '{1,2,3,4}'::text,
+    'correlation', 1.1::real,
+    'most_common_elems', '{3,1}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    'range_empty_frac', -0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+
+SELECT
+    format('SELECT pg_catalog.pg_set_attribute_stats( '
+            || '%L::regclass::oid, '
+            || '%L::name, '
+            || '%L::boolean, '
+            || '%L::integer, '
+            || '%L, %L::real, '
+            || '%L, %L::integer, '
+            || '%L, %L::real %s)',
+        'stats_export_import.' || s.tablename || '_clone',
+        s.attname,
+        s.inherited,
+        150000,
+        'null_frac', s.null_frac,
+        'avg_width', s.avg_width,
+        'n_distinct', s.n_distinct,
+        CASE
+            WHEN s.most_common_vals IS NULL THEN ''
+            ELSE format(', %L, %L::text, %L, %L::real[]',
+                        'most_common_vals', s.most_common_vals,
+                        'most_common_freqs', s.most_common_freqs)
+        END ||
+        CASE
+            WHEN s.histogram_bounds IS NULL THEN ''
+            ELSE format(', %L, %L::text',
+                        'histogram_bounds', s.histogram_bounds)
+        END ||
+        CASE
+            WHEN s.correlation IS NULL THEN ''
+            ELSE format(', %L, %L::real',
+                        'correlation', s.correlation)
+        END ||
+        CASE
+            WHEN s.most_common_elems IS NULL THEN ''
+            ELSE format(', %L, %L::text, %L, %L::real[]',
+                        'most_common_elems', s.most_common_elems,
+                        'most_common_elem_freqs', s.most_common_elem_freqs)
+        END ||
+        CASE
+            WHEN s.elem_count_histogram IS NULL THEN ''
+            ELSE format(', %L, %L::real[]',
+                        'elem_count_histogram', s.elem_count_histogram)
+        END ||
+        CASE
+            WHEN s.range_bounds_histogram IS NULL THEN ''
+            ELSE format(', %L, %L::text',
+                        'range_bounds_histogram', s.range_bounds_histogram)
+        END ||
+        CASE
+            WHEN s.range_empty_frac IS NULL THEN ''
+            ELSE format(', %L, %L::real, %L, %L::text',
+                        'range_empty_frac', s.range_empty_frac,
+                        'range_length_histogram', s.range_length_histogram)
+        END)
+FROM pg_catalog.pg_stats AS s
+WHERE s.schemaname = 'stats_export_import'
+AND s.tablename IN ('test', 'is_odd')
+\gexec
+
+-- restore ECHO to original value
+\set ECHO :orig_ECHO
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 17c44bc338..b2777b55c8 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29629,6 +29629,273 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
     in identifying the specific disk files associated with database objects.
    </para>
 
+   <table id="functions-admin-statsimport">
+    <title>Database Object Statistics Import Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_relation_stats</primary>
+        </indexterm>
+        <function>pg_set_relation_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>version</parameter> <type>integer</type>,
+         <literal>VARIADIC</literal> <parameter>stats</parameter> <type>"any"</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Updates the <structname>pg_class</structname> row for the specified
+        <parameter>relation</parameter>, values from the variadic list of
+        name-value pairs. Each parameter name corresponds to the same-named
+        attribute in <structname>pg_class</structname>:
+       </para>
+       <variablelist>
+        <varlistentry>
+         <term><literal>relpages</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>integer</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>reltuples</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>relalltuples</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>integer</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+       </variablelist>
+       <para>
+        Any other parameter names given must also have a value pair, but will emit
+        a warning and thereafter be ignored.
+       </para>
+       <para>
+        The <parameter>version</parameter> is meant to reflect the server version
+        number of the system where this data was generated, as that may in the
+        future change how the data is imported.
+       </para>
+       <para>
+        To avoid table bloat in <structname>pg_class</structname>, this change
+        is made with an in-place update, and therefore cannot be rolled back
+        through normal transaction processing.
+       </para>
+       <para>
+        This function mimics the behavior of <command>ANALYZE</command> in its
+        effects on the values in <structname>pg_class</structname>, except that
+        the values are supplied as parameters rather than derived from table
+        sampling.
+       </para>
+       <para>
+        The caller must either be the owner of the relation, or have superuser
+        privileges.
+       </para>
+       <para>
+        The parameters given are checked for type validity and must all be
+        NOT NULL.
+       </para>
+       <para>
+        The parameters will return true if stats were updated and false if not.
+       </para>
+       </entry>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_attribute_stats</primary>
+        </indexterm>
+        <function>pg_set_attribute_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>
+         , <parameter>attname</parameter> <type>name</type>
+         , <parameter>inherited</parameter> <type>boolean</type>
+         , <parameter>version</parameter> <type>integer</type>
+         , <literal>VARIADIC</literal> <parameter>stats</parameter> <type>"any"</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Replaces the <structname>pg_statistic</structname> row for the
+        <structname>pg_attribute</structname> row specified by
+        <parameter>relation</parameter>, <parameter>attname</parameter>
+        and <parameter>inherited</parameter>. The <parameter>version</parameter>
+        should be set to the server version number of the server where
+        the variadic <parameter>stats</parameter> originated, as it may in the
+        future affect how the stats are imported. Values from the variadic
+        list form name-value pairs. Each parameter name corresponds to the
+        same-named attribute in
+        <link linkend="view-pg-stats"><structname>pg_stats</structname></link>:
+       </para>
+       <variablelist>
+        <varlistentry>
+         <term><literal>null_frac</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>avg_width</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>integer</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>n_distinct</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_vals</literal></term>
+         <listitem>
+          <para>
+          <literal>most_common_vals</literal>
+           Must be followed by a parameter of type <type>text</type>. If this
+           parameter is specified then <literal>most_common_freqs</literal>
+           must also be specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_freqs</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of <type>real[]</type>, and can only
+           be specified if <literal>most_common_vals</literal> is also specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>histogram_bounds</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>correlation</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_elems</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>. If this
+           parameter is specified then <literal>most_common_elem_freqs</literal>
+           must also be specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_elem_freqs</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real[]</type>, and can
+           only be specified if <literal>most_common_elems</literal> is also
+           specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>elem_count_histogram</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real[]</type>.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>range_length_histogram</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>. If this
+           parameter is specified then <literal>range_empty_frac</literal>
+           must also be specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>range_empty_frac</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>, and can
+           only be specified if <literal>range_length_histogram</literal>
+           is also specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>range_bounds_histogram</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>
+          </para>
+         </listitem>
+        </varlistentry>
+       </variablelist>
+       <para>
+        Any other parameter names given must also have a value pair, but will emit
+        a warning and thereafter be ignored.
+       </para>
+       <para>
+        The parameters will return true if stats were updated and false if not.
+       </para>
+       <para>
+        This function mimics the behavior of <command>ANALYZE</command> in its
+        effects on the values in <structname>pg_statistic</structname>, except
+        that the values are supplied as parameters rather than derived from
+        table sampling.
+       </para>
+       <para>
+        The purpose of this function is to apply statistics values in an
+        upgrade situation that are "good enough" for system operation until
+        they are replaced by the next <command>ANALYZE</command>, usually via
+        <command>autovacuum</command>.
+       </para>
+       <para>
+        The caller must either be the owner of the relation, or have superuser
+        privileges.
+       </para>
+       </entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
    <table id="functions-admin-dblocation">
     <title>Database Object Location Functions</title>
     <tgroup cols="1">
-- 
2.45.1

v22-0002-Add-derivative-flags-dumpSchema-dumpData.patchtext/x-patch; charset=US-ASCII; name=v22-0002-Add-derivative-flags-dumpSchema-dumpData.patchDownload
From d84543a75f1128d096ed6701b557879ab24ab690 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 4 May 2024 04:52:38 -0400
Subject: [PATCH v22 2/3] Add derivative flags dumpSchema, dumpData.

User-set flags --schema-only and --data-only are often consulted by
various operations to determine if they should be skipped or not. While
this logic works when there are only two mutually-exclusive -only
options, it will get progressively more confusing when more are added.

In anticipation of this, create the flags dumpSchema and dumpData which
are derivative of the existing options schemaOnly and dataOnly. This
allows us to restate current skip-this-section tests in terms of what is
enabled, rather than checking if the other -only mode is turned off.
---
 src/bin/pg_dump/pg_backup.h                  |   8 +
 src/bin/pg_dump/pg_dump.c                    | 186 ++++++++++---------
 src/bin/pg_dump/pg_restore.c                 |   4 +
 src/test/regress/sql/stats_export_import.sql |   4 +-
 4 files changed, 109 insertions(+), 93 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index fbf5f1c515..d942a6a256 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -157,6 +157,10 @@ typedef struct _restoreOptions
 	int			enable_row_security;
 	int			sequence_data;	/* dump sequence data even in schema-only mode */
 	int			binary_upgrade;
+
+	/* flags derived entirely from the user-settable flags */
+	bool		dumpSchema;
+	bool		dumpData;
 } RestoreOptions;
 
 typedef struct _dumpOptions
@@ -203,6 +207,10 @@ typedef struct _dumpOptions
 
 	int			sequence_data;	/* dump sequence data even in schema-only mode */
 	int			do_nothing;
+
+	/* flags derived entirely from the user-settable flags */
+	bool		dumpSchema;
+	bool		dumpData;
 } DumpOptions;
 
 /*
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index e324070828..d3032e4205 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -738,6 +738,10 @@ main(int argc, char **argv)
 	if (dopt.if_exists && !dopt.outputClean)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
+	/* set derivative flags */
+	dopt.dumpSchema = (!dopt.dataOnly);
+	dopt.dumpData = (!dopt.schemaOnly);
+
 	/*
 	 * --inserts are already implied above if --column-inserts or
 	 * --rows-per-insert were specified.
@@ -920,7 +924,7 @@ main(int argc, char **argv)
 	 * -s means "schema only" and LOs are data, not schema, so we never
 	 * include LOs when -s is used.
 	 */
-	if (dopt.include_everything && !dopt.schemaOnly && !dopt.dontOutputLOs)
+	if (dopt.include_everything && dopt.dumpData && !dopt.dontOutputLOs)
 		dopt.outputLOs = true;
 
 	/*
@@ -934,15 +938,15 @@ main(int argc, char **argv)
 	 */
 	tblinfo = getSchemaData(fout, &numTables);
 
-	if (!dopt.schemaOnly)
+	if (dopt.dumpData)
 	{
 		getTableData(&dopt, tblinfo, numTables, 0);
 		buildMatViewRefreshDependencies(fout);
-		if (dopt.dataOnly)
+		if (!dopt.dumpSchema)
 			getTableDataFKConstraints();
 	}
 
-	if (dopt.schemaOnly && dopt.sequence_data)
+	if (!dopt.dumpData && dopt.sequence_data)
 		getTableData(&dopt, tblinfo, numTables, RELKIND_SEQUENCE);
 
 	/*
@@ -4067,8 +4071,8 @@ dumpPolicy(Archive *fout, const PolicyInfo *polinfo)
 	const char *cmd;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
@@ -4286,8 +4290,8 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 	char	   *qpubname;
 	bool		first = true;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	delq = createPQExpBuffer();
@@ -4601,8 +4605,8 @@ dumpPublicationNamespace(Archive *fout, const PublicationSchemaInfo *pubsinfo)
 	PQExpBuffer query;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	tag = psprintf("%s %s", pubinfo->dobj.name, schemainfo->dobj.name);
@@ -4644,8 +4648,8 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 	PQExpBuffer query;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	tag = psprintf("%s %s", pubinfo->dobj.name, tbinfo->dobj.name);
@@ -5008,8 +5012,8 @@ dumpSubscriptionTable(Archive *fout, const SubRelInfo *subrinfo)
 	PQExpBuffer query;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	Assert(fout->dopt->binary_upgrade && fout->remoteVersion >= 170000);
@@ -5082,8 +5086,8 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo)
 	int			i;
 	char		two_phase_disabled[] = {LOGICALREP_TWOPHASE_STATE_DISABLED, '\0'};
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	delq = createPQExpBuffer();
@@ -7269,8 +7273,8 @@ getPartitioningInfo(Archive *fout)
 	/* hash partitioning didn't exist before v11 */
 	if (fout->remoteVersion < 110000)
 		return;
-	/* needn't bother if schema-only dump */
-	if (fout->dopt->schemaOnly)
+	/* needn't bother if not dumping data */
+	if (!fout->dopt->dumpData)
 		return;
 
 	query = createPQExpBuffer();
@@ -8946,7 +8950,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * Now get info about column defaults.  This is skipped for a data-only
 	 * dump, as it is only needed for table schemas.
 	 */
-	if (!dopt->dataOnly && tbloids->len > 1)
+	if (dopt->dumpSchema && tbloids->len > 1)
 	{
 		AttrDefInfo *attrdefs;
 		int			numDefaults;
@@ -9076,7 +9080,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * Get info about table CHECK constraints.  This is skipped for a
 	 * data-only dump, as it is only needed for table schemas.
 	 */
-	if (!dopt->dataOnly && checkoids->len > 2)
+	if (dopt->dumpSchema && checkoids->len > 2)
 	{
 		ConstraintInfo *constrs;
 		int			numConstrs;
@@ -10028,13 +10032,13 @@ dumpCommentExtended(Archive *fout, const char *type,
 	/* Comments are schema not data ... except LO comments are data */
 	if (strcmp(type, "LARGE OBJECT") != 0)
 	{
-		if (dopt->dataOnly)
+		if (!dopt->dumpSchema)
 			return;
 	}
 	else
 	{
 		/* We do dump LO comments in binary-upgrade mode */
-		if (dopt->schemaOnly && !dopt->binary_upgrade)
+		if (!dopt->dumpData && !dopt->binary_upgrade)
 			return;
 	}
 
@@ -10141,7 +10145,7 @@ dumpTableComment(Archive *fout, const TableInfo *tbinfo,
 		return;
 
 	/* Comments are SCHEMA not data */
-	if (dopt->dataOnly)
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Search for comments associated with relation, using table */
@@ -10587,8 +10591,8 @@ dumpNamespace(Archive *fout, const NamespaceInfo *nspinfo)
 	PQExpBuffer delq;
 	char	   *qnspname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -10664,8 +10668,8 @@ dumpExtension(Archive *fout, const ExtensionInfo *extinfo)
 	PQExpBuffer delq;
 	char	   *qextname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -10789,8 +10793,8 @@ dumpType(Archive *fout, const TypeInfo *tyinfo)
 {
 	DumpOptions *dopt = fout->dopt;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Dump out in proper style */
@@ -11900,8 +11904,8 @@ dumpShellType(Archive *fout, const ShellTypeInfo *stinfo)
 	DumpOptions *dopt = fout->dopt;
 	PQExpBuffer q;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -11952,8 +11956,8 @@ dumpProcLang(Archive *fout, const ProcLangInfo *plang)
 	FuncInfo   *inlineInfo = NULL;
 	FuncInfo   *validatorInfo = NULL;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
@@ -12160,8 +12164,8 @@ dumpFunc(Archive *fout, const FuncInfo *finfo)
 	int			nconfigitems = 0;
 	const char *keyword;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -12552,8 +12556,8 @@ dumpCast(Archive *fout, const CastInfo *cast)
 	const char *sourceType;
 	const char *targetType;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Cannot dump if we don't have the cast function's info */
@@ -12658,8 +12662,8 @@ dumpTransform(Archive *fout, const TransformInfo *transform)
 	char	   *lanname;
 	const char *transformType;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Cannot dump if we don't have the transform functions' info */
@@ -12807,8 +12811,8 @@ dumpOpr(Archive *fout, const OprInfo *oprinfo)
 	char	   *oprregproc;
 	char	   *oprref;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
@@ -13094,8 +13098,8 @@ dumpAccessMethod(Archive *fout, const AccessMethodInfo *aminfo)
 	PQExpBuffer delq;
 	char	   *qamname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -13197,8 +13201,8 @@ dumpOpclass(Archive *fout, const OpclassInfo *opcinfo)
 	bool		needComma;
 	int			i;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -13468,8 +13472,8 @@ dumpOpfamily(Archive *fout, const OpfamilyInfo *opfinfo)
 	bool		needComma;
 	int			i;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -13675,8 +13679,8 @@ dumpCollation(Archive *fout, const CollInfo *collinfo)
 	const char *colllocale;
 	const char *collicurules;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -13929,8 +13933,8 @@ dumpConversion(Archive *fout, const ConvInfo *convinfo)
 	const char *conproc;
 	bool		condefault;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -14077,8 +14081,8 @@ dumpAgg(Archive *fout, const AggInfo *agginfo)
 	const char *proparallel;
 	char		defaultfinalmodify;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -14407,8 +14411,8 @@ dumpTSParser(Archive *fout, const TSParserInfo *prsinfo)
 	PQExpBuffer delq;
 	char	   *qprsname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14475,8 +14479,8 @@ dumpTSDictionary(Archive *fout, const TSDictInfo *dictinfo)
 	char	   *nspname;
 	char	   *tmplname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14551,8 +14555,8 @@ dumpTSTemplate(Archive *fout, const TSTemplateInfo *tmplinfo)
 	PQExpBuffer delq;
 	char	   *qtmplname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14617,8 +14621,8 @@ dumpTSConfig(Archive *fout, const TSConfigInfo *cfginfo)
 	int			i_tokenname;
 	int			i_dictname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14729,8 +14733,8 @@ dumpForeignDataWrapper(Archive *fout, const FdwInfo *fdwinfo)
 	PQExpBuffer delq;
 	char	   *qfdwname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14802,8 +14806,8 @@ dumpForeignServer(Archive *fout, const ForeignServerInfo *srvinfo)
 	char	   *qsrvname;
 	char	   *fdwname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14993,8 +14997,8 @@ dumpDefaultACL(Archive *fout, const DefaultACLInfo *daclinfo)
 	PQExpBuffer tag;
 	const char *type;
 
-	/* Do nothing in data-only dump, or if we're skipping ACLs */
-	if (dopt->dataOnly || dopt->aclsSkip)
+	/* Do nothing if not dumping schema, or if we're skipping ACLs */
+	if (!dopt->dumpSchema || dopt->aclsSkip)
 		return;
 
 	q = createPQExpBuffer();
@@ -15094,7 +15098,7 @@ dumpACL(Archive *fout, DumpId objDumpId, DumpId altDumpId,
 		return InvalidDumpId;
 
 	/* --data-only skips ACLs *except* large object ACLs */
-	if (dopt->dataOnly && strcmp(type, "LARGE OBJECT") != 0)
+	if (!dopt->dumpSchema && strcmp(type, "LARGE OBJECT") != 0)
 		return InvalidDumpId;
 
 	sql = createPQExpBuffer();
@@ -15223,13 +15227,13 @@ dumpSecLabel(Archive *fout, const char *type, const char *name,
 	 */
 	if (strcmp(type, "LARGE OBJECT") != 0)
 	{
-		if (dopt->dataOnly)
+		if (!dopt->dumpSchema)
 			return;
 	}
 	else
 	{
 		/* We do dump large object security labels in binary-upgrade mode */
-		if (dopt->schemaOnly && !dopt->binary_upgrade)
+		if (!dopt->dumpData && !dopt->binary_upgrade)
 			return;
 	}
 
@@ -15297,7 +15301,7 @@ dumpTableSecLabel(Archive *fout, const TableInfo *tbinfo, const char *reltypenam
 		return;
 
 	/* SecLabel are SCHEMA not data */
-	if (dopt->dataOnly)
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Search for comments associated with relation, using table */
@@ -15536,8 +15540,8 @@ dumpTable(Archive *fout, const TableInfo *tbinfo)
 	DumpId		tableAclDumpId = InvalidDumpId;
 	char	   *namecopy;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	if (tbinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
@@ -16566,8 +16570,8 @@ dumpTableAttach(Archive *fout, const TableAttachInfo *attachinfo)
 	PGresult   *res;
 	char	   *partbound;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -16638,8 +16642,8 @@ dumpAttrDef(Archive *fout, const AttrDefInfo *adinfo)
 	char	   *tag;
 	char	   *foreign;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Skip if not "separate"; it was dumped in the table's definition */
@@ -16727,8 +16731,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 	char	   *qindxname;
 	char	   *qqindxname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -16860,8 +16864,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 static void
 dumpIndexAttach(Archive *fout, const IndexAttachInfo *attachinfo)
 {
-	/* Do nothing in data-only dump */
-	if (fout->dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!fout->dopt->dumpSchema)
 		return;
 
 	if (attachinfo->partitionIdx->dobj.dump & DUMP_COMPONENT_DEFINITION)
@@ -16907,8 +16911,8 @@ dumpStatisticsExt(Archive *fout, const StatsExtInfo *statsextinfo)
 	PGresult   *res;
 	char	   *stxdef;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -16983,8 +16987,8 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
 	char	   *tag = NULL;
 	char	   *foreign;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -17615,8 +17619,8 @@ dumpTrigger(Archive *fout, const TriggerInfo *tginfo)
 	char	   *qtabname;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -17737,8 +17741,8 @@ dumpEventTrigger(Archive *fout, const EventTriggerInfo *evtinfo)
 	PQExpBuffer delqry;
 	char	   *qevtname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -17828,8 +17832,8 @@ dumpRule(Archive *fout, const RuleInfo *rinfo)
 	PGresult   *res;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 5ea78cf7cc..62821cbee4 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -360,6 +360,10 @@ main(int argc, char **argv)
 	if (opts->single_txn && numWorkers > 1)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
+	/* set derivative flags */
+	opts->dumpSchema = (opts->dataOnly != 1);
+	opts->dumpData = (opts->schemaOnly != 1);
+
 	opts->disable_triggers = disable_triggers;
 	opts->enable_row_security = enable_row_security;
 	opts->noDataForFailedTables = no_data_for_failed_tables;
diff --git a/src/test/regress/sql/stats_export_import.sql b/src/test/regress/sql/stats_export_import.sql
index ebdb58aba1..39d96c34ef 100644
--- a/src/test/regress/sql/stats_export_import.sql
+++ b/src/test/regress/sql/stats_export_import.sql
@@ -257,7 +257,7 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: scalars can't have mcelem 
+-- warn: scalars can't have mcelem
 SELECT pg_catalog.pg_set_attribute_stats(
     'stats_export_import.test'::regclass,
     'id'::name,
@@ -294,7 +294,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
     );
 
--- ok: mcelem 
+-- ok: mcelem
 SELECT pg_catalog.pg_set_attribute_stats(
     'stats_export_import.test'::regclass,
     'tags'::name,
-- 
2.45.1

v22-0003-Enable-dumping-of-table-index-stats-in-pg_dump.patchtext/x-patch; charset=US-ASCII; name=v22-0003-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From 766c7b57b00dde816e125ab7b8e5926a470131d4 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH v22 3/3] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will generate a statement that
calls pg_set_relation_stats(), and it will generate a series of
statements that call pg_set_attribute_stats(), one per attribute. These
statements will restore the statistics of the current system onto the
destination system.

Adds the command-line options -X / --statistics-only, which are mutually
exclusive to --schema-only and --data-only.

Statistics are not dumped when --schema-only is specified, except during
a binary upgrade.

As is the pattern with pg_dump options, staistics can be disabled using
--no-statistics.

Table statistics are dumped in the data section. This is true even if
dumping stats in a binary upgrade. Index and Materialized View
statistics are dumped in the post-data section.
---
 src/bin/pg_dump/pg_backup.h          |   6 +
 src/bin/pg_dump/pg_backup_archiver.c |   5 +
 src/bin/pg_dump/pg_dump.c            | 397 ++++++++++++++++++++++++++-
 src/bin/pg_dump/pg_dump.h            |   8 +
 src/bin/pg_dump/pg_dump_sort.c       |   5 +
 src/bin/pg_dump/pg_dumpall.c         |   5 +
 src/bin/pg_dump/pg_restore.c         |   8 +
 src/bin/pg_dump/t/001_basic.pl       |  10 +-
 doc/src/sgml/ref/pg_dump.sgml        |  36 ++-
 doc/src/sgml/ref/pg_restore.sgml     |  31 ++-
 10 files changed, 493 insertions(+), 18 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index d942a6a256..25d386f5bb 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -112,11 +112,13 @@ typedef struct _restoreOptions
 	int			no_publications;	/* Skip publication entries */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
 	int			dataOnly;
 	int			schemaOnly;
+	int			statisticsOnly;
 	int			dumpSections;
 	int			verbose;
 	int			aclsSkip;
@@ -161,6 +163,7 @@ typedef struct _restoreOptions
 	/* flags derived entirely from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } RestoreOptions;
 
 typedef struct _dumpOptions
@@ -172,6 +175,7 @@ typedef struct _dumpOptions
 	/* various user-settable parameters */
 	bool		schemaOnly;
 	bool		dataOnly;
+	bool		statisticsOnly;
 	int			dumpSections;	/* bitmask of chosen sections */
 	bool		aclsSkip;
 	const char *lockWaitTimeout;
@@ -185,6 +189,7 @@ typedef struct _dumpOptions
 	int			no_security_labels;
 	int			no_publications;
 	int			no_subscriptions;
+	int			no_statistics;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
@@ -211,6 +216,7 @@ typedef struct _dumpOptions
 	/* flags derived entirely from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } DumpOptions;
 
 /*
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 68e321212d..9bfa2f1326 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2958,6 +2958,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's a stats dump, maybe ignore it */
+	if (ropt->no_statistics && strcmp(te->desc, "STATISTICS") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2987,6 +2991,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index d3032e4205..a89c4ac150 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -407,6 +407,7 @@ main(int argc, char **argv)
 		{"encoding", required_argument, NULL, 'E'},
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
+		{"statistics-only", no_argument, NULL, 'X'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -435,6 +436,7 @@ main(int argc, char **argv)
 		{"no-comments", no_argument, &dopt.no_comments, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -480,7 +482,7 @@ main(int argc, char **argv)
 
 	InitDumpOptions(&dopt);
 
-	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:RsS:t:T:U:vwWxZ:",
+	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:PRsS:t:T:U:vwWxXZ:",
 							long_options, &optindex)) != -1)
 	{
 		switch (c)
@@ -554,6 +556,10 @@ main(int argc, char **argv)
 				dopt.cparams.pgport = pg_strdup(optarg);
 				break;
 
+			case 'X':			/* Dump statistics only */
+				dopt.statisticsOnly = true;
+				break;
+
 			case 'R':
 				/* no-op, still accepted for backwards compatibility */
 				break;
@@ -723,8 +729,11 @@ main(int argc, char **argv)
 	if (dopt.binary_upgrade)
 		dopt.sequence_data = 1;
 
-	if (dopt.dataOnly && dopt.schemaOnly)
-		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (((int)dopt.dataOnly + (int) dopt.schemaOnly + (int) dopt.statisticsOnly) > 1)
+		pg_fatal("options -s/--schema-only, -a/--data-only, and -X/--statistics-only cannot be used together");
+
+	if (dopt.statisticsOnly && dopt.no_statistics)
+		pg_fatal("options -X/--statistics-only and --no-statistics cannot be used together");
 
 	if (dopt.schemaOnly && foreign_servers_include_patterns.head != NULL)
 		pg_fatal("options -s/--schema-only and --include-foreign-data cannot be used together");
@@ -739,8 +748,23 @@ main(int argc, char **argv)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
 	/* set derivative flags */
-	dopt.dumpSchema = (!dopt.dataOnly);
-	dopt.dumpData = (!dopt.schemaOnly);
+	dopt.dumpSchema = dopt.schemaOnly ||
+		!(dopt.dataOnly || dopt.statisticsOnly);
+
+	dopt.dumpData = dopt.dataOnly ||
+		!(dopt.schemaOnly || dopt.statisticsOnly);
+
+	if (dopt.statisticsOnly)
+		/* stats are only thing wanted */
+		dopt.dumpStatistics = true;
+	else if (dopt.no_statistics)
+		/* stats specifically excluded */
+		dopt.dumpStatistics = false;
+	else if (dopt.binary_upgrade)
+		/* binary upgrade and not specifically excluded */
+		dopt.dumpStatistics = true;
+	else
+		dopt.dumpStatistics = !(dopt.schemaOnly || dopt.dataOnly);
 
 	/*
 	 * --inserts are already implied above if --column-inserts or
@@ -1033,6 +1057,7 @@ main(int argc, char **argv)
 	ropt->dropSchema = dopt.outputClean;
 	ropt->dataOnly = dopt.dataOnly;
 	ropt->schemaOnly = dopt.schemaOnly;
+	ropt->statisticsOnly = dopt.statisticsOnly;
 	ropt->if_exists = dopt.if_exists;
 	ropt->column_inserts = dopt.column_inserts;
 	ropt->dumpSections = dopt.dumpSections;
@@ -1111,7 +1136,7 @@ help(const char *progname)
 	printf(_("  -?, --help                   show this help, then exit\n"));
 
 	printf(_("\nOptions controlling the output content:\n"));
-	printf(_("  -a, --data-only              dump only the data, not the schema\n"));
+	printf(_("  -a, --data-only              dump only the data, not the schema or statistics\n"));
 	printf(_("  -b, --large-objects          include large objects in dump\n"));
 	printf(_("  --blobs                      (same as --large-objects, deprecated)\n"));
 	printf(_("  -B, --no-large-objects       exclude large objects in dump\n"));
@@ -1124,11 +1149,12 @@ help(const char *progname)
 	printf(_("  -N, --exclude-schema=PATTERN do NOT dump the specified schema(s)\n"));
 	printf(_("  -O, --no-owner               skip restoration of object ownership in\n"
 			 "                               plain-text format\n"));
-	printf(_("  -s, --schema-only            dump only the schema, no data\n"));
+	printf(_("  -s, --schema-only            dump only the schema, no data or statistics\n"));
 	printf(_("  -S, --superuser=NAME         superuser user name to use in plain-text format\n"));
 	printf(_("  -t, --table=PATTERN          dump only the specified table(s)\n"));
 	printf(_("  -T, --exclude-table=PATTERN  do NOT dump the specified table(s)\n"));
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
+	printf(_("  -X, --statistics-only        dump only the statistics, not schema or data\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
@@ -1155,6 +1181,7 @@ help(const char *progname)
 	printf(_("  --no-comments                do not dump comments\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -6691,6 +6718,42 @@ getFuncs(Archive *fout, int *numFuncs)
 	return finfo;
 }
 
+/*
+ * getRelationStatistics
+ *    register the statistics object as a dependent of the relation.
+ *
+ */
+static RelStatsInfo *
+getRelationStatistics(Archive *fout, DumpableObject *rel, char relkind)
+{
+	if ((relkind == RELKIND_RELATION) ||
+		(relkind == RELKIND_PARTITIONED_TABLE) ||
+		(relkind == RELKIND_INDEX) ||
+		(relkind == RELKIND_PARTITIONED_INDEX) ||
+		(relkind == RELKIND_MATVIEW))
+	{
+		RelStatsInfo	   *info = pg_malloc0(sizeof(RelStatsInfo));
+		DumpableObject	   *dobj = &info->dobj;
+
+		dobj->objType = DO_REL_STATS;
+		dobj->catId.tableoid = 0;
+		dobj->catId.oid = 0;
+		AssignDumpId(dobj);
+		dobj->dependencies = (DumpId *) pg_malloc(sizeof(DumpId));
+		dobj->dependencies[0] = rel->dumpId;
+		dobj->nDeps = 1;
+		dobj->allocDeps = 1;
+		dobj->components |= DUMP_COMPONENT_STATISTICS;
+		dobj->dump = rel->dump;
+		dobj->name = pg_strdup(rel->name);
+		dobj->namespace = rel->namespace;
+		info->relkind = relkind;
+
+		return info;
+	}
+	return NULL;
+}
+
 /*
  * getTables
  *	  read all the tables (no indexes) in the system catalogs,
@@ -7068,6 +7131,7 @@ getTables(Archive *fout, int *numTables)
 
 		/* Tables have data */
 		tblinfo[i].dobj.components |= DUMP_COMPONENT_DATA;
+		tblinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
 
 		/* Mark whether table has an ACL */
 		if (!PQgetisnull(res, i, i_relacl))
@@ -7116,6 +7180,8 @@ getTables(Archive *fout, int *numTables)
 				}
 			}
 		}
+		if (tblinfo[i].interesting)
+			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relkind);
 	}
 
 	if (query->len != 0)
@@ -7551,11 +7617,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 		for (int c = 0; c < numinds; c++, j++)
 		{
 			char		contype;
+			char		indexkind;
+			RelStatsInfo   *relstats;
 
 			indxinfo[j].dobj.objType = DO_INDEX;
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -7578,7 +7647,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			{
 				NULL, NULL
 			};
+
+			if (indxinfo[j].parentidx == 0)
+				indexkind = RELKIND_INDEX;
+			else
+				indexkind = RELKIND_PARTITIONED_INDEX;
+
 			contype = *(PQgetvalue(res, j, i_contype));
+			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, indexkind);
 
 			if (contype == 'p' || contype == 'u' || contype == 'x')
 			{
@@ -7611,6 +7687,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				constrinfo->separate = true;
 
 				indxinfo[j].indexconstraint = constrinfo->dobj.dumpId;
+				if (relstats != NULL)
+					addObjectDependency(&relstats->dobj, constrinfo->dobj.dumpId);
 			}
 			else
 			{
@@ -10124,6 +10202,296 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * Tabular description of the parameters to pg_set_relation_stats()
+ * required, param_name, param_type
+ */
+static const char *rel_stats_arginfo[][3] = {
+	{"t", "relation", "regclass"},
+	{"t", "version", "integer"},
+	{"f", "relpages", "integer"},
+	{"f", "reltuples", "real"},
+	{"f", "relallvisible", "integer"},
+};
+
+/*
+ * Tabular description of the parameters to pg_set_attribute_stats()
+ * required, param_name, param_type
+ */
+static const char *att_stats_arginfo[][3] = {
+	{"t", "relation", "regclass"},
+	{"t", "attname", "name"},
+	{"t", "inherited", "boolean"},
+	{"t", "version", "integer"},
+	{"f", "null_frac", "float4"},
+	{"f", "avg_width", "integer"},
+	{"f", "n_distinct", "float4"},
+	{"f", "most_common_vals", "text"},
+	{"f", "most_common_freqs", "float4[]"},
+	{"f", "histogram_bounds", "text"},
+	{"f", "correlation", "float4"},
+	{"f", "most_common_elems", "text"},
+	{"f", "most_common_elem_freqs", "float4[]"},
+	{"f", "elem_count_histogram", "float4[]"},
+	{"f", "range_length_histogram", "text"},
+	{"f", "range_empty_frac", "float4"},
+	{"f", "range_bounds_histogram", "text"},
+};
+
+/*
+ * getRelStatsExportQuery --
+ *
+ * Generate a query that will fetch all relation (e.g. pg_class)
+ * stats for a given relation.
+ */
+static void
+getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "current_setting('server_version_num') AS version, "
+						 "c.relpages, c.reltuples, c.relallvisible "
+						 "FROM pg_class c "
+						 "JOIN pg_namespace n "
+						 "ON n.oid = c.relnamespace "
+						 "WHERE n.nspname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND c.relname = ");
+	appendStringLiteralAH(query, relname, fout);
+}
+
+/*
+ * getAttStatsExportQuery --
+ *
+ * Generate a query that will fetch all attribute (e.g. pg_statistic)
+ * stats for a given relation.
+ */
+static void
+getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "s.attname,"
+						 "s.inherited,"
+						 "current_setting('server_version_num') AS version, "
+						 "s.null_frac,"
+						 "s.avg_width,"
+						 "s.n_distinct,"
+						 "s.most_common_vals,"
+						 "s.most_common_freqs,"
+						 "s.histogram_bounds,"
+						 "s.correlation,"
+						 "s.most_common_elems,"
+						 "s.most_common_elem_freqs,"
+						 "s.elem_count_histogram,");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 "s.range_length_histogram,"
+							 "s.range_empty_frac,"
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(query,
+							 "NULL AS range_length_histogram,"
+							 "NULL AS range_empty_frac,"
+							 "NULL AS range_bounds_histogram ");
+
+	appendPQExpBufferStr(query,
+						 "FROM pg_stats s "
+						 "JOIN pg_namespace n "
+						 "ON n.nspname = s.schemaname "
+						 "JOIN pg_class c "
+						 "ON c.relname = s.tablename "
+						 "AND c.relnamespace = n.oid "
+						 "WHERE s.schemaname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND s.tablename = ");
+	appendStringLiteralAH(query, relname, fout);
+	appendPQExpBufferStr(query, " ORDER BY s.attname, s.inherited");
+}
+
+
+/*
+ * appendNamedArgument --
+ *
+ * Convenience routine for constructing parameters of the form:
+ * 'paraname', 'value'::type
+ */
+static void
+appendNamedArgument(PQExpBuffer out, Archive *fout,
+					const char *argname, bool positional,
+					const char *argval, const char *argtype)
+{
+	appendPQExpBufferStr(out, "\t");
+
+	if (!positional)
+	{
+		appendStringLiteralAH(out, argname, fout);
+		appendPQExpBufferStr(out, ", ");
+	}
+
+	appendStringLiteralAH(out, argval, fout);
+	appendPQExpBuffer(out, "::%s", argtype);
+}
+
+/*
+ * appendRelStatsImport --
+ *
+ * Append a formatted pg_set_relation_stats statement.
+ */
+static void
+appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	const char *sep = "";
+
+	if (PQntuples(res) == 0)
+		return;
+
+	appendPQExpBufferStr(out, "SELECT pg_catalog.pg_set_relation_stats(\n");
+
+	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
+	{
+		bool		positional = (rel_stats_arginfo[argno][0][0] == 't');
+		const char *argname = rel_stats_arginfo[argno][1];
+		const char *argtype = rel_stats_arginfo[argno][2];
+		int			fieldno = PQfnumber(res, argname);
+
+		if (fieldno < 0)
+			pg_fatal("relation stats export query missing field '%s'",
+					 argname);
+
+		if (PQgetisnull(res, 0, fieldno))
+		{
+			if (!positional)
+				pg_fatal("relation stats export query unexpected NULL in '%s'",
+						 argname);
+			else
+				continue;
+		}
+
+		appendPQExpBufferStr(out, sep);
+		appendNamedArgument(out, fout, argname, positional,
+							PQgetvalue(res, 0, fieldno), argtype);
+
+		sep = ",\n";
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+/*
+ * appendAttStatsImport --
+ *
+ * Append a series of formatted pg_set_attribute_stats statements.
+ */
+static void
+appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	{
+		const char *sep = "";
+
+		appendPQExpBufferStr(out, "SELECT pg_catalog.pg_set_attribute_stats(\n");
+		for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
+		{
+			bool		positional = (att_stats_arginfo[argno][0][0] == 't');
+			const char *argname = att_stats_arginfo[argno][1];
+			const char *argtype = att_stats_arginfo[argno][2];
+			int			fieldno = PQfnumber(res, argname);
+
+			if (fieldno < 0)
+				pg_fatal("attribute stats export query missing field '%s'",
+						 argname);
+
+			if (PQgetisnull(res, rownum, fieldno))
+			{
+				if (positional)
+					pg_fatal("attribute stats export query unexpected NULL in '%s'",
+							 argname);
+				else
+					continue;
+			}
+
+			appendPQExpBufferStr(out, sep);
+			appendNamedArgument(out, fout, argname, positional,
+								PQgetvalue(res, rownum, fieldno), argtype);
+			sep = ",\n";
+		}
+		appendPQExpBufferStr(out, "\n);\n");
+	}
+}
+
+/*
+ * get statistics dump section, which depends on the parent object type
+ *
+ * objects created in SECTION_PRE_DATA have stats in SECTION_DATA
+ * objects created in SECTION_POST_DATA have stats in SECTION_POST_DATA
+ */
+static teSection
+statisticsDumpSection(const RelStatsInfo *rsinfo)
+{
+
+	if ((rsinfo->relkind == RELKIND_MATVIEW) ||
+		(rsinfo->relkind == RELKIND_INDEX) ||
+		(rsinfo->relkind == RELKIND_PARTITIONED_INDEX))
+			return SECTION_POST_DATA;
+
+	return SECTION_DATA;
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	PGresult	   *res;
+	PQExpBuffer		query;
+	PQExpBuffer		out;
+	PQExpBuffer		tag;
+	DumpableObject *dobj = (DumpableObject *) &rsinfo->dobj;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", "STATISTICS DATA", fmtId(dobj->name));
+
+	query = createPQExpBuffer();
+	out = createPQExpBuffer();
+
+	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendRelStatsImport(out, fout, res);
+	PQclear(res);
+
+	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendAttStatsImport(out, fout, res);
+	PQclear(res);
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATISTICS DATA",
+							  .section = statisticsDumpSection(rsinfo),
+							  .createStmt = out->data,
+							  .deps = dobj->dependencies,
+							  .nDeps = dobj->nDeps));
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(out);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -10572,6 +10940,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_SUBSCRIPTION_REL:
 			dumpSubscriptionTable(fout, (const SubRelInfo *) dobj);
 			break;
+		case DO_REL_STATS:
+			dumpRelationStats(fout, (const RelStatsInfo *) dobj);
+			break;
 		case DO_PRE_DATA_BOUNDARY:
 		case DO_POST_DATA_BOUNDARY:
 			/* never dumped, nothing to do */
@@ -16842,6 +17213,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 		free(indstatvalsarray);
 	}
 
+	/* Comments and stats share same .dep */
+
 	/* Dump Index Comments */
 	if (indxinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
 		dumpComment(fout, "INDEX", qindxname,
@@ -18524,6 +18897,16 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 				/* must come after the pre-data boundary */
 				addObjectDependency(dobj, preDataBound->dumpId);
 				break;
+			case DO_REL_STATS:
+				/* stats section varies by parent object type, DATA or POST */
+				if (statisticsDumpSection((RelStatsInfo *) dobj) == SECTION_DATA)
+				{
+					addObjectDependency(dobj, preDataBound->dumpId);
+					addObjectDependency(postDataBound, dobj->dumpId);
+				}
+				else
+					addObjectDependency(dobj, postDataBound->dumpId);
+				break;
 		}
 	}
 }
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 865823868f..d958089ef5 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -84,6 +84,7 @@ typedef enum
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION,
 	DO_SUBSCRIPTION_REL,		/* see note for SubRelInfo */
+	DO_REL_STATS,
 } DumpableObjectType;
 
 /*
@@ -101,6 +102,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
@@ -416,6 +418,12 @@ typedef struct _indexAttachInfo
 	IndxInfo   *partitionIdx;	/* link to index on partition */
 } IndexAttachInfo;
 
+typedef struct _relStatsInfo
+{
+	DumpableObject	dobj;
+	char			relkind;		/* 'r', 'v', 'c', etc */
+} RelStatsInfo;
+
 typedef struct _statsExtInfo
 {
 	DumpableObject dobj;
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 4cb754caa5..cfe277f5ce 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -1490,6 +1490,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "POST-DATA BOUNDARY  (ID %d)",
 					 obj->dumpId);
 			return;
+		case DO_REL_STATS:
+			snprintf(buf, bufsize,
+					 "RELATION STATISTICS FOR %s  (ID %d OID %u)",
+					 obj->name, obj->dumpId, obj->catId.oid);
+			return;
 	}
 	/* shouldn't get here */
 	snprintf(buf, bufsize,
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 73337f3392..c4eaff3063 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -103,6 +103,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -172,6 +173,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -451,6 +453,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -666,6 +670,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 62821cbee4..984c113560 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -75,6 +75,7 @@ main(int argc, char **argv)
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
 	static int	no_subscriptions = 0;
+	static int  no_statistics = 0;
 	static int	strict_names = 0;
 
 	struct option cmdopts[] = {
@@ -107,6 +108,7 @@ main(int argc, char **argv)
 		{"username", 1, NULL, 'U'},
 		{"verbose", 0, NULL, 'v'},
 		{"single-transaction", 0, NULL, '1'},
+		{"statistics-only", no_argument, NULL, 'P'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -127,6 +129,7 @@ main(int argc, char **argv)
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
+		{"no-statistics", no_argument, &no_statistics, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -270,6 +273,10 @@ main(int argc, char **argv)
 				opts->aclsSkip = 1;
 				break;
 
+			case 'X':			/* Restore statistics only */
+				opts->statisticsOnly = 1;
+				break;
+
 			case '1':			/* Restore data in a single transaction */
 				opts->single_txn = true;
 				opts->exit_on_error = true;
@@ -374,6 +381,7 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
diff --git a/src/bin/pg_dump/t/001_basic.pl b/src/bin/pg_dump/t/001_basic.pl
index b9d13a0e1d..b1c7a10919 100644
--- a/src/bin/pg_dump/t/001_basic.pl
+++ b/src/bin/pg_dump/t/001_basic.pl
@@ -46,8 +46,8 @@ command_fails_like(
 
 command_fails_like(
 	[ 'pg_dump', '-s', '-a' ],
-	qr/\Qpg_dump: error: options -s\/--schema-only and -a\/--data-only cannot be used together\E/,
-	'pg_dump: options -s/--schema-only and -a/--data-only cannot be used together'
+	qr/\Qpg_dump: error: options -s\/--schema-only, -a\/--data-only, and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: options -s/--schema-only, -a/--data-only, and -X/--statistics-only cannot be used together'
 );
 
 command_fails_like(
@@ -56,6 +56,12 @@ command_fails_like(
 	'pg_dump: options -s/--schema-only and --include-foreign-data cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '--statistics-only', '--no-statistics' ],
+	qr/\Qpg_dump: error: options -X\/--statistics-only and --no-statistics cannot be used together\E/,
+	'pg_dump: options -X\/--statistics-only and --no-statistics cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-j2', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: option --include-foreign-data is not supported with parallel backup\E/,
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index 08d775379f..037273701b 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -119,7 +119,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Dump only the data, not the schema (data definitions).
+        Dump only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are dumped.
        </para>
 
@@ -137,8 +137,9 @@ PostgreSQL documentation
       <listitem>
        <para>
         Include large objects in the dump.  This is the default behavior
-        except when <option>--schema</option>, <option>--table</option>, or
-        <option>--schema-only</option> is specified.  The <option>-b</option>
+        except when <option>--schema</option>, <option>--table</option>,
+        <option>--schema-only</option>, or
+        <option>--statistics-only</option> is specified.  The <option>-b</option>
         switch is therefore only useful to add large objects to dumps
         where a specific schema or table has been requested.  Note that
         large objects are considered data and therefore will be included when
@@ -512,10 +513,11 @@ PostgreSQL documentation
       <term><option>--schema-only</option></term>
       <listitem>
        <para>
-        Dump only the object definitions (schema), not data.
+        Dump only the object definitions (schema), not data or statistics.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive to <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
         <option>--section=pre-data --section=post-data</option>.
@@ -648,6 +650,18 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Dump only the statistics, not the schema (data definitions) or data.
+        Statistics for tables, materialized views, and indexes are dumped.
+       </para>
+
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-Z <replaceable class="parameter">level</replaceable></option></term>
       <term><option>-Z <replaceable class="parameter">method</replaceable></option>[:<replaceable>detail</replaceable>]</term>
@@ -829,7 +843,8 @@ PostgreSQL documentation
         though you do not need the data in it.
        </para>
        <para>
-        To exclude data for all tables in the database, see <option>--schema-only</option>.
+        To exclude data for all tables in the database, see <option>--schema-only</option>
+        or <option>--statistics-only</option>.
        </para>
       </listitem>
      </varlistentry>
@@ -1081,6 +1096,15 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index 2e3ba80258..8b1658e648 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -94,7 +94,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Restore only the data, not the schema (data definitions).
+        Restore only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are restored,
         if present in the archive.
        </para>
@@ -483,10 +483,11 @@ PostgreSQL documentation
         to the extent that schema entries are present in the archive.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive of <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
-        <option>--section=pre-data --section=post-data</option>.
+        <option>--section=pre-data --section=post-data --no-statistics</option>.
        </para>
        <para>
         (Do not confuse this with the <option>--schema</option> option, which
@@ -599,6 +600,20 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Restore only the statistics, not schema or data.
+       </para>
+       <para>
+        (Do not confuse this with the <option>--schema</option> option, which
+        uses the word <quote>schema</quote> in a different meaning.)
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-1</option></term>
       <term><option>--single-transaction</option></term>
@@ -723,6 +738,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore statistics, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
-- 
2.45.1

#170Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#169)
3 attachment(s)
Re: Statistics Import and Export

v23:

Split pg_set_relation_stats into two functions: pg_set_relation_stats with
named parameters like it had around v19 and pg_restore_relations_stats with
the variadic parameters it has had in more recent versions, which processes
the variadic parameters and then makes a call to pg_set_relation_stats.

Split pg_set_attribute_stats into two functions: pg_set_attribute_stats
with named parameters like it had around v19 and pg_restore_attribute_stats
with the variadic parameters it has had in more recent versions, which
processes the variadic parameters and then makes a call to
pg_set_attribute_stats.

The intention here is that the named parameters signatures are easier for
ad-hoc use, while the variadic signatures are evergreen and thus ideal for
pg_dump/pg_upgrade.

rebased to a0a5869a8598cdeae1d2f2d632038d26dcc69d19 (master as of early
July 18)

Attachments:

v23-0001-Create-statistics-set-and-restore-functions.patchtext/x-patch; charset=US-ASCII; name=v23-0001-Create-statistics-set-and-restore-functions.patchDownload
From 7e1443d12757851cd58cb5f116c30ddfda0b8ffa Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 17 Jul 2024 23:23:23 -0400
Subject: [PATCH v23 1/3] Create statistics set and restore functions.

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

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

Both functions take an oid to identify the target relation that will
receive the statistics. There is nothing requiring that relation to be
the same one as the one exported, though the statistics supplied have
to make sense in the context of the new relation. For example, it is not
possible to assign most_common_elems statistics to a scalar attribute.

These functions allows for tweaking of table statistics in-place,
allowing the user to inflate rowcounts, skew histograms, etc, to see
what those changes will evoke from the query planner.

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

The functions pg_restore_relation_stats and pg_restore_attribute_stats
are variadic variants of the pg_set_relation_stats and
pg_set_attribute_stats, respectively. The variadic parametrs are in
name-value pairs, with each name matching a parameter name in the
corresponding -set function. The paired value parameter must be of the
appropriate type for the corresponding parameter in the -set function,
with the exception that ANYARRAY values are passed as TEXT. The
intention of these functions is to be used in pg_dump/pg_restore and
pg_upgrade to allow the user to avoid having to run vacuumdb
--analyze-in-stages after an upgrade or restore.
---
 src/include/catalog/pg_proc.dat               |   33 +
 src/include/statistics/statistics.h           |    4 +
 src/backend/statistics/Makefile               |    3 +-
 src/backend/statistics/meson.build            |    1 +
 src/backend/statistics/statistics.c           | 1673 +++++++++++++++++
 .../regress/expected/stats_export_import.out  |  870 +++++++++
 src/test/regress/parallel_schedule            |    2 +-
 src/test/regress/sql/stats_export_import.sql  |  677 +++++++
 doc/src/sgml/func.sgml                        |  399 ++++
 9 files changed, 3660 insertions(+), 2 deletions(-)
 create mode 100644 src/backend/statistics/statistics.c
 create mode 100644 src/test/regress/expected/stats_export_import.out
 create mode 100644 src/test/regress/sql/stats_export_import.sql

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 73d9cf8582..42ee44bce6 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12208,4 +12208,37 @@
   proargnames => '{summarized_tli,summarized_lsn,pending_lsn,summarizer_pid}',
   prosrc => 'pg_get_wal_summarizer_state' },
 
+# Statistics Import
+{ oid => '8048',
+  descr => 'set statistics on relation',
+  proname => 'pg_set_relation_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'oid int4 int4 float4 int4',
+  proargnames => '{relation,version,relpages,reltuples,relallvisible}',
+  prosrc => 'pg_set_relation_stats' },
+{ oid => '8049',
+  descr => 'set statistics on attribute',
+  proname => 'pg_set_attribute_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'oid name bool int4 float4 int4 float4 text _float4 text float4 text _float4 _float4 text float4 text',
+  proargnames => '{relation,attname,inherited,version,null_frac,avg_width,n_distinct,most_common_vals,most_common_freqs,histogram_bounds,correlation,most_common_elems,most_common_elem_freqs,elem_count_histogram,range_length_histogram,range_empty_frac,range_bounds_histogram}',
+  prosrc => 'pg_set_attribute_stats' },
+{ oid => '8050',
+  descr => 'set statistics on relation',
+  proname => 'pg_restore_relation_stats', provariadic => 'any',
+  proisstrict => 'f', provolatile => 'v',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'regclass int4 any',
+  proargnames => '{relation,version,stats}',
+  proargmodes => '{i,i,v}',
+  prosrc => 'pg_restore_relation_stats' },
+{ oid => '8051',
+  descr => 'set statistics on attribute',
+  proname => 'pg_restore_attribute_stats', provariadic => 'any',
+  proisstrict => 'f', provolatile => 'v',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'regclass name bool int4 any',
+  proargnames => '{relation,attname,inherited,version,stats}',
+  proargmodes => '{i,i,i,i,v}',
+  prosrc => 'pg_restore_attribute_stats' },
 ]
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 7f2bf18716..f0503d1c87 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -127,4 +127,8 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												int nclauses);
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
+extern Datum pg_set_relation_stats(PG_FUNCTION_ARGS);
+extern Datum pg_set_attribute_stats(PG_FUNCTION_ARGS);
+extern Datum pg_restore_relation_stats(PG_FUNCTION_ARGS);
+extern Datum pg_restore_attribute_stats(PG_FUNCTION_ARGS);
 #endif							/* STATISTICS_H */
diff --git a/src/backend/statistics/Makefile b/src/backend/statistics/Makefile
index 89cf8c2797..e4f8ab7c4f 100644
--- a/src/backend/statistics/Makefile
+++ b/src/backend/statistics/Makefile
@@ -16,6 +16,7 @@ OBJS = \
 	dependencies.o \
 	extended_stats.o \
 	mcv.o \
-	mvdistinct.o
+	mvdistinct.o \
+	statistics.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/statistics/meson.build b/src/backend/statistics/meson.build
index 73b29a3d50..331e82c776 100644
--- a/src/backend/statistics/meson.build
+++ b/src/backend/statistics/meson.build
@@ -5,4 +5,5 @@ backend_sources += files(
   'extended_stats.c',
   'mcv.c',
   'mvdistinct.c',
+  'statistics.c',
 )
diff --git a/src/backend/statistics/statistics.c b/src/backend/statistics/statistics.c
new file mode 100644
index 0000000000..8c6b159514
--- /dev/null
+++ b/src/backend/statistics/statistics.c
@@ -0,0 +1,1673 @@
+/*-------------------------------------------------------------------------
+ * statistics.c
+ *
+ *	  POSTGRES statistics import
+ *
+ * Code supporting the direct importation of relation statistics, similar to
+ * what is done by the ANALYZE command.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *       src/backend/statistics/statistics.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_collation.h"
+#include "catalog/pg_database.h"
+#include "catalog/pg_operator.h"
+#include "catalog/pg_type.h"
+#include "fmgr.h"
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "nodes/nodeFuncs.h"
+#include "statistics/statistics.h"
+#include "utils/acl.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/elog.h"
+#include "utils/float.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/rangetypes.h"
+#include "utils/syscache.h"
+#include "utils/typcache.h"
+
+/*
+ * Names of parameters found in both pg_set_X_stats and pg_restore_X_stats
+ * functions.
+ */
+const char *relation_name = "relation";
+const char *relpages_name = "relpages";
+const char *reltuples_name = "reltuples";
+const char *relallvisible_name = "relallvisible";
+const char *attname_name = "attname";
+const char *inherited_name = "inherited";
+const char *null_frac_name = "null_frac";
+const char *avg_width_name = "avg_width";
+const char *n_distinct_name = "n_distinct";
+const char *mc_vals_name = "most_common_vals";
+const char *mc_freqs_name = "most_common_freqs";
+const char *histogram_bounds_name = "histogram_bounds";
+const char *correlation_name = "correlation";
+const char *mc_elems_name = "most_common_elems";
+const char *mc_elem_freqs_name = "most_common_elem_freqs";
+const char *elem_count_hist_name = "elem_count_histogram";
+const char *range_length_hist_name = "range_length_histogram";
+const char *range_empty_frac_name = "range_empty_frac";
+const char *range_bounds_hist_name = "range_bounds_histogram";
+
+/*
+ * A role has privileges to set statistics on the relation if any of the
+ * following are true:
+ *   - the role owns the current database and the relation is not shared
+ *   - the role has the MAINTAIN privilege on the relation
+ *
+ */
+static bool
+can_modify_relation(Relation rel)
+{
+	Oid			relid = RelationGetRelid(rel);
+	Form_pg_class reltuple = rel->rd_rel;
+
+	return ((object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId())
+			 && !reltuple->relisshared) ||
+			pg_class_aclcheck(relid, GetUserId(), ACL_MAINTAIN) == ACLCHECK_OK);
+}
+
+/*
+ * Set statistics for a given pg_class entry.
+ *
+ * This does an in-place (i.e. non-transactional) update of pg_class, just as
+ * is done in ANALYZE.
+ *
+ */
+Datum
+pg_set_relation_stats(PG_FUNCTION_ARGS)
+{
+
+	Datum		relation_datum = PG_GETARG_DATUM(0);
+	bool		relation_isnull = PG_ARGISNULL(0);
+	Oid			relation;
+
+	Datum		version_datum = PG_GETARG_DATUM(1);
+	bool		version_isnull = PG_ARGISNULL(1);
+	int			version;
+
+	Datum		relpages_datum = PG_GETARG_DATUM(2);
+	bool		relpages_isnull = PG_ARGISNULL(2);
+	int			relpages;
+
+	Datum		reltuples_datum = PG_GETARG_DATUM(3);
+	bool		reltuples_isnull = PG_ARGISNULL(3);
+	float4		reltuples;
+
+	Datum		relallvisible_datum = PG_GETARG_DATUM(4);
+	bool		relallvisible_isnull = PG_ARGISNULL(4);
+	int			relallvisible;
+
+	Relation	rel;
+	HeapTuple	ctup;
+	Form_pg_class pgcform;
+
+	/* If we don't know what relation we're modifying, give up */
+	if (relation_isnull)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Parameter %s cannot be null", relation_name)));
+		PG_RETURN_BOOL(false);
+	}
+	else
+		relation = DatumGetObjectId(relation_datum);
+
+	/* NULL version means assume current server version */
+	if (version_isnull)
+		version = PG_VERSION_NUM;
+	else
+	{
+		version = DatumGetInt32(version_datum);
+		if (version < 90200)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Cannot export statistics prior to version 9.2")));
+			PG_RETURN_BOOL(false);
+		}
+	}
+
+	if (relpages_isnull)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", relpages_name)));
+		PG_RETURN_BOOL(false);
+	}
+	else
+	{
+		relpages = DatumGetInt32(relpages_datum);
+		if (relpages < -1)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be < -1", relpages_name)));
+			PG_RETURN_BOOL(false);
+		}
+	}
+
+	if (reltuples_isnull)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", reltuples_name)));
+		PG_RETURN_BOOL(false);
+	}
+	else
+	{
+		reltuples = DatumGetFloat4(reltuples_datum);
+		if (reltuples < -1.0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be < -1.0", reltuples_name)));
+			PG_RETURN_BOOL(false);
+		}
+	}
+
+	if (relallvisible_isnull)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", relallvisible_name)));
+		PG_RETURN_BOOL(false);
+	}
+	else
+	{
+		relallvisible = DatumGetInt32(relallvisible_datum);
+		if (relallvisible < -1)
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be < -1", relallvisible_name)));
+			PG_RETURN_BOOL(false);
+		}
+	}
+
+	/*
+	 * Open the relation, getting ShareUpdateExclusiveLock to ensure that no
+	 * other stat-setting operation can run on it concurrently.
+	 */
+	rel = table_open(RelationRelationId, ShareUpdateExclusiveLock);
+
+	ctup = SearchSysCacheCopy1(RELOID, relation_datum);
+	if (!HeapTupleIsValid(ctup))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_OBJECT_IN_USE),
+				 errmsg("pg_class entry for relid %u not found", relation)));
+		PG_RETURN_BOOL(false);
+	}
+
+	pgcform = (Form_pg_class) GETSTRUCT(ctup);
+
+	if (!can_modify_relation(rel))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						RelationGetRelationName(rel))));
+		PG_RETURN_BOOL(false);
+	}
+
+	/* Only update pg_class if there is a meaningful change */
+	if ((pgcform->reltuples != reltuples)
+		|| (pgcform->relpages != relpages)
+		|| (pgcform->relallvisible != relallvisible))
+	{
+		pgcform->relpages = relpages;
+		pgcform->reltuples = reltuples;
+		pgcform->relallvisible = relallvisible;
+
+		heap_inplace_update(rel, ctup);
+	}
+
+	table_close(rel, NoLock);
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * Restore statistics for a given pg_class entry.
+ *
+ * This does an in-place (i.e. non-transactional) update of pg_class, just as
+ * is done in ANALYZE.
+ *
+ * The actual modification of stats happesn in a call to pg_set_relation_stats(),
+ * which has a named parameters for each statistic type. This function serves
+ * as a way to allow stats import calls written for a previous version to work
+ * on the current version, regardless of what parameters were introduced or
+ * removed in the time in between.
+ *
+ */
+Datum
+pg_restore_relation_stats(PG_FUNCTION_ARGS)
+{
+	Datum		relation_datum = PG_GETARG_DATUM(0);
+	bool		relation_isnull = PG_ARGISNULL(0);
+
+	Datum		version_datum = PG_GETARG_DATUM(1);
+	bool		version_isnull = PG_ARGISNULL(1);
+
+	/* Flags to indicate that a datum has been found once already */
+	bool		relpages_set = false;
+	bool		reltuples_set = false;
+	bool		relallvisible_set = false;
+
+	/* build argument values to build the object */
+	Datum	   *args;
+	bool	   *argnulls;
+	Oid		   *types;
+	int			nargs;
+
+	LOCAL_FCINFO(set_fcinfo, 5);
+	Datum		result;
+
+	nargs = extract_variadic_args(fcinfo, 2, true, &args, &types, &argnulls);
+
+	/* if the pairs aren't pairs, something is malformed */
+	if (nargs % 2 == 1)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("stats parameters must be in name-value pairs")));
+		PG_RETURN_BOOL(false);
+	}
+
+	InitFunctionCallInfoData(*set_fcinfo, NULL, 5, InvalidOid, NULL, NULL);
+
+	set_fcinfo->args[0].value = relation_datum;
+	set_fcinfo->args[0].isnull = relation_isnull;
+	set_fcinfo->args[1].value = version_datum;
+	set_fcinfo->args[1].isnull = version_isnull;
+
+	/* Assume an argument is NULL unless matching pair found */
+	for (int i = 2; i < 5; i++)
+	{
+		set_fcinfo->args[i].value = (Datum) 0;
+		set_fcinfo->args[i].isnull = true;
+	}
+
+	/* loop through args, matching params to their arg indexes */
+	for (int i = 0; i < nargs; i += 2)
+	{
+		char	   *statname;
+		int			argidx = i + 1;
+
+		if (types[i] != TEXTOID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("stat names must be of type text")));
+			continue;
+		}
+		statname = TextDatumGetCString(args[i]);
+
+		/*
+		 * Match each named parameter to the index of the value that follows
+		 * it. Skip past any duplicates. Verify that value parameter is of the
+		 * correct type.
+		 */
+		if (strcmp(statname, relpages_name) == 0)
+		{
+			if (relpages_set)
+			{
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("stat name %s already used, subsequent values ignored",
+								relpages_name)));
+				continue;
+			}
+			else if (types[argidx] != INT4OID)
+			{
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be of type integer",
+								relpages_name)));
+				PG_RETURN_BOOL(false);
+			}
+			else
+			{
+				relpages_set = true;
+				set_fcinfo->args[2].value = args[argidx];
+				set_fcinfo->args[2].isnull = argnulls[argidx];
+			}
+		}
+		else if (strcmp(statname, reltuples_name) == 0)
+		{
+			if (reltuples_set)
+			{
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("stat name %s already used, subsequent values ignored",
+								reltuples_name)));
+				continue;
+			}
+			else if (types[argidx] != FLOAT4OID)
+			{
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be of type real", reltuples_name)));
+				PG_RETURN_BOOL(false);
+			}
+			else
+			{
+				reltuples_set = true;
+				set_fcinfo->args[3].value = args[argidx];
+				set_fcinfo->args[3].isnull = argnulls[argidx];
+			}
+		}
+		else if (strcmp(statname, relallvisible_name) == 0)
+		{
+			if (relallvisible_set)
+			{
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("stat name %s already used, subsequent values ignored",
+								relallvisible_name)));
+				continue;
+			}
+			else if (types[argidx] != INT4OID)
+			{
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be of type integer",
+								relallvisible_name)));
+				PG_RETURN_BOOL(false);
+			}
+			else
+			{
+				relallvisible_set = true;
+				set_fcinfo->args[4].value = args[argidx];
+				set_fcinfo->args[4].isnull = argnulls[argidx];
+			}
+		}
+	}
+
+	result = (*pg_set_relation_stats) (set_fcinfo);
+
+	PG_RETURN_DATUM(result);
+}
+
+/*
+ * Test if the type is a scalar for MCELEM purposes
+ */
+static bool
+type_is_scalar(Oid typid)
+{
+	HeapTuple	tp;
+	bool		result = false;
+
+	tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+	if (HeapTupleIsValid(tp))
+	{
+		Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+
+		result = (!OidIsValid(typtup->typanalyze));
+		ReleaseSysCache(tp);
+	}
+	return result;
+}
+
+/*
+ * If this relation is an index and that index has expressions in it, and
+ * the attnum specified is known to be an expression, then we must walk
+ * the list attributes up to the specified attnum to get the right
+ * expression.
+ */
+static Node *
+get_attr_expr(Relation rel, int attnum)
+{
+	if ((rel->rd_rel->relkind == RELKIND_INDEX
+		 || (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+		&& (rel->rd_indexprs != NIL)
+		&& (rel->rd_index->indkey.values[attnum - 1] == 0))
+	{
+		ListCell   *indexpr_item = list_head(rel->rd_indexprs);
+
+		for (int i = 0; i < attnum - 1; i++)
+			if (rel->rd_index->indkey.values[i] == 0)
+				indexpr_item = lnext(rel->rd_indexprs, indexpr_item);
+
+		if (indexpr_item == NULL)	/* shouldn't happen */
+			elog(ERROR, "too few entries in indexprs list");
+
+		return (Node *) lfirst(indexpr_item);
+	}
+	return NULL;
+}
+
+/*
+ * Fetch datatype information, this is needed to derive the proper staopN
+ * and stacollN values.
+ *
+ */
+static TypeCacheEntry *
+get_attr_stat_type(Relation rel, Name attname,
+				   int16 *attnum, int32 *typmod, Oid *typcoll)
+{
+	Oid			relid = RelationGetRelid(rel);
+	Form_pg_attribute attr;
+	HeapTuple	atup;
+	Oid			typid;
+	Node	   *expr;
+
+	atup = SearchSysCache2(ATTNAME, ObjectIdGetDatum(relid),
+						   NameGetDatum(attname));
+
+	/* Attribute not found */
+	if (!HeapTupleIsValid(atup))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s has no attname %s",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+		return NULL;
+	}
+
+	attr = (Form_pg_attribute) GETSTRUCT(atup);
+	if (attr->attisdropped)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s attname %s is dropped",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+		return NULL;
+	}
+	*attnum = attr->attnum;
+
+	expr = get_attr_expr(rel, attr->attnum);
+
+	if (expr == NULL)
+	{
+		/* regular attribute */
+		typid = attr->atttypid;
+		*typmod = attr->atttypmod;
+		*typcoll = attr->attcollation;
+	}
+	else
+	{
+		typid = exprType(expr);
+		*typmod = exprTypmod(expr);
+
+		/*
+		 * If a collation has been specified for the index column, use that in
+		 * preference to anything else; but if not, fall back to whatever we
+		 * can get from the expression.
+		 */
+		if (OidIsValid(attr->attcollation))
+			*typcoll = attr->attcollation;
+		else
+			*typcoll = exprCollation(expr);
+	}
+	ReleaseSysCache(atup);
+
+	/* if it's a multirange, step down to the range type */
+	if (type_is_multirange(typid))
+		typid = get_multirange_range(typid);
+
+	return lookup_type_cache(typid,
+							 TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR);
+}
+
+/*
+ * Perform the cast of a known TextDatum into the type specified.
+ *
+ * If no errors are found, ok is set to true. Otherwise, set ok
+ * to false, capture the error found, and re-throw at warning level.
+ */
+static Datum
+cast_stavalues(FmgrInfo *flinfo, Datum d, Oid typid, int32 typmod, bool *ok)
+{
+	LOCAL_FCINFO(fcinfo, 8);
+	char	   *s;
+	Datum		result;
+	ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+	escontext.details_wanted = true;
+
+	s = TextDatumGetCString(d);
+
+	InitFunctionCallInfoData(*fcinfo, flinfo, 3, InvalidOid,
+							 (Node *) &escontext, NULL);
+
+	fcinfo->args[0].value = CStringGetDatum(s);
+	fcinfo->args[0].isnull = false;
+	fcinfo->args[1].value = ObjectIdGetDatum(typid);
+	fcinfo->args[1].isnull = false;
+	fcinfo->args[2].value = Int32GetDatum(typmod);
+	fcinfo->args[2].isnull = false;
+
+	result = FunctionCallInvoke(fcinfo);
+
+	if (SOFT_ERROR_OCCURRED(&escontext))
+	{
+		escontext.error_data->elevel = WARNING;
+		ThrowErrorData(escontext.error_data);
+		*ok = false;
+	}
+	else
+		*ok = true;
+
+	pfree(s);
+
+	return result;
+}
+
+
+/*
+ * Check array for any NULLs, and optionally for one-dimensionality.
+ *
+ * Report any failures as warnings.
+ */
+static bool
+array_check(Datum datum, int one_dim, const char *statname)
+{
+	ArrayType  *arr = DatumGetArrayTypeP(datum);
+	int16		elmlen;
+	char		elmalign;
+	bool		elembyval;
+	Datum	   *values;
+	bool	   *nulls;
+	int			nelems;
+
+	if (one_dim && (arr->ndim != 1))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be a multidimensional array", statname)));
+		return false;
+	}
+
+	get_typlenbyvalalign(ARR_ELEMTYPE(arr), &elmlen, &elembyval, &elmalign);
+
+	deconstruct_array(arr, ARR_ELEMTYPE(arr), elmlen, elembyval, elmalign,
+					  &values, &nulls, &nelems);
+
+	for (int i = 0; i < nelems; i++)
+		if (nulls[i])
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s array cannot contain NULL values", statname)));
+			return false;
+		}
+	return true;
+}
+
+/*
+ * Update the pg_statistic record.
+ */
+static void
+update_pg_statistic(Datum values[], bool nulls[])
+{
+	Relation	sd = table_open(StatisticRelationId, RowExclusiveLock);
+	CatalogIndexState indstate = CatalogOpenIndexes(sd);
+	HeapTuple	oldtup;
+	HeapTuple	stup;
+
+	/* Is there already a pg_statistic tuple for this attribute? */
+	oldtup = SearchSysCache3(STATRELATTINH,
+							 values[Anum_pg_statistic_starelid - 1],
+							 values[Anum_pg_statistic_staattnum - 1],
+							 values[Anum_pg_statistic_stainherit - 1]);
+
+	if (HeapTupleIsValid(oldtup))
+	{
+		/* Yes, replace it */
+		bool		replaces[Natts_pg_statistic];
+
+		for (int i = 0; i < Natts_pg_statistic; i++)
+			replaces[i] = true;
+
+		stup = heap_modify_tuple(oldtup, RelationGetDescr(sd),
+								 values, nulls, replaces);
+		ReleaseSysCache(oldtup);
+		CatalogTupleUpdateWithInfo(sd, &stup->t_self, stup, indstate);
+	}
+	else
+	{
+		/* No, insert new tuple */
+		stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
+		CatalogTupleInsertWithInfo(sd, stup, indstate);
+	}
+
+	heap_freetuple(stup);
+	CatalogCloseIndexes(indstate);
+	table_close(sd, NoLock);
+}
+
+
+
+/*
+ * Import statistics for a given relation attribute.
+ *
+ * This will insert/replace a row in pg_statistic for the given relation and
+ * attribute name.
+ *
+ * The function takes input parameters that correspond to columns in the view
+ * pg_stats.
+ *
+ * Of those, the columns attname, inherited, null_frac, avg_width, and
+ * n_distinct all correspond to NOT NULL columns in pg_statistic. These
+ * parameters have no default value and passing NULL to them will result
+ * in an error.
+ *
+ * If there is no attribute with a matching attname in the relation, the
+ * function will raise an error. Likewise for setting inherited statistics
+ * on a table that is not partitioned.
+ *
+ * The remaining parameters all belong to a specific stakind. Some stakinds
+ * have multiple parameters, and in those cases both parameters must be
+ * NOT NULL or both NULL, otherwise an error will be raised.
+ *
+ * Omitting a parameter or explicitly passing NULL means that that particular
+ * stakind is not associated with the attribute.
+ *
+ * Parameters that are NOT NULL will be inspected for consistency checks,
+ * any of which can raise an error.
+ *
+ * Parameters corresponding to ANYARRAY columns are instead passed in as text
+ * values, which is a valid input string for an array of the type or element
+ * type of the attribute. Any error generated by the array_in() function will
+ * in turn fail the function.
+ */
+Datum
+pg_set_attribute_stats(PG_FUNCTION_ARGS)
+{
+	Datum		relation_datum = PG_GETARG_DATUM(0);
+	bool		relation_isnull = PG_ARGISNULL(0);
+	Oid			relation = PG_GETARG_OID(0);
+
+	Name		attname = PG_GETARG_NAME(1);
+	bool		attname_isnull = PG_ARGISNULL(1);
+
+	Datum		inherited_datum = PG_GETARG_DATUM(2);
+	bool		inherited_isnull = PG_ARGISNULL(2);
+	/* TODO
+	bool		inherited = PG_GETARG_BOOL(2);
+	*/
+
+	int			version = PG_GETARG_INT32(3);
+	int			version_isnull = PG_ARGISNULL(3);
+
+	Datum		null_frac_datum = PG_GETARG_DATUM(4);
+	bool		null_frac_isnull = PG_ARGISNULL(4);
+
+	Datum		avg_width_datum = PG_GETARG_DATUM(5);
+	bool		avg_width_isnull = PG_ARGISNULL(5);
+
+	Datum		n_distinct_datum = PG_GETARG_DATUM(6);
+	bool		n_distinct_isnull = PG_ARGISNULL(6);
+
+	Datum		mc_vals_datum = PG_GETARG_DATUM(7);
+	bool		mc_vals_isnull = PG_ARGISNULL(7);
+
+	Datum		mc_freqs_datum = PG_GETARG_DATUM(8);
+	bool		mc_freqs_isnull = PG_ARGISNULL(8);
+
+	Datum		histogram_bounds_datum = PG_GETARG_DATUM(9);
+	bool		histogram_bounds_isnull = PG_ARGISNULL(9);
+
+	Datum		correlation_datum = PG_GETARG_DATUM(10);
+	bool		correlation_isnull = PG_ARGISNULL(10);
+
+	Datum		mc_elems_datum = PG_GETARG_DATUM(11);
+	bool		mc_elems_isnull = PG_ARGISNULL(11);
+
+	Datum		mc_elem_freqs_datum = PG_GETARG_DATUM(12);
+	bool		mc_elem_freqs_isnull = PG_ARGISNULL(12);
+
+	Datum		elem_count_hist_datum = PG_GETARG_DATUM(13);
+	bool		elem_count_hist_isnull = PG_ARGISNULL(13);
+
+	Datum		range_length_hist_datum = PG_GETARG_DATUM(14);
+	bool		range_length_hist_isnull = PG_ARGISNULL(14);
+
+	Datum		range_empty_frac_datum = PG_GETARG_DATUM(15);
+	bool		range_empty_frac_isnull = PG_ARGISNULL(15);
+
+	Datum		range_bounds_hist_datum = PG_GETARG_DATUM(16);
+	bool		range_bounds_hist_isnull = PG_ARGISNULL(16);
+
+	Relation	rel;
+
+	TypeCacheEntry *typcache;
+	TypeCacheEntry *elemtypcache = NULL;
+
+	int16		attnum;
+	int32		typmod;
+	Oid			typcoll;
+
+	FmgrInfo	finfo;
+
+	int			stakind_count;
+
+	/* known-used */
+	bool		has_mcv = false;
+	bool		has_mc_elems = false;
+	bool		has_rl_hist = false;
+
+	int			k = 0;
+
+	Datum		values[Natts_pg_statistic];
+	bool		nulls[Natts_pg_statistic];
+
+	/* initialize output tuple */
+	for (int i = 0; i < Natts_pg_statistic; i++)
+	{
+		values[i] = (Datum) 0;
+		nulls[i] = false;
+	}
+
+	/*
+	 * Some parameters are "required" in that nothing can happen if any of
+	 * them are NULL.
+	 */
+	if (relation_isnull)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", relation_name)));
+		PG_RETURN_BOOL(false);
+	}
+
+	if (attname_isnull)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", attname_name)));
+		PG_RETURN_BOOL(false);
+	}
+
+	if (inherited_isnull)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", inherited_name)));
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * NULL version means assume current server version
+	 */
+	if (version_isnull)
+		version = PG_VERSION_NUM;
+	else
+	{
+		if (version < 90200)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Cannot export statistics prior to version 9.2")));
+			PG_RETURN_BOOL(false);
+		}
+	}
+
+	if (null_frac_isnull)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", null_frac_name)));
+		PG_RETURN_BOOL(false);
+	}
+
+	if (avg_width_isnull)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", avg_width_name)));
+		PG_RETURN_BOOL(false);
+	}
+
+	if (n_distinct_isnull)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", n_distinct_name)));
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * Params most_common_vals and most_common_freqs are linked
+	 */
+	if ((!mc_vals_isnull) && (!mc_freqs_isnull))
+		has_mcv = true;
+	else if (mc_vals_isnull != mc_freqs_isnull)
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL when %s is NOT NULL",
+						mc_vals_isnull ? mc_vals_name : mc_freqs_name,
+						!mc_vals_isnull ? mc_vals_name : mc_freqs_name)));
+
+	/*
+	 * Params most_common_elems and most_common_elem_freqs are linked
+	 */
+	if ((!mc_elems_isnull) && (!mc_elem_freqs_isnull))
+		has_mc_elems = true;
+	else if (mc_elems_isnull != mc_elem_freqs_isnull)
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL when %s is NOT NULL",
+						mc_elems_isnull ? mc_elems_name : mc_elem_freqs_name,
+						!mc_elems_isnull ? mc_elems_name : mc_elem_freqs_name)));
+
+	/*
+	 * Params range_empty_frac and range_length_histogram are linked
+	 */
+	if ((!range_length_hist_isnull) && (!range_empty_frac_isnull))
+		has_rl_hist = true;
+	else if (range_length_hist_isnull != range_empty_frac_isnull)
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL when %s is NOT NULL",
+						range_length_hist_isnull ?
+						range_length_hist_name : range_empty_frac_name,
+						!range_length_hist_isnull ?
+						range_length_hist_name : range_empty_frac_name)));
+
+	/*
+	 * If a caller specifies more stakind-stats than we have slots to store
+	 * them, reject them all
+	 */
+	stakind_count = (int) has_mcv + (int) has_mc_elems + (int) has_rl_hist +
+		(int) !histogram_bounds_isnull + (int) !correlation_isnull +
+		(int) !elem_count_hist_isnull + (int) !range_bounds_hist_isnull;
+
+	if (stakind_count > STATISTIC_NUM_SLOTS)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("imported statistics must have a maximum of %d slots "
+						"but %d given",
+						STATISTIC_NUM_SLOTS, stakind_count)));
+		PG_RETURN_BOOL(false);
+	}
+
+	rel = try_relation_open(relation, ShareUpdateExclusiveLock);
+
+	if (rel == NULL)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Parameter relation OID %u is invalid", relation)));
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * Many of the values that are set for a particular stakind are entirely
+	 * derived from the attribute itself, or it's expression.
+	 */
+	typcache = get_attr_stat_type(rel, attname, &attnum, &typmod, &typcoll);
+	if (typcache == NULL)
+	{
+		relation_close(rel, NoLock);
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("unexpected typecache error")));
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * Derive element type if we have stat kinds that need it.
+	 *
+	 * This duplicates some type-specific logic found in various typanalyze
+	 * functions which are called from vacuum's examine_attribute(), but using
+	 * that directly has proven awkward.
+	 */
+	if (has_mc_elems || !elem_count_hist_isnull)
+	{
+		Oid			elemtypid;
+
+		if (typcache->type_id == TSVECTOROID)
+		{
+			/*
+			 * tsvectors always have a text oid base type and default
+			 * collation
+			 */
+			elemtypid = TEXTOID;
+			typcoll = DEFAULT_COLLATION_OID;
+		}
+		else if (typcache->typtype == TYPTYPE_RANGE)
+			elemtypid = get_range_subtype(typcache->type_id);
+		else
+			elemtypid = get_base_element_type(typcache->type_id);
+
+		/* not finding a basetype means we already had it */
+		if (elemtypid == InvalidOid)
+			elemtypid = typcache->type_id;
+
+		/* The stats need the eq_opr, any validation would need the lt_opr */
+		elemtypcache = lookup_type_cache(elemtypid, TYPECACHE_EQ_OPR);
+		if (elemtypcache == NULL)
+		{
+			/* warn and ignore any stats that can't be fulfilled */
+			has_mc_elems = false;
+
+			if (!mc_elems_isnull)
+			{
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s cannot accept %s stats, ignored",
+								NameStr(*attname),
+								mc_elems_name)));
+				mc_elems_isnull = true;
+			}
+
+			if (!mc_elem_freqs_isnull)
+			{
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s cannot accept %s stats, ignored",
+								NameStr(*attname),
+								mc_elem_freqs_name)));
+				mc_elem_freqs_isnull = true;
+			}
+
+			if (!elem_count_hist_isnull)
+			{
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s cannot accept %s stats, ignored",
+								NameStr(*attname),
+								elem_count_hist_name)));
+				elem_count_hist_isnull = true;
+			}
+		}
+	}
+
+	/*
+	 * histogram_bounds and correlation must have a type < operator
+	 */
+	if (typcache->lt_opr == InvalidOid)
+	{
+		if (!histogram_bounds_isnull)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+					 errmsg("Relation %s attname %s cannot "
+							"have stats of type %s, ignored.",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							histogram_bounds_name)));
+			histogram_bounds_isnull = true;
+		}
+
+		if (!correlation_isnull)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+					 errmsg("Relation %s attname %s cannot "
+							"have stats of type %s, ignored.",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							correlation_name)));
+			correlation_isnull = true;
+		}
+	}
+
+	/*
+	 * Scalar types can't have most_common_elems, most_common_elem_freqs, or
+	 * element_count_histogram
+	 */
+	if (type_is_scalar(typcache->type_id))
+	{
+		/* warn and ignore any stats that can't be fulfilled */
+		has_mc_elems = false;
+
+		if (!mc_elems_isnull)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+					 errmsg("Relation %s attname %s is a scalar type, "
+							"cannot have stats of type %s, ignored",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							mc_elems_name)));
+			mc_elems_isnull = true;
+		}
+
+		if (!mc_elem_freqs_isnull)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+					 errmsg("Relation %s attname %s is a scalar type, "
+							"cannot have stats of type %s, ignored",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							mc_elem_freqs_name)));
+			mc_elem_freqs_isnull = true;
+		}
+
+		if (!elem_count_hist_isnull)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+					 errmsg("Relation %s attname %s is a scalar type, "
+							"cannot have stats of type %s, ignored",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							elem_count_hist_name)));
+			elem_count_hist_isnull = true;
+		}
+	}
+
+	/* Only range types can have range stats */
+	if ((typcache->typtype != TYPTYPE_RANGE) &&
+		(typcache->typtype != TYPTYPE_MULTIRANGE))
+	{
+		/* warn and ignore any stats that can't be fulfilled */
+		has_rl_hist = false;
+
+		if (!range_length_hist_isnull)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+					 errmsg("Relation %s attname %s is not a range type, "
+							"cannot have stats of type %s",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							range_length_hist_name)));
+			range_length_hist_isnull = true;
+		}
+
+		if (!range_empty_frac_isnull)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+					 errmsg("Relation %s attname %s is not a range type, "
+							"cannot have stats of type %s",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							range_empty_frac_name)));
+			range_empty_frac_isnull = true;
+		}
+		if (!range_bounds_hist_isnull)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+					 errmsg("Relation %s attname %s is not a range type, "
+							"cannot have stats of type %s",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							range_bounds_hist_name)));
+			range_bounds_hist_isnull = true;
+		}
+	}
+
+	if (!can_modify_relation(rel))
+	{
+		relation_close(rel, NoLock);
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						RelationGetRelationName(rel))));
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * TODO: this test may not be necessary
+	 *
+	 * On the one hand, we don't want to be able to inject stats that
+	 * cannot be removed. On the other hand, we may want to inject inherited
+	 * stats on a table that will later be inherited by another stats
+	 */
+	/*
+	if (inherited &&
+		(rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE) &&
+		(rel->rd_rel->relkind != RELKIND_PARTITIONED_INDEX) &&
+		(rel->rd_rel->relhassubclass == false))
+	{
+		relation_close(rel, NoLock);
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("%s is not partitioned, cannot accepted inherited stats",
+						RelationGetRelationName(rel))));
+		PG_RETURN_BOOL(false);
+	}
+	*/
+
+	/* Populate pg_statistic tuple */
+	values[Anum_pg_statistic_starelid - 1] = relation_datum;
+	values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(attnum);
+	values[Anum_pg_statistic_stainherit - 1] = inherited_datum;
+	values[Anum_pg_statistic_stanullfrac - 1] = null_frac_datum;
+	values[Anum_pg_statistic_stawidth - 1] = avg_width_datum;
+	values[Anum_pg_statistic_stadistinct - 1] = n_distinct_datum;
+
+	fmgr_info(F_ARRAY_IN, &finfo);
+
+	/*
+	 * STATISTIC_KIND_MCV
+	 *
+	 * most_common_vals: ANYARRAY::text most_common_freqs: real[]
+	 */
+	if (has_mcv)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_MCV);
+		Datum		staop = ObjectIdGetDatum(typcache->eq_opr);
+		Datum		stacoll = ObjectIdGetDatum(typcoll);
+		bool		converted = false;
+		Datum		stanumbers = mc_freqs_datum;
+		Datum		stavalues = cast_stavalues(&finfo, mc_vals_datum,
+											   typcache->type_id, typmod,
+											   &converted);
+
+		if (converted &&
+			array_check(stavalues, false, mc_vals_name) &&
+			array_check(stanumbers, true, mc_freqs_name))
+		{
+			values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+			values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+			values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+			values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(typcache->eq_opr);
+			values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+			values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+			values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+			k++;
+		}
+	}
+
+
+	/*
+	 * STATISTIC_KIND_HISTOGRAM
+	 *
+	 * histogram_bounds: ANYARRAY::text
+	 */
+	if (!histogram_bounds_isnull)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_HISTOGRAM);
+		Datum		staop = ObjectIdGetDatum(typcache->lt_opr);
+		Datum		stacoll = ObjectIdGetDatum(typcoll);
+		Datum		stavalues;
+		bool		converted = false;
+
+		stavalues = cast_stavalues(&finfo, histogram_bounds_datum,
+								   typcache->type_id, typmod, &converted);
+
+		if (converted && array_check(stavalues, false, histogram_bounds_name))
+		{
+			values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+			values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+			values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+			nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+			values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+			k++;
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_CORRELATION
+	 *
+	 * correlation: real
+	 */
+	if (!correlation_isnull)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_CORRELATION);
+		Datum		staop = ObjectIdGetDatum(typcache->lt_opr);
+		Datum		stacoll = ObjectIdGetDatum(typcoll);
+		Datum		elems[] = {correlation_datum};
+		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+		Datum		stanumbers = PointerGetDatum(arry);
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+		values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+		k++;
+	}
+
+	/*
+	 * STATISTIC_KIND_MCELEM
+	 *
+	 * most_common_elem_freqs: real[]
+	 *
+	 * most_common_elems     : ANYARRAY::text
+	 */
+	if (has_mc_elems)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_MCELEM);
+		Datum		staop = ObjectIdGetDatum(elemtypcache->eq_opr);
+		Datum		stacoll = ObjectIdGetDatum(typcoll);
+		Datum		stanumbers = mc_elem_freqs_datum;
+		bool		converted = false;
+		Datum		stavalues;
+
+		stavalues = cast_stavalues(&finfo, mc_elems_datum,
+								   elemtypcache->type_id, typmod, &converted);
+
+		if (converted &&
+			array_check(stavalues, false, mc_elems_name) &&
+			array_check(stanumbers, true, mc_elem_freqs_name))
+		{
+			values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+			values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+			values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+			values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+			values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+			k++;
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_DECHIST
+	 *
+	 * elem_count_histogram:	real[]
+	 */
+	if (!elem_count_hist_isnull)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_DECHIST);
+		Datum		staop = ObjectIdGetDatum(elemtypcache->eq_opr);
+		Datum		stacoll = ObjectIdGetDatum(typcoll);
+		Datum		stanumbers = elem_count_hist_datum;
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+		values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+		k++;
+	}
+
+	/*
+	 * STATISTIC_KIND_BOUNDS_HISTOGRAM
+	 *
+	 * range_bounds_histogram: ANYARRAY::text
+	 *
+	 * This stakind appears before STATISTIC_KIND_BOUNDS_HISTOGRAM even though
+	 * it is numerically greater, and all other stakinds appear in numerical
+	 * order. We duplicate this quirk to make before/after tests of
+	 * pg_statistic records easier.
+	 */
+	if (!range_bounds_hist_isnull)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_BOUNDS_HISTOGRAM);
+		Datum		staop = ObjectIdGetDatum(InvalidOid);
+		Datum		stacoll = ObjectIdGetDatum(InvalidOid);
+
+		bool		converted = false;
+		Datum		stavalues;
+
+		stavalues = cast_stavalues(&finfo, range_bounds_hist_datum,
+								   typcache->type_id, typmod, &converted);
+
+		if (converted &&
+			array_check(stavalues, false, "range_bounds_histogram"))
+		{
+			values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+			values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+			values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+			nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+			values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+			k++;
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM
+	 *
+	 * range_empty_frac: real
+	 *
+	 * range_length_histogram:  double precision[]::text
+	 */
+	if (has_rl_hist)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM);
+		Datum		staop = ObjectIdGetDatum(Float8LessOperator);
+		Datum		stacoll = ObjectIdGetDatum(InvalidOid);
+
+		/* The anyarray is always a float8[] for this stakind */
+		Datum		elems[] = {range_empty_frac_datum};
+		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+		Datum		stanumbers = PointerGetDatum(arry);
+
+		bool		converted = false;
+		Datum		stavalues;
+
+		stavalues = cast_stavalues(&finfo, range_length_hist_datum, FLOAT8OID,
+								   0, &converted);
+
+		if (converted &&
+			array_check(stavalues, false, range_length_hist_name))
+		{
+			values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+			values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+			values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+			values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+			values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+			k++;
+		}
+	}
+
+	/* fill in all remaining slots */
+	while (k < STATISTIC_NUM_SLOTS)
+	{
+		values[Anum_pg_statistic_stakind1 - 1 + k] = Int16GetDatum(0);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+		k++;
+	}
+
+	update_pg_statistic(values, nulls);
+
+	relation_close(rel, NoLock);
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * Import statistics for a given relation attribute.
+ *
+ * This will insert/replace a row in pg_statistic for the given combinarion
+ * of relation, attribute, and inherited flag.
+ *
+ * The version parameter is for future use in the events as future versions
+ * may change them meaning of certain parameters.
+ *
+ * The variadic parameters all represent name-value pairs, with the names
+ * corresponding to attributes in pg_stats. Unkown names will generate a
+ * warning.
+ *
+ * Parameters null_frac, avg_width, and n_distinct are required because
+ * those attributes have no default value in pg_statistic.
+ *
+ * The remaining parameters all belong to a specific stakind, and all are
+ * optional. Some stakinds have multiple parameters, and in those cases
+ * both parameters must be specified if one of them is, otherwise a
+ * warning is generated but the rest of the stats may still be imported.
+ *
+ * If there is no attribute with a matching attname in the relation, the
+ * function will raise a warning and return false.
+ *
+ * Parameters corresponding to ANYARRAY columns are instead passed in as text
+ * values, which is a valid input string for an array of the type or element
+ * type of the attribute.
+ */
+Datum
+pg_restore_attribute_stats(PG_FUNCTION_ARGS)
+{
+	/* build argument values to build the object */
+	Datum	   *args;
+	bool	   *argnulls;
+	Oid		   *types;
+	int			nargs;
+
+	LOCAL_FCINFO(set_fcinfo, 17);
+	Datum		result;
+
+	InitFunctionCallInfoData(*set_fcinfo, NULL, 17, InvalidOid, NULL, NULL);
+
+	/* line up positional arguments for pg_set_attribute_stats */
+	for (int i = 0; i <= 3; i++)
+	{
+		set_fcinfo->args[i].value = PG_GETARG_DATUM(i);
+		set_fcinfo->args[i].isnull = PG_ARGISNULL(i);
+	}
+
+	/* initialize remaining arguments which may not get set */
+	for (int i = 4; i < 17; i++)
+	{
+		set_fcinfo->args[i].value = (Datum) 0;
+		set_fcinfo->args[i].isnull = true;
+	}
+
+	nargs = extract_variadic_args(fcinfo, 4, true, &args, &types, &argnulls);
+
+	/* if the pairs aren't pairs, something is malformed */
+	if (nargs % 2 == 1)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("stats parameters must be in name-value pairs")));
+		PG_RETURN_BOOL(false);
+	}
+
+	/* iterate through variagic arguments, putting them in set_fcinfo */
+	for (int i = 0; i < nargs; i += 2)
+	{
+		char	   *statname;
+		int			argidx = i + 1;
+
+		if (types[i] != TEXTOID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("stat names must be of type text")));
+			break;
+		}
+
+		statname = TextDatumGetCString(args[i]);
+
+		if (strcmp(statname, null_frac_name) == 0)
+		{
+			if (types[argidx] == FLOAT4OID)
+			{
+				set_fcinfo->args[4].value = args[argidx];
+				set_fcinfo->args[4].isnull = argnulls[argidx];
+			}
+			else
+			{
+				/* required param, not recoverable */
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be of type %s",
+								null_frac_name, "real")));
+				PG_RETURN_BOOL(false);
+			}
+			continue;
+		}
+		else if (strcmp(statname, avg_width_name) == 0)
+		{
+			if (types[argidx] == INT4OID)
+			{
+				set_fcinfo->args[5].value = args[argidx];
+				set_fcinfo->args[5].isnull = argnulls[argidx];
+			}
+			else
+			{
+				/* required param, not recoverable */
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be of type %s",
+								avg_width_name, "integer")));
+				PG_RETURN_BOOL(false);
+			}
+			continue;
+		}
+		else if (strcmp(statname, n_distinct_name) == 0)
+		{
+			if (types[argidx] == FLOAT4OID)
+			{
+				set_fcinfo->args[6].value = args[argidx];
+				set_fcinfo->args[6].isnull = argnulls[argidx];
+			}
+			else
+			{
+				/* required param, not recoverable */
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be of type %s",
+								n_distinct_name, "real")));
+				PG_RETURN_BOOL(false);
+			}
+			continue;
+		}
+		else if (strcmp(statname, mc_vals_name) == 0)
+		{
+			if (types[argidx] == TEXTOID)
+			{
+				set_fcinfo->args[7].value = args[argidx];
+				set_fcinfo->args[7].isnull = argnulls[argidx];
+			}
+			else
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be type %s, ignored",
+								mc_vals_name, "text")));
+			continue;
+		}
+		else if (strcmp(statname, mc_freqs_name) == 0)
+		{
+			if (get_element_type(types[argidx]) == FLOAT4OID)
+			{
+				set_fcinfo->args[8].value = args[argidx];
+				set_fcinfo->args[8].isnull = argnulls[argidx];
+			}
+			else
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be type %s, set to NULL",
+								mc_freqs_name, "real[]")));
+			continue;
+		}
+		else if (strcmp(statname, histogram_bounds_name) == 0)
+		{
+			if (types[argidx] == TEXTOID)
+			{
+				set_fcinfo->args[9].value = args[argidx];
+				set_fcinfo->args[9].isnull = argnulls[argidx];
+			}
+			else
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be type %s, set to NULL",
+								histogram_bounds_name, "text")));
+			continue;
+		}
+		else if (strcmp(statname, correlation_name) == 0)
+		{
+			if (types[argidx] == FLOAT4OID)
+			{
+				set_fcinfo->args[10].value = args[argidx];
+				set_fcinfo->args[10].isnull = argnulls[argidx];
+			}
+			else
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be of type %s, set to NULL",
+								correlation_name, "real")));
+			continue;
+		}
+		else if (strcmp(statname, mc_elems_name) == 0)
+		{
+			if (types[argidx] == TEXTOID)
+			{
+				set_fcinfo->args[11].value = args[argidx];
+				set_fcinfo->args[11].isnull = argnulls[argidx];
+			}
+			else
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be of type %s, set to NULL",
+								mc_elems_name, "real")));
+			continue;
+		}
+		else if (strcmp(statname, mc_elem_freqs_name) == 0)
+		{
+			if (get_element_type(types[argidx]) == FLOAT4OID)
+			{
+				set_fcinfo->args[12].value = args[argidx];
+				set_fcinfo->args[12].isnull = argnulls[argidx];
+			}
+			else
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be type %s, set to NULL",
+								mc_elem_freqs_name, "real[]")));
+			continue;
+		}
+		else if (strcmp(statname, elem_count_hist_name) == 0)
+		{
+			if (get_element_type(types[argidx]) == FLOAT4OID)
+			{
+				set_fcinfo->args[13].value = args[argidx];
+				set_fcinfo->args[13].isnull = argnulls[argidx];
+			}
+			else
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be type %s, set to NULL",
+								elem_count_hist_name, "real[]")));
+			continue;
+		}
+		else if (strcmp(statname, range_length_hist_name) == 0)
+		{
+			if (types[argidx] == TEXTOID)
+			{
+				set_fcinfo->args[14].value = args[argidx];
+				set_fcinfo->args[14].isnull = argnulls[argidx];
+			}
+			else
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be type %s, set to NULL",
+								range_length_hist_name, "text")));
+			continue;
+		}
+		else if (strcmp(statname, range_empty_frac_name) == 0)
+		{
+			if (types[argidx] == FLOAT4OID)
+			{
+				set_fcinfo->args[15].value = args[argidx];
+				set_fcinfo->args[15].isnull = argnulls[argidx];
+			}
+			else
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be type %s, set to NULL",
+								range_empty_frac_name, "real")));
+			continue;
+		}
+		else if (strcmp(statname, range_bounds_hist_name) == 0)
+		{
+			if (types[argidx] == TEXTOID)
+			{
+				set_fcinfo->args[16].value = args[argidx];
+				set_fcinfo->args[16].isnull = argnulls[argidx];
+			}
+			else
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be type %s, set to NULL",
+								range_bounds_hist_name, "text")));
+			continue;
+		}
+		else
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("unknown stat name '%s', ignored", statname)));
+
+		pfree(statname);
+	}
+
+	result = (*pg_set_attribute_stats) (set_fcinfo);
+
+	PG_RETURN_DATUM(result);
+}
diff --git a/src/test/regress/expected/stats_export_import.out b/src/test/regress/expected/stats_export_import.out
new file mode 100644
index 0000000000..9e658fa70a
--- /dev/null
+++ b/src/test/regress/expected/stats_export_import.out
@@ -0,0 +1,870 @@
+CREATE SCHEMA stats_export_import;
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+--
+-- pg_restore_relation_stats() invokes pg_set_relation_stats
+-- pg_restore_attribute_stats() invokes pg_set_attribute_stats
+--
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+        0 |        -1 |             0
+(1 row)
+
+-- error: regclass not found
+SELECT pg_restore_relation_stats('stats_export_import.nope'::regclass,
+                             150000::integer);
+ERROR:  relation "stats_export_import.nope" does not exist
+LINE 1: SELECT pg_restore_relation_stats('stats_export_import.nope':...
+                                         ^
+-- error: all three params missing
+SELECT pg_restore_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer);
+ERROR:  function pg_restore_relation_stats(regclass, integer) does not exist
+LINE 1: SELECT pg_restore_relation_stats('stats_export_import.test':...
+               ^
+HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+-- error: reltuples, relallvisible missing
+SELECT pg_restore_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 4::integer);
+WARNING:  reltuples cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+-- error: null value
+SELECT pg_restore_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', NULL::real,
+                             'relallvisible', 4::integer);
+WARNING:  relpages must be of type integer
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+-- error: bad relpages type
+SELECT pg_restore_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+WARNING:  relpages must be of type integer
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+SELECT pg_restore_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 17::integer,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+ pg_restore_relation_stats 
+---------------------------
+ t
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       17 |       400 |             4
+(1 row)
+
+-- error: object doesn't exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    '0'::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+WARNING:  Parameter relation OID 0 is invalid
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- error: relation null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    NULL::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+WARNING:  relation cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- error: attname null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    NULL::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+WARNING:  attname cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- error: inherited null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    NULL::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+WARNING:  inherited cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- error: null_frac null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', NULL::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+WARNING:  null_frac cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- error: avg_width null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', NULL::integer,
+    'n_distinct', 0.3::real);
+WARNING:  avg_width cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- error: avg_width null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', NULL::real);
+WARNING:  n_distinct cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT stanullfrac, stawidth, stadistinct
+FROM pg_statistic
+WHERE starelid = 'stats_export_import.test'::regclass;
+ stanullfrac | stawidth | stadistinct 
+-------------+----------+-------------
+         0.1 |        2 |         0.3
+(1 row)
+
+-- warn: mcv / mcf null mismatch
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_freqs', '{0.1,0.2,0.3}'::real[]
+    );
+WARNING:  most_common_vals cannot be NULL when most_common_freqs is NOT NULL
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{1,2,3}'::text
+    );
+WARNING:  most_common_freqs cannot be NULL when most_common_vals is NOT NULL
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.2,0.1}'::double precision[]
+    );
+WARNING:  most_common_freqs must be type real[], set to NULL
+WARNING:  most_common_freqs cannot be NULL when most_common_vals is NOT NULL
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- warning: mcv cast failure
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,four,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+WARNING:  invalid input syntax for type integer: "four"
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: histogram elements null value
+-- this generates no warnings, but perhaps it should
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,NULL,3,4}'::text
+    );
+WARNING:  histogram_bounds array cannot contain NULL values
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,2,3,4}'::text
+    );
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- ok: correlation
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'correlation', 0.5::real);
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |         0.5 |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have mcelem
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{1,3}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+WARNING:  Relation test attname id is a scalar type, cannot have stats of type most_common_elems, ignored
+WARNING:  Relation test attname id is a scalar type, cannot have stats of type most_common_elem_freqs, ignored
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,two}'::text
+    );
+WARNING:  most_common_elem_freqs cannot be NULL when most_common_elems is NOT NULL
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
+    );
+WARNING:  most_common_elems cannot be NULL when most_common_elem_freqs is NOT NULL
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- ok: mcelem
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             | {one,three}       | {0.3,0.2,0.2,0.3,0}    |                      |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+WARNING:  Relation test attname id is a scalar type, cannot have stats of type elem_count_histogram, ignored
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+WARNING:  Relation test attname id is not a range type, cannot have stats of type range_length_histogram
+WARNING:  Relation test attname id is not a range type, cannot have stats of type range_empty_frac
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+WARNING:  range_empty_frac cannot be NULL when range_length_histogram is NOT NULL
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real
+    );
+WARNING:  range_length_histogram cannot be NULL when range_empty_frac is NOT NULL
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
+(1 row)
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+WARNING:  Relation test attname id is not a range type, cannot have stats of type range_bounds_histogram
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
+ stats_export_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      |                        |                  | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+(1 row)
+
+-- warn: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
+    'histogram_bounds', '{1,2,3,4}'::text,
+    'correlation', 1.1::real,
+    'most_common_elems', '{3,1}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    'range_empty_frac', -0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+WARNING:  imported statistics must have a maximum of 5 slots but 6 given
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+   relname    | num_stats 
+--------------+-----------
+ is_odd       |         1
+ is_odd_clone |         1
+ test         |         5
+ test_clone   |         5
+(4 rows)
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 2429ec2bba..ea99302741 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,7 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_export_import
 
 # ----------
 # Load huge amounts of data
diff --git a/src/test/regress/sql/stats_export_import.sql b/src/test/regress/sql/stats_export_import.sql
new file mode 100644
index 0000000000..f781eec5dd
--- /dev/null
+++ b/src/test/regress/sql/stats_export_import.sql
@@ -0,0 +1,677 @@
+CREATE SCHEMA stats_export_import;
+
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+
+--
+-- pg_restore_relation_stats() invokes pg_set_relation_stats
+-- pg_restore_attribute_stats() invokes pg_set_attribute_stats
+
+--
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+-- error: regclass not found
+SELECT pg_restore_relation_stats('stats_export_import.nope'::regclass,
+                             150000::integer);
+
+-- error: all three params missing
+SELECT pg_restore_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer);
+
+-- error: reltuples, relallvisible missing
+SELECT pg_restore_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 4::integer);
+
+-- error: null value
+SELECT pg_restore_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', NULL::real,
+                             'relallvisible', 4::integer);
+
+-- error: bad relpages type
+SELECT pg_restore_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+
+SELECT pg_restore_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 17::integer,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_export_import.test'::regclass;
+
+-- error: object doesn't exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    '0'::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: relation null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    NULL::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: attname null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    NULL::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: inherited null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    NULL::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: null_frac null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', NULL::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', NULL::integer,
+    'n_distinct', 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', NULL::real);
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+SELECT stanullfrac, stawidth, stadistinct
+FROM pg_statistic
+WHERE starelid = 'stats_export_import.test'::regclass;
+
+-- warn: mcv / mcf null mismatch
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_freqs', '{0.1,0.2,0.3}'::real[]
+    );
+
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{1,2,3}'::text
+    );
+
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.2,0.1}'::double precision[]
+    );
+
+-- warning: mcv cast failure
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,four,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: histogram elements null value
+-- this generates no warnings, but perhaps it should
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,NULL,3,4}'::text
+    );
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,2,3,4}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- ok: correlation
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'correlation', 0.5::real);
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: scalars can't have mcelem
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{1,3}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,two}'::text
+    );
+
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
+    );
+
+-- ok: mcelem
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- warn: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real
+    );
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
+    'histogram_bounds', '{1,2,3,4}'::text,
+    'correlation', 1.1::real,
+    'most_common_elems', '{3,1}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    'range_empty_frac', -0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+
+SELECT
+    format('SELECT pg_catalog.pg_restore_attribute_stats( '
+            || '%L::regclass::oid, '
+            || '%L::name, '
+            || '%L::boolean, '
+            || '%L::integer, '
+            || '%L, %L::real, '
+            || '%L, %L::integer, '
+            || '%L, %L::real %s)',
+        'stats_export_import.' || s.tablename || '_clone',
+        s.attname,
+        s.inherited,
+        150000,
+        'null_frac', s.null_frac,
+        'avg_width', s.avg_width,
+        'n_distinct', s.n_distinct,
+        CASE
+            WHEN s.most_common_vals IS NULL THEN ''
+            ELSE format(', %L, %L::text, %L, %L::real[]',
+                        'most_common_vals', s.most_common_vals,
+                        'most_common_freqs', s.most_common_freqs)
+        END ||
+        CASE
+            WHEN s.histogram_bounds IS NULL THEN ''
+            ELSE format(', %L, %L::text',
+                        'histogram_bounds', s.histogram_bounds)
+        END ||
+        CASE
+            WHEN s.correlation IS NULL THEN ''
+            ELSE format(', %L, %L::real',
+                        'correlation', s.correlation)
+        END ||
+        CASE
+            WHEN s.most_common_elems IS NULL THEN ''
+            ELSE format(', %L, %L::text, %L, %L::real[]',
+                        'most_common_elems', s.most_common_elems,
+                        'most_common_elem_freqs', s.most_common_elem_freqs)
+        END ||
+        CASE
+            WHEN s.elem_count_histogram IS NULL THEN ''
+            ELSE format(', %L, %L::real[]',
+                        'elem_count_histogram', s.elem_count_histogram)
+        END ||
+        CASE
+            WHEN s.range_bounds_histogram IS NULL THEN ''
+            ELSE format(', %L, %L::text',
+                        'range_bounds_histogram', s.range_bounds_histogram)
+        END ||
+        CASE
+            WHEN s.range_empty_frac IS NULL THEN ''
+            ELSE format(', %L, %L::real, %L, %L::text',
+                        'range_empty_frac', s.range_empty_frac,
+                        'range_length_histogram', s.range_length_histogram)
+        END)
+FROM pg_catalog.pg_stats AS s
+WHERE s.schemaname = 'stats_export_import'
+AND s.tablename IN ('test', 'is_odd')
+\gexec
+
+-- restore ECHO to original value
+\set ECHO :orig_ECHO
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 3f93c61aa3..896f87bf74 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29761,6 +29761,405 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
     in identifying the specific disk files associated with database objects.
    </para>
 
+   <table id="functions-admin-statsimport">
+    <title>Database Object Statistics Import Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_relation_stats</primary>
+        </indexterm>
+        <function>pg_set_relation_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>
+         , <parameter>version</parameter> <type>integer</type>,
+         , <parameter>relpages</parameter> <type>integer</type>,
+         , <parameter>reltuples</parameter> <type>real</type>,
+         , <parameter>relallvisible</parameter> <type>integer</type> )
+        <returnvalue>void</returnvalue>
+       </para>
+       <para>
+        Updates the <structname>pg_class</structname> row for the specified
+        <parameter>relation</parameter>, setting the values for the columns
+        <structfield>reltuples</structfield>,
+        <structfield>relpages</structfield>, and
+        <structfield>relallvisible</structfield>.
+        To avoid table bloat in <structname>pg_class</structname>, this change
+        is made with an in-place update, and therefore cannot be rolled back
+        through normal transaction processing.
+       </para>
+       <para>
+        This function mimics the behavior of <command>ANALYZE</command> in its
+        effects on the values in <structname>pg_class</structname>, except that
+        the values are supplied as parameters rather than derived from table
+        sampling.
+       </para>
+       <para>
+        The caller must either be the owner of the relation, or have superuser
+        privileges.
+       </para>
+       <para>
+        The value of <structfield>version</structfield> represents the
+        integer <term><varname>SERVER_VERSION_NUM</varname></term> of the
+        database that was the source of these values. A value of
+        <literal>NULL</literal> means to use the
+        <term><varname>SERVER_VERSION_NUM</varname></term> of the
+        current database. It should be noted that presently this value does
+        not alter the behavior of the function, but it could in future
+        versions.
+       </para>
+       <para>
+        The value of <structfield>relpages</structfield> must not be less than
+        0.
+       </para>
+       <para>
+        The value of <structfield>reltuples</structfield> must not be less than
+        -1.0.
+       </para>
+       <para>
+        The value of <structfield>relallvisible</structfield> must not be less
+        than 0.
+       </para>
+       </entry>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_attribute_stats</primary>
+        </indexterm>
+        <function>pg_set_attribute_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>
+         , <parameter>attname</parameter> <type>name</type>
+         , <parameter>inherited</parameter> <type>boolean</type>
+         , <parameter>version</parameter> <type>integer</type>
+         , <parameter>null_frac</parameter> <type>real</type>
+         , <parameter>avg_width</parameter> <type>integer</type>
+         , <parameter>n_distinct</parameter> <type>real</type>
+         [, <parameter>most_common_vals</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_freqs</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>histogram_bounds</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>correlation</parameter> <type>real</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_elems</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_elem_freqs</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>elem_count_histogram</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_length_histogram</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_empty_frac</parameter> <type>real</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_bounds_histogram</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ] )
+        <returnvalue>void</returnvalue>
+       </para>
+       <para>
+        Replaces the <structname>pg_statistic</structname> row for the
+        <structname>pg_attribute</structname> row specified by
+        <parameter>relation</parameter>, <parameter>attname</parameter>
+        and <parameter>inherited</parameter>.
+       </para>
+       <para>
+        The value of <structfield>version</structfield> represents the
+        integer <term><varname>SERVER_VERSION_NUM</varname></term> of the
+        database that was the source of these values. A value of
+        <literal>NULL</literal> means to use the
+        <term><varname>SERVER_VERSION_NUM</varname></term> of the
+        current database. It should be noted that presently this value does
+        not alter the behavior of the function, but it could in future
+        versions.
+       </para>
+       <para>
+        The remaining parameters all correspond to attributes of the same name
+        found in <link linkend="view-pg-stats"><structname>pg_stats</structname></link>,
+        and the values supplied in the parameter must meet the requirements of
+        the corresponding attribute.
+       </para>
+       <para>
+        This function mimics the behavior of <command>ANALYZE</command> in its
+        effects on the values in <structname>pg_statistic</structname>, except
+        that the values are supplied as parameters rather than derived from
+        table sampling.
+       </para>
+       <para>
+        The purpose of this function is to apply statistics values in an
+        upgrade situation that are "good enough" for system operation until
+        they are replaced by the next <command>ANALYZE</command>, usually via
+        <command>autovacuum</command> This function is used by
+        <command>pg_upgrade</command> and <command>pg_restore</command> to
+        convey the statistics from the old system version into the new one.
+       </para>
+       <para>
+        The caller must either be the owner of the relation, or have superuser
+        privileges.
+       </para>
+       </entry>
+      </row>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_restore_relation_stats</primary>
+        </indexterm>
+        <function>pg_restore_relation_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>version</parameter> <type>integer</type>,
+         <literal>VARIADIC</literal> <parameter>stats</parameter> <type>"any"</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Updates the <structname>pg_class</structname> row for the specified
+        <parameter>relation</parameter>, values from the variadic list of
+        name-value pairs. Each parameter name corresponds to the same-named
+        attribute in <structname>pg_class</structname>:
+       </para>
+       <variablelist>
+        <varlistentry>
+         <term><literal>relpages</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>integer</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>reltuples</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>relalltuples</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>integer</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+       </variablelist>
+       <para>
+        Any other parameter names given must also have a value pair, but will emit
+        a warning and thereafter be ignored.
+       </para>
+       <para>
+        The <parameter>version</parameter> is meant to reflect the server version
+        number of the system where this data was generated, as that may in the
+        future change how the data is imported.
+       </para>
+       <para>
+        To avoid table bloat in <structname>pg_class</structname>, this change
+        is made with an in-place update, and therefore cannot be rolled back
+        through normal transaction processing.
+       </para>
+       <para>
+        This function mimics the behavior of <command>ANALYZE</command> in its
+        effects on the values in <structname>pg_class</structname>, except that
+        the values are supplied as parameters rather than derived from table
+        sampling.
+       </para>
+       <para>
+        The caller must either be the owner of the relation, or have superuser
+        privileges.
+       </para>
+       <para>
+        The parameters given are checked for type validity and must all be
+        NOT NULL.
+       </para>
+       <para>
+        The parameters will return true if stats were updated and false if not.
+       </para>
+       </entry>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_restore_attribute_stats</primary>
+        </indexterm>
+        <function>pg_restore_attribute_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>
+         , <parameter>attname</parameter> <type>name</type>
+         , <parameter>inherited</parameter> <type>boolean</type>
+         , <parameter>version</parameter> <type>integer</type>
+         , <literal>VARIADIC</literal> <parameter>stats</parameter> <type>"any"</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Replaces the <structname>pg_statistic</structname> row for the
+        <structname>pg_attribute</structname> row specified by
+        <parameter>relation</parameter>, <parameter>attname</parameter>
+        and <parameter>inherited</parameter>. The <parameter>version</parameter>
+        should be set to the server version number of the server where
+        the variadic <parameter>stats</parameter> originated, as it may in the
+        future affect how the stats are imported. Values from the variadic
+        list form name-value pairs. Each parameter name corresponds to the
+        same-named attribute in
+        <link linkend="view-pg-stats"><structname>pg_stats</structname></link>:
+       </para>
+       <variablelist>
+        <varlistentry>
+         <term><literal>null_frac</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>avg_width</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>integer</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>n_distinct</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_vals</literal></term>
+         <listitem>
+          <para>
+          <literal>most_common_vals</literal>
+           Must be followed by a parameter of type <type>text</type>. If this
+           parameter is specified then <literal>most_common_freqs</literal>
+           must also be specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_freqs</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of <type>real[]</type>, and can only
+           be specified if <literal>most_common_vals</literal> is also specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>histogram_bounds</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>correlation</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_elems</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>. If this
+           parameter is specified then <literal>most_common_elem_freqs</literal>
+           must also be specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_elem_freqs</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real[]</type>, and can
+           only be specified if <literal>most_common_elems</literal> is also
+           specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>elem_count_histogram</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real[]</type>.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>range_length_histogram</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>. If this
+           parameter is specified then <literal>range_empty_frac</literal>
+           must also be specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>range_empty_frac</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>, and can
+           only be specified if <literal>range_length_histogram</literal>
+           is also specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>range_bounds_histogram</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>
+          </para>
+         </listitem>
+        </varlistentry>
+       </variablelist>
+       <para>
+        Any other parameter names given must also have a value pair, but will emit
+        a warning and thereafter be ignored.
+       </para>
+       <para>
+        The parameters will return true if stats were updated and false if not.
+       </para>
+       <para>
+        This function mimics the behavior of <command>ANALYZE</command> in its
+        effects on the values in <structname>pg_statistic</structname>, except
+        that the values are supplied as parameters rather than derived from
+        table sampling.
+       </para>
+       <para>
+        The purpose of this function is to apply statistics values in an
+        upgrade situation that are "good enough" for system operation until
+        they are replaced by the next <command>ANALYZE</command>, usually via
+        <command>autovacuum</command>.
+       </para>
+       <para>
+        The caller must either be the owner of the relation, or have superuser
+        privileges.
+       </para>
+       </entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
    <table id="functions-admin-dblocation">
     <title>Database Object Location Functions</title>
     <tgroup cols="1">
-- 
2.45.2

v23-0002-Add-derivative-flags-dumpSchema-dumpData.patchtext/x-patch; charset=US-ASCII; name=v23-0002-Add-derivative-flags-dumpSchema-dumpData.patchDownload
From 41719115c6068e9fd3ca0ceb3b629482e2deb21e Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 4 May 2024 04:52:38 -0400
Subject: [PATCH v23 2/3] Add derivative flags dumpSchema, dumpData.

User-set flags --schema-only and --data-only are often consulted by
various operations to determine if they should be skipped or not. While
this logic works when there are only two mutually-exclusive -only
options, it will get progressively more confusing when more are added.

In anticipation of this, create the flags dumpSchema and dumpData which
are derivative of the existing options schemaOnly and dataOnly. This
allows us to restate current skip-this-section tests in terms of what is
enabled, rather than checking if the other -only mode is turned off.
---
 src/bin/pg_dump/pg_backup.h  |   8 ++
 src/bin/pg_dump/pg_dump.c    | 186 ++++++++++++++++++-----------------
 src/bin/pg_dump/pg_restore.c |   4 +
 3 files changed, 107 insertions(+), 91 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index fbf5f1c515..d942a6a256 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -157,6 +157,10 @@ typedef struct _restoreOptions
 	int			enable_row_security;
 	int			sequence_data;	/* dump sequence data even in schema-only mode */
 	int			binary_upgrade;
+
+	/* flags derived entirely from the user-settable flags */
+	bool		dumpSchema;
+	bool		dumpData;
 } RestoreOptions;
 
 typedef struct _dumpOptions
@@ -203,6 +207,10 @@ typedef struct _dumpOptions
 
 	int			sequence_data;	/* dump sequence data even in schema-only mode */
 	int			do_nothing;
+
+	/* flags derived entirely from the user-settable flags */
+	bool		dumpSchema;
+	bool		dumpData;
 } DumpOptions;
 
 /*
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index b8b1888bd3..5f7cd2b29e 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -755,6 +755,10 @@ main(int argc, char **argv)
 	if (dopt.if_exists && !dopt.outputClean)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
+	/* set derivative flags */
+	dopt.dumpSchema = (!dopt.dataOnly);
+	dopt.dumpData = (!dopt.schemaOnly);
+
 	/*
 	 * --inserts are already implied above if --column-inserts or
 	 * --rows-per-insert were specified.
@@ -937,7 +941,7 @@ main(int argc, char **argv)
 	 * -s means "schema only" and LOs are data, not schema, so we never
 	 * include LOs when -s is used.
 	 */
-	if (dopt.include_everything && !dopt.schemaOnly && !dopt.dontOutputLOs)
+	if (dopt.include_everything && dopt.dumpData && !dopt.dontOutputLOs)
 		dopt.outputLOs = true;
 
 	/*
@@ -951,15 +955,15 @@ main(int argc, char **argv)
 	 */
 	tblinfo = getSchemaData(fout, &numTables);
 
-	if (!dopt.schemaOnly)
+	if (dopt.dumpData)
 	{
 		getTableData(&dopt, tblinfo, numTables, 0);
 		buildMatViewRefreshDependencies(fout);
-		if (dopt.dataOnly)
+		if (!dopt.dumpSchema)
 			getTableDataFKConstraints();
 	}
 
-	if (dopt.schemaOnly && dopt.sequence_data)
+	if (!dopt.dumpData && dopt.sequence_data)
 		getTableData(&dopt, tblinfo, numTables, RELKIND_SEQUENCE);
 
 	/*
@@ -4094,8 +4098,8 @@ dumpPolicy(Archive *fout, const PolicyInfo *polinfo)
 	const char *cmd;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
@@ -4307,8 +4311,8 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 	char	   *qpubname;
 	bool		first = true;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	delq = createPQExpBuffer();
@@ -4622,8 +4626,8 @@ dumpPublicationNamespace(Archive *fout, const PublicationSchemaInfo *pubsinfo)
 	PQExpBuffer query;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	tag = psprintf("%s %s", pubinfo->dobj.name, schemainfo->dobj.name);
@@ -4665,8 +4669,8 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 	PQExpBuffer query;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	tag = psprintf("%s %s", pubinfo->dobj.name, tbinfo->dobj.name);
@@ -5029,8 +5033,8 @@ dumpSubscriptionTable(Archive *fout, const SubRelInfo *subrinfo)
 	PQExpBuffer query;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	Assert(fout->dopt->binary_upgrade && fout->remoteVersion >= 170000);
@@ -5103,8 +5107,8 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo)
 	int			i;
 	char		two_phase_disabled[] = {LOGICALREP_TWOPHASE_STATE_DISABLED, '\0'};
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	delq = createPQExpBuffer();
@@ -7251,8 +7255,8 @@ getPartitioningInfo(Archive *fout)
 	/* hash partitioning didn't exist before v11 */
 	if (fout->remoteVersion < 110000)
 		return;
-	/* needn't bother if schema-only dump */
-	if (fout->dopt->schemaOnly)
+	/* needn't bother if not dumping data */
+	if (!fout->dopt->dumpData)
 		return;
 
 	query = createPQExpBuffer();
@@ -8894,7 +8898,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * Now get info about column defaults.  This is skipped for a data-only
 	 * dump, as it is only needed for table schemas.
 	 */
-	if (!dopt->dataOnly && tbloids->len > 1)
+	if (dopt->dumpSchema && tbloids->len > 1)
 	{
 		AttrDefInfo *attrdefs;
 		int			numDefaults;
@@ -9024,7 +9028,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * Get info about table CHECK constraints.  This is skipped for a
 	 * data-only dump, as it is only needed for table schemas.
 	 */
-	if (!dopt->dataOnly && checkoids->len > 2)
+	if (dopt->dumpSchema && checkoids->len > 2)
 	{
 		ConstraintInfo *constrs;
 		int			numConstrs;
@@ -9934,13 +9938,13 @@ dumpCommentExtended(Archive *fout, const char *type,
 	/* Comments are schema not data ... except LO comments are data */
 	if (strcmp(type, "LARGE OBJECT") != 0)
 	{
-		if (dopt->dataOnly)
+		if (!dopt->dumpSchema)
 			return;
 	}
 	else
 	{
 		/* We do dump LO comments in binary-upgrade mode */
-		if (dopt->schemaOnly && !dopt->binary_upgrade)
+		if (!dopt->dumpData && !dopt->binary_upgrade)
 			return;
 	}
 
@@ -10047,7 +10051,7 @@ dumpTableComment(Archive *fout, const TableInfo *tbinfo,
 		return;
 
 	/* Comments are SCHEMA not data */
-	if (dopt->dataOnly)
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Search for comments associated with relation, using table */
@@ -10493,8 +10497,8 @@ dumpNamespace(Archive *fout, const NamespaceInfo *nspinfo)
 	PQExpBuffer delq;
 	char	   *qnspname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -10570,8 +10574,8 @@ dumpExtension(Archive *fout, const ExtensionInfo *extinfo)
 	PQExpBuffer delq;
 	char	   *qextname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -10695,8 +10699,8 @@ dumpType(Archive *fout, const TypeInfo *tyinfo)
 {
 	DumpOptions *dopt = fout->dopt;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Dump out in proper style */
@@ -11806,8 +11810,8 @@ dumpShellType(Archive *fout, const ShellTypeInfo *stinfo)
 	DumpOptions *dopt = fout->dopt;
 	PQExpBuffer q;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -11858,8 +11862,8 @@ dumpProcLang(Archive *fout, const ProcLangInfo *plang)
 	FuncInfo   *inlineInfo = NULL;
 	FuncInfo   *validatorInfo = NULL;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
@@ -12066,8 +12070,8 @@ dumpFunc(Archive *fout, const FuncInfo *finfo)
 	int			nconfigitems = 0;
 	const char *keyword;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -12458,8 +12462,8 @@ dumpCast(Archive *fout, const CastInfo *cast)
 	const char *sourceType;
 	const char *targetType;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Cannot dump if we don't have the cast function's info */
@@ -12564,8 +12568,8 @@ dumpTransform(Archive *fout, const TransformInfo *transform)
 	char	   *lanname;
 	const char *transformType;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Cannot dump if we don't have the transform functions' info */
@@ -12713,8 +12717,8 @@ dumpOpr(Archive *fout, const OprInfo *oprinfo)
 	char	   *oprregproc;
 	char	   *oprref;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
@@ -13000,8 +13004,8 @@ dumpAccessMethod(Archive *fout, const AccessMethodInfo *aminfo)
 	PQExpBuffer delq;
 	char	   *qamname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -13103,8 +13107,8 @@ dumpOpclass(Archive *fout, const OpclassInfo *opcinfo)
 	bool		needComma;
 	int			i;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -13374,8 +13378,8 @@ dumpOpfamily(Archive *fout, const OpfamilyInfo *opfinfo)
 	bool		needComma;
 	int			i;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -13581,8 +13585,8 @@ dumpCollation(Archive *fout, const CollInfo *collinfo)
 	const char *colllocale;
 	const char *collicurules;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -13835,8 +13839,8 @@ dumpConversion(Archive *fout, const ConvInfo *convinfo)
 	const char *conproc;
 	bool		condefault;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -13983,8 +13987,8 @@ dumpAgg(Archive *fout, const AggInfo *agginfo)
 	const char *proparallel;
 	char		defaultfinalmodify;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -14313,8 +14317,8 @@ dumpTSParser(Archive *fout, const TSParserInfo *prsinfo)
 	PQExpBuffer delq;
 	char	   *qprsname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14381,8 +14385,8 @@ dumpTSDictionary(Archive *fout, const TSDictInfo *dictinfo)
 	char	   *nspname;
 	char	   *tmplname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14457,8 +14461,8 @@ dumpTSTemplate(Archive *fout, const TSTemplateInfo *tmplinfo)
 	PQExpBuffer delq;
 	char	   *qtmplname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14523,8 +14527,8 @@ dumpTSConfig(Archive *fout, const TSConfigInfo *cfginfo)
 	int			i_tokenname;
 	int			i_dictname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14635,8 +14639,8 @@ dumpForeignDataWrapper(Archive *fout, const FdwInfo *fdwinfo)
 	PQExpBuffer delq;
 	char	   *qfdwname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14708,8 +14712,8 @@ dumpForeignServer(Archive *fout, const ForeignServerInfo *srvinfo)
 	char	   *qsrvname;
 	char	   *fdwname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14899,8 +14903,8 @@ dumpDefaultACL(Archive *fout, const DefaultACLInfo *daclinfo)
 	PQExpBuffer tag;
 	const char *type;
 
-	/* Do nothing in data-only dump, or if we're skipping ACLs */
-	if (dopt->dataOnly || dopt->aclsSkip)
+	/* Do nothing if not dumping schema, or if we're skipping ACLs */
+	if (!dopt->dumpSchema || dopt->aclsSkip)
 		return;
 
 	q = createPQExpBuffer();
@@ -15000,7 +15004,7 @@ dumpACL(Archive *fout, DumpId objDumpId, DumpId altDumpId,
 		return InvalidDumpId;
 
 	/* --data-only skips ACLs *except* large object ACLs */
-	if (dopt->dataOnly && strcmp(type, "LARGE OBJECT") != 0)
+	if (!dopt->dumpSchema && strcmp(type, "LARGE OBJECT") != 0)
 		return InvalidDumpId;
 
 	sql = createPQExpBuffer();
@@ -15129,13 +15133,13 @@ dumpSecLabel(Archive *fout, const char *type, const char *name,
 	 */
 	if (strcmp(type, "LARGE OBJECT") != 0)
 	{
-		if (dopt->dataOnly)
+		if (!dopt->dumpSchema)
 			return;
 	}
 	else
 	{
 		/* We do dump large object security labels in binary-upgrade mode */
-		if (dopt->schemaOnly && !dopt->binary_upgrade)
+		if (!dopt->dumpData && !dopt->binary_upgrade)
 			return;
 	}
 
@@ -15203,7 +15207,7 @@ dumpTableSecLabel(Archive *fout, const TableInfo *tbinfo, const char *reltypenam
 		return;
 
 	/* SecLabel are SCHEMA not data */
-	if (dopt->dataOnly)
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Search for comments associated with relation, using table */
@@ -15442,8 +15446,8 @@ dumpTable(Archive *fout, const TableInfo *tbinfo)
 	DumpId		tableAclDumpId = InvalidDumpId;
 	char	   *namecopy;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	if (tbinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
@@ -16472,8 +16476,8 @@ dumpTableAttach(Archive *fout, const TableAttachInfo *attachinfo)
 	PGresult   *res;
 	char	   *partbound;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -16544,8 +16548,8 @@ dumpAttrDef(Archive *fout, const AttrDefInfo *adinfo)
 	char	   *tag;
 	char	   *foreign;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Skip if not "separate"; it was dumped in the table's definition */
@@ -16633,8 +16637,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 	char	   *qindxname;
 	char	   *qqindxname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -16766,8 +16770,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 static void
 dumpIndexAttach(Archive *fout, const IndexAttachInfo *attachinfo)
 {
-	/* Do nothing in data-only dump */
-	if (fout->dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!fout->dopt->dumpSchema)
 		return;
 
 	if (attachinfo->partitionIdx->dobj.dump & DUMP_COMPONENT_DEFINITION)
@@ -16813,8 +16817,8 @@ dumpStatisticsExt(Archive *fout, const StatsExtInfo *statsextinfo)
 	PGresult   *res;
 	char	   *stxdef;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -16889,8 +16893,8 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
 	char	   *tag = NULL;
 	char	   *foreign;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -17521,8 +17525,8 @@ dumpTrigger(Archive *fout, const TriggerInfo *tginfo)
 	char	   *qtabname;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -17643,8 +17647,8 @@ dumpEventTrigger(Archive *fout, const EventTriggerInfo *evtinfo)
 	PQExpBuffer delqry;
 	char	   *qevtname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -17734,8 +17738,8 @@ dumpRule(Archive *fout, const RuleInfo *rinfo)
 	PGresult   *res;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index df119591cc..3475168a64 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -360,6 +360,10 @@ main(int argc, char **argv)
 	if (opts->single_txn && numWorkers > 1)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
+	/* set derivative flags */
+	opts->dumpSchema = (opts->dataOnly != 1);
+	opts->dumpData = (opts->schemaOnly != 1);
+
 	opts->disable_triggers = disable_triggers;
 	opts->enable_row_security = enable_row_security;
 	opts->noDataForFailedTables = no_data_for_failed_tables;
-- 
2.45.2

v23-0003-Enable-dumping-of-table-index-stats-in-pg_dump.patchtext/x-patch; charset=US-ASCII; name=v23-0003-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From 9187d9135905038697f55ffd98634e340c87673c Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH v23 3/3] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will generate a statement that
calls pg_set_relation_stats(), and it will generate a series of
statements that call pg_set_attribute_stats(), one per attribute. These
statements will restore the statistics of the current system onto the
destination system.

Adds the command-line options -X / --statistics-only, which are mutually
exclusive to --schema-only and --data-only.

Statistics are not dumped when --schema-only is specified, except during
a binary upgrade.

As is the pattern with pg_dump options, staistics can be disabled using
--no-statistics.

Table statistics are dumped in the data section. This is true even if
dumping stats in a binary upgrade. Index and Materialized View
statistics are dumped in the post-data section.
---
 src/bin/pg_dump/pg_backup.h          |   6 +
 src/bin/pg_dump/pg_backup_archiver.c |   5 +
 src/bin/pg_dump/pg_dump.c            | 397 ++++++++++++++++++++++++++-
 src/bin/pg_dump/pg_dump.h            |   8 +
 src/bin/pg_dump/pg_dump_sort.c       |   5 +
 src/bin/pg_dump/pg_dumpall.c         |   5 +
 src/bin/pg_dump/pg_restore.c         |   8 +
 src/bin/pg_dump/t/001_basic.pl       |  10 +-
 doc/src/sgml/ref/pg_dump.sgml        |  36 ++-
 doc/src/sgml/ref/pg_restore.sgml     |  31 ++-
 10 files changed, 493 insertions(+), 18 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index d942a6a256..25d386f5bb 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -112,11 +112,13 @@ typedef struct _restoreOptions
 	int			no_publications;	/* Skip publication entries */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
 	int			dataOnly;
 	int			schemaOnly;
+	int			statisticsOnly;
 	int			dumpSections;
 	int			verbose;
 	int			aclsSkip;
@@ -161,6 +163,7 @@ typedef struct _restoreOptions
 	/* flags derived entirely from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } RestoreOptions;
 
 typedef struct _dumpOptions
@@ -172,6 +175,7 @@ typedef struct _dumpOptions
 	/* various user-settable parameters */
 	bool		schemaOnly;
 	bool		dataOnly;
+	bool		statisticsOnly;
 	int			dumpSections;	/* bitmask of chosen sections */
 	bool		aclsSkip;
 	const char *lockWaitTimeout;
@@ -185,6 +189,7 @@ typedef struct _dumpOptions
 	int			no_security_labels;
 	int			no_publications;
 	int			no_subscriptions;
+	int			no_statistics;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
@@ -211,6 +216,7 @@ typedef struct _dumpOptions
 	/* flags derived entirely from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } DumpOptions;
 
 /*
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 68e321212d..9bfa2f1326 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2958,6 +2958,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's a stats dump, maybe ignore it */
+	if (ropt->no_statistics && strcmp(te->desc, "STATISTICS") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2987,6 +2991,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 5f7cd2b29e..bc94b1b4ff 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -424,6 +424,7 @@ main(int argc, char **argv)
 		{"encoding", required_argument, NULL, 'E'},
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
+		{"statistics-only", no_argument, NULL, 'X'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -452,6 +453,7 @@ main(int argc, char **argv)
 		{"no-comments", no_argument, &dopt.no_comments, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -497,7 +499,7 @@ main(int argc, char **argv)
 
 	InitDumpOptions(&dopt);
 
-	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:RsS:t:T:U:vwWxZ:",
+	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:PRsS:t:T:U:vwWxXZ:",
 							long_options, &optindex)) != -1)
 	{
 		switch (c)
@@ -571,6 +573,10 @@ main(int argc, char **argv)
 				dopt.cparams.pgport = pg_strdup(optarg);
 				break;
 
+			case 'X':			/* Dump statistics only */
+				dopt.statisticsOnly = true;
+				break;
+
 			case 'R':
 				/* no-op, still accepted for backwards compatibility */
 				break;
@@ -740,8 +746,11 @@ main(int argc, char **argv)
 	if (dopt.binary_upgrade)
 		dopt.sequence_data = 1;
 
-	if (dopt.dataOnly && dopt.schemaOnly)
-		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (((int)dopt.dataOnly + (int) dopt.schemaOnly + (int) dopt.statisticsOnly) > 1)
+		pg_fatal("options -s/--schema-only, -a/--data-only, and -X/--statistics-only cannot be used together");
+
+	if (dopt.statisticsOnly && dopt.no_statistics)
+		pg_fatal("options -X/--statistics-only and --no-statistics cannot be used together");
 
 	if (dopt.schemaOnly && foreign_servers_include_patterns.head != NULL)
 		pg_fatal("options -s/--schema-only and --include-foreign-data cannot be used together");
@@ -756,8 +765,23 @@ main(int argc, char **argv)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
 	/* set derivative flags */
-	dopt.dumpSchema = (!dopt.dataOnly);
-	dopt.dumpData = (!dopt.schemaOnly);
+	dopt.dumpSchema = dopt.schemaOnly ||
+		!(dopt.dataOnly || dopt.statisticsOnly);
+
+	dopt.dumpData = dopt.dataOnly ||
+		!(dopt.schemaOnly || dopt.statisticsOnly);
+
+	if (dopt.statisticsOnly)
+		/* stats are only thing wanted */
+		dopt.dumpStatistics = true;
+	else if (dopt.no_statistics)
+		/* stats specifically excluded */
+		dopt.dumpStatistics = false;
+	else if (dopt.binary_upgrade)
+		/* binary upgrade and not specifically excluded */
+		dopt.dumpStatistics = true;
+	else
+		dopt.dumpStatistics = !(dopt.schemaOnly || dopt.dataOnly);
 
 	/*
 	 * --inserts are already implied above if --column-inserts or
@@ -1054,6 +1078,7 @@ main(int argc, char **argv)
 	ropt->dropSchema = dopt.outputClean;
 	ropt->dataOnly = dopt.dataOnly;
 	ropt->schemaOnly = dopt.schemaOnly;
+	ropt->statisticsOnly = dopt.statisticsOnly;
 	ropt->if_exists = dopt.if_exists;
 	ropt->column_inserts = dopt.column_inserts;
 	ropt->dumpSections = dopt.dumpSections;
@@ -1132,7 +1157,7 @@ help(const char *progname)
 	printf(_("  -?, --help                   show this help, then exit\n"));
 
 	printf(_("\nOptions controlling the output content:\n"));
-	printf(_("  -a, --data-only              dump only the data, not the schema\n"));
+	printf(_("  -a, --data-only              dump only the data, not the schema or statistics\n"));
 	printf(_("  -b, --large-objects          include large objects in dump\n"));
 	printf(_("  --blobs                      (same as --large-objects, deprecated)\n"));
 	printf(_("  -B, --no-large-objects       exclude large objects in dump\n"));
@@ -1145,11 +1170,12 @@ help(const char *progname)
 	printf(_("  -N, --exclude-schema=PATTERN do NOT dump the specified schema(s)\n"));
 	printf(_("  -O, --no-owner               skip restoration of object ownership in\n"
 			 "                               plain-text format\n"));
-	printf(_("  -s, --schema-only            dump only the schema, no data\n"));
+	printf(_("  -s, --schema-only            dump only the schema, no data or statistics\n"));
 	printf(_("  -S, --superuser=NAME         superuser user name to use in plain-text format\n"));
 	printf(_("  -t, --table=PATTERN          dump only the specified table(s)\n"));
 	printf(_("  -T, --exclude-table=PATTERN  do NOT dump the specified table(s)\n"));
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
+	printf(_("  -X, --statistics-only        dump only the statistics, not schema or data\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
@@ -1176,6 +1202,7 @@ help(const char *progname)
 	printf(_("  --no-comments                do not dump comments\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -6673,6 +6700,42 @@ getFuncs(Archive *fout)
 	destroyPQExpBuffer(query);
 }
 
+/*
+ * getRelationStatistics
+ *    register the statistics object as a dependent of the relation.
+ *
+ */
+static RelStatsInfo *
+getRelationStatistics(Archive *fout, DumpableObject *rel, char relkind)
+{
+	if ((relkind == RELKIND_RELATION) ||
+		(relkind == RELKIND_PARTITIONED_TABLE) ||
+		(relkind == RELKIND_INDEX) ||
+		(relkind == RELKIND_PARTITIONED_INDEX) ||
+		(relkind == RELKIND_MATVIEW))
+	{
+		RelStatsInfo	   *info = pg_malloc0(sizeof(RelStatsInfo));
+		DumpableObject	   *dobj = &info->dobj;
+
+		dobj->objType = DO_REL_STATS;
+		dobj->catId.tableoid = 0;
+		dobj->catId.oid = 0;
+		AssignDumpId(dobj);
+		dobj->dependencies = (DumpId *) pg_malloc(sizeof(DumpId));
+		dobj->dependencies[0] = rel->dumpId;
+		dobj->nDeps = 1;
+		dobj->allocDeps = 1;
+		dobj->components |= DUMP_COMPONENT_STATISTICS;
+		dobj->dump = rel->dump;
+		dobj->name = pg_strdup(rel->name);
+		dobj->namespace = rel->namespace;
+		info->relkind = relkind;
+
+		return info;
+	}
+	return NULL;
+}
+
 /*
  * getTables
  *	  read all the tables (no indexes) in the system catalogs,
@@ -7050,6 +7113,7 @@ getTables(Archive *fout, int *numTables)
 
 		/* Tables have data */
 		tblinfo[i].dobj.components |= DUMP_COMPONENT_DATA;
+		tblinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
 
 		/* Mark whether table has an ACL */
 		if (!PQgetisnull(res, i, i_relacl))
@@ -7098,6 +7162,8 @@ getTables(Archive *fout, int *numTables)
 				}
 			}
 		}
+		if (tblinfo[i].interesting)
+			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relkind);
 	}
 
 	if (query->len != 0)
@@ -7533,11 +7599,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 		for (int c = 0; c < numinds; c++, j++)
 		{
 			char		contype;
+			char		indexkind;
+			RelStatsInfo   *relstats;
 
 			indxinfo[j].dobj.objType = DO_INDEX;
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -7560,7 +7629,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			{
 				NULL, NULL
 			};
+
+			if (indxinfo[j].parentidx == 0)
+				indexkind = RELKIND_INDEX;
+			else
+				indexkind = RELKIND_PARTITIONED_INDEX;
+
 			contype = *(PQgetvalue(res, j, i_contype));
+			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, indexkind);
 
 			if (contype == 'p' || contype == 'u' || contype == 'x')
 			{
@@ -7593,6 +7669,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				constrinfo->separate = true;
 
 				indxinfo[j].indexconstraint = constrinfo->dobj.dumpId;
+				if (relstats != NULL)
+					addObjectDependency(&relstats->dobj, constrinfo->dobj.dumpId);
 			}
 			else
 			{
@@ -10030,6 +10108,296 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * Tabular description of the parameters to pg_restore_relation_stats()
+ * required, param_name, param_type
+ */
+static const char *rel_stats_arginfo[][3] = {
+	{"t", "relation", "regclass"},
+	{"t", "version", "integer"},
+	{"f", "relpages", "integer"},
+	{"f", "reltuples", "real"},
+	{"f", "relallvisible", "integer"},
+};
+
+/*
+ * Tabular description of the parameters to pg_restore_attribute_stats()
+ * required, param_name, param_type
+ */
+static const char *att_stats_arginfo[][3] = {
+	{"t", "relation", "regclass"},
+	{"t", "attname", "name"},
+	{"t", "inherited", "boolean"},
+	{"t", "version", "integer"},
+	{"f", "null_frac", "float4"},
+	{"f", "avg_width", "integer"},
+	{"f", "n_distinct", "float4"},
+	{"f", "most_common_vals", "text"},
+	{"f", "most_common_freqs", "float4[]"},
+	{"f", "histogram_bounds", "text"},
+	{"f", "correlation", "float4"},
+	{"f", "most_common_elems", "text"},
+	{"f", "most_common_elem_freqs", "float4[]"},
+	{"f", "elem_count_histogram", "float4[]"},
+	{"f", "range_length_histogram", "text"},
+	{"f", "range_empty_frac", "float4"},
+	{"f", "range_bounds_histogram", "text"},
+};
+
+/*
+ * getRelStatsExportQuery --
+ *
+ * Generate a query that will fetch all relation (e.g. pg_class)
+ * stats for a given relation.
+ */
+static void
+getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "current_setting('server_version_num') AS version, "
+						 "c.relpages, c.reltuples, c.relallvisible "
+						 "FROM pg_class c "
+						 "JOIN pg_namespace n "
+						 "ON n.oid = c.relnamespace "
+						 "WHERE n.nspname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND c.relname = ");
+	appendStringLiteralAH(query, relname, fout);
+}
+
+/*
+ * getAttStatsExportQuery --
+ *
+ * Generate a query that will fetch all attribute (e.g. pg_statistic)
+ * stats for a given relation.
+ */
+static void
+getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "s.attname,"
+						 "s.inherited,"
+						 "current_setting('server_version_num') AS version, "
+						 "s.null_frac,"
+						 "s.avg_width,"
+						 "s.n_distinct,"
+						 "s.most_common_vals,"
+						 "s.most_common_freqs,"
+						 "s.histogram_bounds,"
+						 "s.correlation,"
+						 "s.most_common_elems,"
+						 "s.most_common_elem_freqs,"
+						 "s.elem_count_histogram,");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 "s.range_length_histogram,"
+							 "s.range_empty_frac,"
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(query,
+							 "NULL AS range_length_histogram,"
+							 "NULL AS range_empty_frac,"
+							 "NULL AS range_bounds_histogram ");
+
+	appendPQExpBufferStr(query,
+						 "FROM pg_stats s "
+						 "JOIN pg_namespace n "
+						 "ON n.nspname = s.schemaname "
+						 "JOIN pg_class c "
+						 "ON c.relname = s.tablename "
+						 "AND c.relnamespace = n.oid "
+						 "WHERE s.schemaname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND s.tablename = ");
+	appendStringLiteralAH(query, relname, fout);
+	appendPQExpBufferStr(query, " ORDER BY s.attname, s.inherited");
+}
+
+
+/*
+ * appendNamedArgument --
+ *
+ * Convenience routine for constructing parameters of the form:
+ * 'paraname', 'value'::type
+ */
+static void
+appendNamedArgument(PQExpBuffer out, Archive *fout,
+					const char *argname, bool positional,
+					const char *argval, const char *argtype)
+{
+	appendPQExpBufferStr(out, "\t");
+
+	if (!positional)
+	{
+		appendStringLiteralAH(out, argname, fout);
+		appendPQExpBufferStr(out, ", ");
+	}
+
+	appendStringLiteralAH(out, argval, fout);
+	appendPQExpBuffer(out, "::%s", argtype);
+}
+
+/*
+ * appendRelStatsImport --
+ *
+ * Append a formatted pg_restore_relation_stats statement.
+ */
+static void
+appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	const char *sep = "";
+
+	if (PQntuples(res) == 0)
+		return;
+
+	appendPQExpBufferStr(out, "SELECT pg_catalog.pg_restore_relation_stats(\n");
+
+	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
+	{
+		bool		positional = (rel_stats_arginfo[argno][0][0] == 't');
+		const char *argname = rel_stats_arginfo[argno][1];
+		const char *argtype = rel_stats_arginfo[argno][2];
+		int			fieldno = PQfnumber(res, argname);
+
+		if (fieldno < 0)
+			pg_fatal("relation stats export query missing field '%s'",
+					 argname);
+
+		if (PQgetisnull(res, 0, fieldno))
+		{
+			if (!positional)
+				pg_fatal("relation stats export query unexpected NULL in '%s'",
+						 argname);
+			else
+				continue;
+		}
+
+		appendPQExpBufferStr(out, sep);
+		appendNamedArgument(out, fout, argname, positional,
+							PQgetvalue(res, 0, fieldno), argtype);
+
+		sep = ",\n";
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+/*
+ * appendAttStatsImport --
+ *
+ * Append a series of formatted pg_restore_attribute_stats statements.
+ */
+static void
+appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	{
+		const char *sep = "";
+
+		appendPQExpBufferStr(out, "SELECT pg_catalog.pg_restore_attribute_stats(\n");
+		for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
+		{
+			bool		positional = (att_stats_arginfo[argno][0][0] == 't');
+			const char *argname = att_stats_arginfo[argno][1];
+			const char *argtype = att_stats_arginfo[argno][2];
+			int			fieldno = PQfnumber(res, argname);
+
+			if (fieldno < 0)
+				pg_fatal("attribute stats export query missing field '%s'",
+						 argname);
+
+			if (PQgetisnull(res, rownum, fieldno))
+			{
+				if (positional)
+					pg_fatal("attribute stats export query unexpected NULL in '%s'",
+							 argname);
+				else
+					continue;
+			}
+
+			appendPQExpBufferStr(out, sep);
+			appendNamedArgument(out, fout, argname, positional,
+								PQgetvalue(res, rownum, fieldno), argtype);
+			sep = ",\n";
+		}
+		appendPQExpBufferStr(out, "\n);\n");
+	}
+}
+
+/*
+ * get statistics dump section, which depends on the parent object type
+ *
+ * objects created in SECTION_PRE_DATA have stats in SECTION_DATA
+ * objects created in SECTION_POST_DATA have stats in SECTION_POST_DATA
+ */
+static teSection
+statisticsDumpSection(const RelStatsInfo *rsinfo)
+{
+
+	if ((rsinfo->relkind == RELKIND_MATVIEW) ||
+		(rsinfo->relkind == RELKIND_INDEX) ||
+		(rsinfo->relkind == RELKIND_PARTITIONED_INDEX))
+			return SECTION_POST_DATA;
+
+	return SECTION_DATA;
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	PGresult	   *res;
+	PQExpBuffer		query;
+	PQExpBuffer		out;
+	PQExpBuffer		tag;
+	DumpableObject *dobj = (DumpableObject *) &rsinfo->dobj;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", "STATISTICS DATA", fmtId(dobj->name));
+
+	query = createPQExpBuffer();
+	out = createPQExpBuffer();
+
+	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendRelStatsImport(out, fout, res);
+	PQclear(res);
+
+	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendAttStatsImport(out, fout, res);
+	PQclear(res);
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATISTICS DATA",
+							  .section = statisticsDumpSection(rsinfo),
+							  .createStmt = out->data,
+							  .deps = dobj->dependencies,
+							  .nDeps = dobj->nDeps));
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(out);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -10478,6 +10846,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_SUBSCRIPTION_REL:
 			dumpSubscriptionTable(fout, (const SubRelInfo *) dobj);
 			break;
+		case DO_REL_STATS:
+			dumpRelationStats(fout, (const RelStatsInfo *) dobj);
+			break;
 		case DO_PRE_DATA_BOUNDARY:
 		case DO_POST_DATA_BOUNDARY:
 			/* never dumped, nothing to do */
@@ -16748,6 +17119,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 		free(indstatvalsarray);
 	}
 
+	/* Comments and stats share same .dep */
+
 	/* Dump Index Comments */
 	if (indxinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
 		dumpComment(fout, "INDEX", qindxname,
@@ -18430,6 +18803,16 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 				/* must come after the pre-data boundary */
 				addObjectDependency(dobj, preDataBound->dumpId);
 				break;
+			case DO_REL_STATS:
+				/* stats section varies by parent object type, DATA or POST */
+				if (statisticsDumpSection((RelStatsInfo *) dobj) == SECTION_DATA)
+				{
+					addObjectDependency(dobj, preDataBound->dumpId);
+					addObjectDependency(postDataBound, dobj->dumpId);
+				}
+				else
+					addObjectDependency(dobj, postDataBound->dumpId);
+				break;
 		}
 	}
 }
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 4b2e5870a9..602f3e4417 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -84,6 +84,7 @@ typedef enum
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION,
 	DO_SUBSCRIPTION_REL,		/* see note for SubRelInfo */
+	DO_REL_STATS,
 } DumpableObjectType;
 
 /*
@@ -101,6 +102,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
@@ -416,6 +418,12 @@ typedef struct _indexAttachInfo
 	IndxInfo   *partitionIdx;	/* link to index on partition */
 } IndexAttachInfo;
 
+typedef struct _relStatsInfo
+{
+	DumpableObject	dobj;
+	char			relkind;		/* 'r', 'v', 'c', etc */
+} RelStatsInfo;
+
 typedef struct _statsExtInfo
 {
 	DumpableObject dobj;
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 4cb754caa5..cfe277f5ce 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -1490,6 +1490,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "POST-DATA BOUNDARY  (ID %d)",
 					 obj->dumpId);
 			return;
+		case DO_REL_STATS:
+			snprintf(buf, bufsize,
+					 "RELATION STATISTICS FOR %s  (ID %d OID %u)",
+					 obj->name, obj->dumpId, obj->catId.oid);
+			return;
 	}
 	/* shouldn't get here */
 	snprintf(buf, bufsize,
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 882dbf8e86..aea4aeb189 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -103,6 +103,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -172,6 +173,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -451,6 +453,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -666,6 +670,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 3475168a64..e285b2828f 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -75,6 +75,7 @@ main(int argc, char **argv)
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
 	static int	no_subscriptions = 0;
+	static int  no_statistics = 0;
 	static int	strict_names = 0;
 
 	struct option cmdopts[] = {
@@ -107,6 +108,7 @@ main(int argc, char **argv)
 		{"username", 1, NULL, 'U'},
 		{"verbose", 0, NULL, 'v'},
 		{"single-transaction", 0, NULL, '1'},
+		{"statistics-only", no_argument, NULL, 'P'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -127,6 +129,7 @@ main(int argc, char **argv)
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
+		{"no-statistics", no_argument, &no_statistics, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -270,6 +273,10 @@ main(int argc, char **argv)
 				opts->aclsSkip = 1;
 				break;
 
+			case 'X':			/* Restore statistics only */
+				opts->statisticsOnly = 1;
+				break;
+
 			case '1':			/* Restore data in a single transaction */
 				opts->single_txn = true;
 				opts->exit_on_error = true;
@@ -374,6 +381,7 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
diff --git a/src/bin/pg_dump/t/001_basic.pl b/src/bin/pg_dump/t/001_basic.pl
index b9d13a0e1d..b1c7a10919 100644
--- a/src/bin/pg_dump/t/001_basic.pl
+++ b/src/bin/pg_dump/t/001_basic.pl
@@ -46,8 +46,8 @@ command_fails_like(
 
 command_fails_like(
 	[ 'pg_dump', '-s', '-a' ],
-	qr/\Qpg_dump: error: options -s\/--schema-only and -a\/--data-only cannot be used together\E/,
-	'pg_dump: options -s/--schema-only and -a/--data-only cannot be used together'
+	qr/\Qpg_dump: error: options -s\/--schema-only, -a\/--data-only, and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: options -s/--schema-only, -a/--data-only, and -X/--statistics-only cannot be used together'
 );
 
 command_fails_like(
@@ -56,6 +56,12 @@ command_fails_like(
 	'pg_dump: options -s/--schema-only and --include-foreign-data cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '--statistics-only', '--no-statistics' ],
+	qr/\Qpg_dump: error: options -X\/--statistics-only and --no-statistics cannot be used together\E/,
+	'pg_dump: options -X\/--statistics-only and --no-statistics cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-j2', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: option --include-foreign-data is not supported with parallel backup\E/,
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index b95ed87517..aee50b37a1 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -119,7 +119,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Dump only the data, not the schema (data definitions).
+        Dump only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are dumped.
        </para>
 
@@ -137,8 +137,9 @@ PostgreSQL documentation
       <listitem>
        <para>
         Include large objects in the dump.  This is the default behavior
-        except when <option>--schema</option>, <option>--table</option>, or
-        <option>--schema-only</option> is specified.  The <option>-b</option>
+        except when <option>--schema</option>, <option>--table</option>,
+        <option>--schema-only</option>, or
+        <option>--statistics-only</option> is specified.  The <option>-b</option>
         switch is therefore only useful to add large objects to dumps
         where a specific schema or table has been requested.  Note that
         large objects are considered data and therefore will be included when
@@ -512,10 +513,11 @@ PostgreSQL documentation
       <term><option>--schema-only</option></term>
       <listitem>
        <para>
-        Dump only the object definitions (schema), not data.
+        Dump only the object definitions (schema), not data or statistics.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive to <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
         <option>--section=pre-data --section=post-data</option>.
@@ -648,6 +650,18 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Dump only the statistics, not the schema (data definitions) or data.
+        Statistics for tables, materialized views, and indexes are dumped.
+       </para>
+
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-Z <replaceable class="parameter">level</replaceable></option></term>
       <term><option>-Z <replaceable class="parameter">method</replaceable></option>[:<replaceable>detail</replaceable>]</term>
@@ -829,7 +843,8 @@ PostgreSQL documentation
         though you do not need the data in it.
        </para>
        <para>
-        To exclude data for all tables in the database, see <option>--schema-only</option>.
+        To exclude data for all tables in the database, see <option>--schema-only</option>
+        or <option>--statistics-only</option>.
        </para>
       </listitem>
      </varlistentry>
@@ -1086,6 +1101,15 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index 2e3ba80258..8b1658e648 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -94,7 +94,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Restore only the data, not the schema (data definitions).
+        Restore only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are restored,
         if present in the archive.
        </para>
@@ -483,10 +483,11 @@ PostgreSQL documentation
         to the extent that schema entries are present in the archive.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive of <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
-        <option>--section=pre-data --section=post-data</option>.
+        <option>--section=pre-data --section=post-data --no-statistics</option>.
        </para>
        <para>
         (Do not confuse this with the <option>--schema</option> option, which
@@ -599,6 +600,20 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Restore only the statistics, not schema or data.
+       </para>
+       <para>
+        (Do not confuse this with the <option>--schema</option> option, which
+        uses the word <quote>schema</quote> in a different meaning.)
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-1</option></term>
       <term><option>--single-transaction</option></term>
@@ -723,6 +738,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore statistics, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
-- 
2.45.2

#171Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#170)
Re: Statistics Import and Export

On Thu, 2024-07-18 at 02:09 -0400, Corey Huinker wrote:

v23:

Split pg_set_relation_stats into two functions: pg_set_relation_stats
with named parameters like it had around v19 and
pg_restore_relations_stats with the variadic parameters it has had in
more recent versions, which processes the variadic parameters and
then makes a call to pg_set_relation_stats.

Split pg_set_attribute_stats into two functions:
pg_set_attribute_stats with named parameters like it had around v19
and pg_restore_attribute_stats with the variadic parameters it has
had in more recent versions, which processes the variadic parameters
and then makes a call to pg_set_attribute_stats.

The intention here is that the named parameters signatures are easier
for ad-hoc use, while the variadic signatures are evergreen and thus
ideal for pg_dump/pg_upgrade.

v23-0001:

* I like the split for the reason you mention. I'm not 100% sure that
we need both, but from the standpoint of reviewing, it makes things
easier. We can always remove one at the last minute if its found to be
unnecessary. I also like the names.

* Doc build error and malformatting.

* I'm not certain that we want all changes to relation stats to be non-
transactional. Are there transactional use cases? Should it be an
option? Should it be transactional for pg_set_relation_stats() but non-
transactional for pg_restore_relation_stats()?

* The documentation for the pg_set_attribute_stats() still refers to
upgrade scenarios -- shouldn't that be in the
pg_restore_attribute_stats() docs? I imagine the pg_set variant to be
used for ad-hoc planner stuff rather than upgrades.

* For the "WARNING: stat names must be of type text" I think we need an
ERROR instead. The calling convention of name/value pairs is broken and
we can't safely continue.

* The huge list of "else if (strcmp(statname, mc_freqs_name) == 0) ..."
seems wasteful and hard to read. I think we already discussed this,
what was the reason we can't just use an array to map the arg name to
an arg position type OID?

* How much error checking did we decide is appropriate? Do we need to
check that range_length_hist is always specified with range_empty_frac,
or should we just call that the planner's problem if one is specified
and the other not? Similarly, range stats for a non-range type.

* I think most of the tests should be of pg_set_*_stats(). For
pg_restore_, we just want to know that it's translating the name/value
pairs reasonably well and throwing WARNINGs when appropriate. Then, for
pg_dump tests, it should exercise pg_restore_*_stats() more completely.

* It might help to clarify which arguments are important (like
n_distinct) vs not. I assume the difference is that it's a non-NULLable
column in pg_statistic.

* Some arguments, like the relid, just seem absolutely required, and
it's weird to just emit a WARNING and return false in that case.

* To clarify: a return of "true" means all settings were successfully
applied, whereas "false" means that some were applied and some were
unrecognized, correct? Or does it also mean that some recognized
options may not have been applied?

* pg_set_attribute_stats(): why initialize the output tuple nulls array
to false? It seems like initializing it to true would be safer.

* please use a better name for "k" and add some error checking to make
sure it doesn't overrun the available slots.

* the pg_statistic tuple is always completely replaced, but the way you
can call pg_set_attribute_stats() doesn't imply that -- calling
pg_set_attribute_stats(..., most_common_vals => ..., most_common_freqs
=> ...) looks like it would just replace the most_common_vals+freqs and
leave histogram_bounds as it was, but it actually clears
histogram_bounds, right? Should we make that work or should we document
better that it doesn't?

Regards,
Jeff Davis

#172Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#171)
Re: Statistics Import and Export

* Doc build error and malformatting.

Looking into it.

* I'm not certain that we want all changes to relation stats to be non-
transactional. Are there transactional use cases? Should it be an
option? Should it be transactional for pg_set_relation_stats() but non-
transactional for pg_restore_relation_stats()?

It's non-transactional because that's how ANALYZE does it to avoid bloating
pg_class. We _could_ do it transactionally, but on restore we'd immediately
have a pg_class that was 50% bloat.

* The documentation for the pg_set_attribute_stats() still refers to
upgrade scenarios -- shouldn't that be in the
pg_restore_attribute_stats() docs? I imagine the pg_set variant to be
used for ad-hoc planner stuff rather than upgrades.

Noted.

* For the "WARNING: stat names must be of type text" I think we need an
ERROR instead. The calling convention of name/value pairs is broken and
we can't safely continue.

They can't be errors, because any one error fails the whole pg_upgrade.

* The huge list of "else if (strcmp(statname, mc_freqs_name) == 0) ..."
seems wasteful and hard to read. I think we already discussed this,
what was the reason we can't just use an array to map the arg name to
an arg position type OID?

That was my overreaction to the dislike that the P_argname enum got in
previous reviews.

We'd need an array of struct like

argname (ex. "mc_vals")
argtypeoid (one of: int, text, real, rea[])
argtypename (name we want to call the argtypeoid (integer, text. real,
real[] about covers it).
argpos (position in the arg list of the corresponding pg_set_ function

* How much error checking did we decide is appropriate? Do we need to
check that range_length_hist is always specified with range_empty_frac,
or should we just call that the planner's problem if one is specified
and the other not? Similarly, range stats for a non-range type.

I suppose we can let that go, and leave incomplete stat pairs in there.

The big risk is that somebody packs the call with more than 5 statkinds,
which would overflow the struct.

* I think most of the tests should be of pg_set_*_stats(). For
pg_restore_, we just want to know that it's translating the name/value
pairs reasonably well and throwing WARNINGs when appropriate. Then, for
pg_dump tests, it should exercise pg_restore_*_stats() more completely.

I was afraid you'd suggest that, in which case I'd break up the patch into
the pg_sets and the pg_restores.

* It might help to clarify which arguments are important (like
n_distinct) vs not. I assume the difference is that it's a non-NULLable
column in pg_statistic.

There are NOT NULL stats...now. They might not be in the future. Does that
change your opinion?

* Some arguments, like the relid, just seem absolutely required, and
it's weird to just emit a WARNING and return false in that case.

Again, we can't fail.Any one failure breaks pg_upgrade.

* To clarify: a return of "true" means all settings were successfully
applied, whereas "false" means that some were applied and some were
unrecognized, correct? Or does it also mean that some recognized
options may not have been applied?

True means "at least some stats were applied. False means "nothing was
modified".

* pg_set_attribute_stats(): why initialize the output tuple nulls array
to false? It seems like initializing it to true would be safer.

+1

* please use a better name for "k" and add some error checking to make
sure it doesn't overrun the available slots.

k was an inheritance from analzye.c, from whence the very first version was
cribbed. No objection to renaming.

* the pg_statistic tuple is always completely replaced, but the way you
can call pg_set_attribute_stats() doesn't imply that -- calling
pg_set_attribute_stats(..., most_common_vals => ..., most_common_freqs
=> ...) looks like it would just replace the most_common_vals+freqs and
leave histogram_bounds as it was, but it actually clears
histogram_bounds, right? Should we make that work or should we document
better that it doesn't?

That would complicate things. How would we intentionally null-out one stat,
while leaving others unchanged? However, this points out that I didn't
re-instate the re-definition that applied the NULL defaults.

#173Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#172)
5 attachment(s)
Re: Statistics Import and Export

Attached is v24, incorporating Jeff's feedback - looping an arg data
structure rather than individually checking each param type being the
biggest of them.

v23's part one has been broken into three patches:

* pg_set_relation_stats
* pg_set_attribute_stats
* pg_restore_X_stats

And the two pg_dump-related patches remain unchanged.

I think this split is a net-positive for reviewability. The one drawback is
that there's a lot of redundancy in the regression tests now, much of which
can go away once we decide what other data problems we don't need to check.

Show quoted text

Attachments:

v24-0004-Add-derivative-flags-dumpSchema-dumpData.patchtext/x-patch; charset=US-ASCII; name=v24-0004-Add-derivative-flags-dumpSchema-dumpData.patchDownload
From c34dfd3feb4129af8f550238b2408ef8689357c1 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 4 May 2024 04:52:38 -0400
Subject: [PATCH v24 4/5] Add derivative flags dumpSchema, dumpData.

User-set flags --schema-only and --data-only are often consulted by
various operations to determine if they should be skipped or not. While
this logic works when there are only two mutually-exclusive -only
options, it will get progressively more confusing when more are added.

In anticipation of this, create the flags dumpSchema and dumpData which
are derivative of the existing options schemaOnly and dataOnly. This
allows us to restate current skip-this-section tests in terms of what is
enabled, rather than checking if the other -only mode is turned off.
---
 src/bin/pg_dump/pg_backup.h  |   8 ++
 src/bin/pg_dump/pg_dump.c    | 186 ++++++++++++++++++-----------------
 src/bin/pg_dump/pg_restore.c |   4 +
 3 files changed, 107 insertions(+), 91 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index fbf5f1c515..d942a6a256 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -157,6 +157,10 @@ typedef struct _restoreOptions
 	int			enable_row_security;
 	int			sequence_data;	/* dump sequence data even in schema-only mode */
 	int			binary_upgrade;
+
+	/* flags derived entirely from the user-settable flags */
+	bool		dumpSchema;
+	bool		dumpData;
 } RestoreOptions;
 
 typedef struct _dumpOptions
@@ -203,6 +207,10 @@ typedef struct _dumpOptions
 
 	int			sequence_data;	/* dump sequence data even in schema-only mode */
 	int			do_nothing;
+
+	/* flags derived entirely from the user-settable flags */
+	bool		dumpSchema;
+	bool		dumpData;
 } DumpOptions;
 
 /*
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index b8b1888bd3..5f7cd2b29e 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -755,6 +755,10 @@ main(int argc, char **argv)
 	if (dopt.if_exists && !dopt.outputClean)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
+	/* set derivative flags */
+	dopt.dumpSchema = (!dopt.dataOnly);
+	dopt.dumpData = (!dopt.schemaOnly);
+
 	/*
 	 * --inserts are already implied above if --column-inserts or
 	 * --rows-per-insert were specified.
@@ -937,7 +941,7 @@ main(int argc, char **argv)
 	 * -s means "schema only" and LOs are data, not schema, so we never
 	 * include LOs when -s is used.
 	 */
-	if (dopt.include_everything && !dopt.schemaOnly && !dopt.dontOutputLOs)
+	if (dopt.include_everything && dopt.dumpData && !dopt.dontOutputLOs)
 		dopt.outputLOs = true;
 
 	/*
@@ -951,15 +955,15 @@ main(int argc, char **argv)
 	 */
 	tblinfo = getSchemaData(fout, &numTables);
 
-	if (!dopt.schemaOnly)
+	if (dopt.dumpData)
 	{
 		getTableData(&dopt, tblinfo, numTables, 0);
 		buildMatViewRefreshDependencies(fout);
-		if (dopt.dataOnly)
+		if (!dopt.dumpSchema)
 			getTableDataFKConstraints();
 	}
 
-	if (dopt.schemaOnly && dopt.sequence_data)
+	if (!dopt.dumpData && dopt.sequence_data)
 		getTableData(&dopt, tblinfo, numTables, RELKIND_SEQUENCE);
 
 	/*
@@ -4094,8 +4098,8 @@ dumpPolicy(Archive *fout, const PolicyInfo *polinfo)
 	const char *cmd;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
@@ -4307,8 +4311,8 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 	char	   *qpubname;
 	bool		first = true;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	delq = createPQExpBuffer();
@@ -4622,8 +4626,8 @@ dumpPublicationNamespace(Archive *fout, const PublicationSchemaInfo *pubsinfo)
 	PQExpBuffer query;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	tag = psprintf("%s %s", pubinfo->dobj.name, schemainfo->dobj.name);
@@ -4665,8 +4669,8 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 	PQExpBuffer query;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	tag = psprintf("%s %s", pubinfo->dobj.name, tbinfo->dobj.name);
@@ -5029,8 +5033,8 @@ dumpSubscriptionTable(Archive *fout, const SubRelInfo *subrinfo)
 	PQExpBuffer query;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	Assert(fout->dopt->binary_upgrade && fout->remoteVersion >= 170000);
@@ -5103,8 +5107,8 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo)
 	int			i;
 	char		two_phase_disabled[] = {LOGICALREP_TWOPHASE_STATE_DISABLED, '\0'};
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	delq = createPQExpBuffer();
@@ -7251,8 +7255,8 @@ getPartitioningInfo(Archive *fout)
 	/* hash partitioning didn't exist before v11 */
 	if (fout->remoteVersion < 110000)
 		return;
-	/* needn't bother if schema-only dump */
-	if (fout->dopt->schemaOnly)
+	/* needn't bother if not dumping data */
+	if (!fout->dopt->dumpData)
 		return;
 
 	query = createPQExpBuffer();
@@ -8894,7 +8898,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * Now get info about column defaults.  This is skipped for a data-only
 	 * dump, as it is only needed for table schemas.
 	 */
-	if (!dopt->dataOnly && tbloids->len > 1)
+	if (dopt->dumpSchema && tbloids->len > 1)
 	{
 		AttrDefInfo *attrdefs;
 		int			numDefaults;
@@ -9024,7 +9028,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * Get info about table CHECK constraints.  This is skipped for a
 	 * data-only dump, as it is only needed for table schemas.
 	 */
-	if (!dopt->dataOnly && checkoids->len > 2)
+	if (dopt->dumpSchema && checkoids->len > 2)
 	{
 		ConstraintInfo *constrs;
 		int			numConstrs;
@@ -9934,13 +9938,13 @@ dumpCommentExtended(Archive *fout, const char *type,
 	/* Comments are schema not data ... except LO comments are data */
 	if (strcmp(type, "LARGE OBJECT") != 0)
 	{
-		if (dopt->dataOnly)
+		if (!dopt->dumpSchema)
 			return;
 	}
 	else
 	{
 		/* We do dump LO comments in binary-upgrade mode */
-		if (dopt->schemaOnly && !dopt->binary_upgrade)
+		if (!dopt->dumpData && !dopt->binary_upgrade)
 			return;
 	}
 
@@ -10047,7 +10051,7 @@ dumpTableComment(Archive *fout, const TableInfo *tbinfo,
 		return;
 
 	/* Comments are SCHEMA not data */
-	if (dopt->dataOnly)
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Search for comments associated with relation, using table */
@@ -10493,8 +10497,8 @@ dumpNamespace(Archive *fout, const NamespaceInfo *nspinfo)
 	PQExpBuffer delq;
 	char	   *qnspname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -10570,8 +10574,8 @@ dumpExtension(Archive *fout, const ExtensionInfo *extinfo)
 	PQExpBuffer delq;
 	char	   *qextname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -10695,8 +10699,8 @@ dumpType(Archive *fout, const TypeInfo *tyinfo)
 {
 	DumpOptions *dopt = fout->dopt;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Dump out in proper style */
@@ -11806,8 +11810,8 @@ dumpShellType(Archive *fout, const ShellTypeInfo *stinfo)
 	DumpOptions *dopt = fout->dopt;
 	PQExpBuffer q;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -11858,8 +11862,8 @@ dumpProcLang(Archive *fout, const ProcLangInfo *plang)
 	FuncInfo   *inlineInfo = NULL;
 	FuncInfo   *validatorInfo = NULL;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
@@ -12066,8 +12070,8 @@ dumpFunc(Archive *fout, const FuncInfo *finfo)
 	int			nconfigitems = 0;
 	const char *keyword;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -12458,8 +12462,8 @@ dumpCast(Archive *fout, const CastInfo *cast)
 	const char *sourceType;
 	const char *targetType;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Cannot dump if we don't have the cast function's info */
@@ -12564,8 +12568,8 @@ dumpTransform(Archive *fout, const TransformInfo *transform)
 	char	   *lanname;
 	const char *transformType;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Cannot dump if we don't have the transform functions' info */
@@ -12713,8 +12717,8 @@ dumpOpr(Archive *fout, const OprInfo *oprinfo)
 	char	   *oprregproc;
 	char	   *oprref;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
@@ -13000,8 +13004,8 @@ dumpAccessMethod(Archive *fout, const AccessMethodInfo *aminfo)
 	PQExpBuffer delq;
 	char	   *qamname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -13103,8 +13107,8 @@ dumpOpclass(Archive *fout, const OpclassInfo *opcinfo)
 	bool		needComma;
 	int			i;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -13374,8 +13378,8 @@ dumpOpfamily(Archive *fout, const OpfamilyInfo *opfinfo)
 	bool		needComma;
 	int			i;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -13581,8 +13585,8 @@ dumpCollation(Archive *fout, const CollInfo *collinfo)
 	const char *colllocale;
 	const char *collicurules;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -13835,8 +13839,8 @@ dumpConversion(Archive *fout, const ConvInfo *convinfo)
 	const char *conproc;
 	bool		condefault;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -13983,8 +13987,8 @@ dumpAgg(Archive *fout, const AggInfo *agginfo)
 	const char *proparallel;
 	char		defaultfinalmodify;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -14313,8 +14317,8 @@ dumpTSParser(Archive *fout, const TSParserInfo *prsinfo)
 	PQExpBuffer delq;
 	char	   *qprsname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14381,8 +14385,8 @@ dumpTSDictionary(Archive *fout, const TSDictInfo *dictinfo)
 	char	   *nspname;
 	char	   *tmplname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14457,8 +14461,8 @@ dumpTSTemplate(Archive *fout, const TSTemplateInfo *tmplinfo)
 	PQExpBuffer delq;
 	char	   *qtmplname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14523,8 +14527,8 @@ dumpTSConfig(Archive *fout, const TSConfigInfo *cfginfo)
 	int			i_tokenname;
 	int			i_dictname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14635,8 +14639,8 @@ dumpForeignDataWrapper(Archive *fout, const FdwInfo *fdwinfo)
 	PQExpBuffer delq;
 	char	   *qfdwname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14708,8 +14712,8 @@ dumpForeignServer(Archive *fout, const ForeignServerInfo *srvinfo)
 	char	   *qsrvname;
 	char	   *fdwname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14899,8 +14903,8 @@ dumpDefaultACL(Archive *fout, const DefaultACLInfo *daclinfo)
 	PQExpBuffer tag;
 	const char *type;
 
-	/* Do nothing in data-only dump, or if we're skipping ACLs */
-	if (dopt->dataOnly || dopt->aclsSkip)
+	/* Do nothing if not dumping schema, or if we're skipping ACLs */
+	if (!dopt->dumpSchema || dopt->aclsSkip)
 		return;
 
 	q = createPQExpBuffer();
@@ -15000,7 +15004,7 @@ dumpACL(Archive *fout, DumpId objDumpId, DumpId altDumpId,
 		return InvalidDumpId;
 
 	/* --data-only skips ACLs *except* large object ACLs */
-	if (dopt->dataOnly && strcmp(type, "LARGE OBJECT") != 0)
+	if (!dopt->dumpSchema && strcmp(type, "LARGE OBJECT") != 0)
 		return InvalidDumpId;
 
 	sql = createPQExpBuffer();
@@ -15129,13 +15133,13 @@ dumpSecLabel(Archive *fout, const char *type, const char *name,
 	 */
 	if (strcmp(type, "LARGE OBJECT") != 0)
 	{
-		if (dopt->dataOnly)
+		if (!dopt->dumpSchema)
 			return;
 	}
 	else
 	{
 		/* We do dump large object security labels in binary-upgrade mode */
-		if (dopt->schemaOnly && !dopt->binary_upgrade)
+		if (!dopt->dumpData && !dopt->binary_upgrade)
 			return;
 	}
 
@@ -15203,7 +15207,7 @@ dumpTableSecLabel(Archive *fout, const TableInfo *tbinfo, const char *reltypenam
 		return;
 
 	/* SecLabel are SCHEMA not data */
-	if (dopt->dataOnly)
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Search for comments associated with relation, using table */
@@ -15442,8 +15446,8 @@ dumpTable(Archive *fout, const TableInfo *tbinfo)
 	DumpId		tableAclDumpId = InvalidDumpId;
 	char	   *namecopy;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	if (tbinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
@@ -16472,8 +16476,8 @@ dumpTableAttach(Archive *fout, const TableAttachInfo *attachinfo)
 	PGresult   *res;
 	char	   *partbound;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -16544,8 +16548,8 @@ dumpAttrDef(Archive *fout, const AttrDefInfo *adinfo)
 	char	   *tag;
 	char	   *foreign;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Skip if not "separate"; it was dumped in the table's definition */
@@ -16633,8 +16637,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 	char	   *qindxname;
 	char	   *qqindxname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -16766,8 +16770,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 static void
 dumpIndexAttach(Archive *fout, const IndexAttachInfo *attachinfo)
 {
-	/* Do nothing in data-only dump */
-	if (fout->dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!fout->dopt->dumpSchema)
 		return;
 
 	if (attachinfo->partitionIdx->dobj.dump & DUMP_COMPONENT_DEFINITION)
@@ -16813,8 +16817,8 @@ dumpStatisticsExt(Archive *fout, const StatsExtInfo *statsextinfo)
 	PGresult   *res;
 	char	   *stxdef;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -16889,8 +16893,8 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
 	char	   *tag = NULL;
 	char	   *foreign;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -17521,8 +17525,8 @@ dumpTrigger(Archive *fout, const TriggerInfo *tginfo)
 	char	   *qtabname;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -17643,8 +17647,8 @@ dumpEventTrigger(Archive *fout, const EventTriggerInfo *evtinfo)
 	PQExpBuffer delqry;
 	char	   *qevtname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -17734,8 +17738,8 @@ dumpRule(Archive *fout, const RuleInfo *rinfo)
 	PGresult   *res;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index df119591cc..3475168a64 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -360,6 +360,10 @@ main(int argc, char **argv)
 	if (opts->single_txn && numWorkers > 1)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
+	/* set derivative flags */
+	opts->dumpSchema = (opts->dataOnly != 1);
+	opts->dumpData = (opts->schemaOnly != 1);
+
 	opts->disable_triggers = disable_triggers;
 	opts->enable_row_security = enable_row_security;
 	opts->noDataForFailedTables = no_data_for_failed_tables;
-- 
2.45.2

v24-0003-Create-funcntions-pg_restore_relation_stats.patchtext/x-patch; charset=US-ASCII; name=v24-0003-Create-funcntions-pg_restore_relation_stats.patchDownload
From 382923d7cf64d47cf882668afe68534991d22ae1 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 17 Jul 2024 23:23:23 -0400
Subject: [PATCH v24 3/5] Create funcntions pg_restore_relation_stats(),
 pg_restore_attribute_stats().

These functions are variadic requivalents of the pg_set_*_stats()
functions, which have a function signature each stat getting its own
defined parameter (or parameter pair, as the case may be).

Such a rigid function signature would make future compability difficult,
and future compatibility is just what pg_dump needs.

Instead, these functions have all of the statistics put into a variable
argument list organized in name-value pairs. The leading or "name"
parameters must all be of type text and the string must exactly
correspond to the name of a statistics parameter in the corresponding
pg_set_X_stats function. The trailing or "value" parameter must be of
the type expected by the same-named parameter in the pg_set_X_stats
function. Names that do not match a parameter name and types that do not
match the expected type will emit a warning and be ignored. If that
parameter was a require parameter, the function will return false and no
stats will be modified.

The intention of these functions is to be used in pg_dump/pg_restore and
pg_upgrade to allow the user to avoid having to run vacuumdb
--analyze-in-stages after an upgrade or restore.
---
 src/include/catalog/pg_proc.dat               |  20 +-
 src/include/statistics/statistics.h           |   4 +-
 src/backend/statistics/statistics.c           | 294 ++++++-
 .../regress/expected/stats_export_import.out  | 814 ++++++++++++++++--
 src/test/regress/sql/stats_export_import.sql  | 586 ++++++++++++-
 doc/src/sgml/func.sgml                        | 209 +++++
 6 files changed, 1837 insertions(+), 90 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 5e553939b7..42ee44bce6 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12222,5 +12222,23 @@
   proparallel => 'u', prorettype => 'bool',
   proargtypes => 'oid name bool int4 float4 int4 float4 text _float4 text float4 text _float4 _float4 text float4 text',
   proargnames => '{relation,attname,inherited,version,null_frac,avg_width,n_distinct,most_common_vals,most_common_freqs,histogram_bounds,correlation,most_common_elems,most_common_elem_freqs,elem_count_histogram,range_length_histogram,range_empty_frac,range_bounds_histogram}',
-  prosrc => 'pg_set_attribute_stats' }
+  prosrc => 'pg_set_attribute_stats' },
+{ oid => '8050',
+  descr => 'set statistics on relation',
+  proname => 'pg_restore_relation_stats', provariadic => 'any',
+  proisstrict => 'f', provolatile => 'v',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'regclass int4 any',
+  proargnames => '{relation,version,stats}',
+  proargmodes => '{i,i,v}',
+  prosrc => 'pg_restore_relation_stats' },
+{ oid => '8051',
+  descr => 'set statistics on attribute',
+  proname => 'pg_restore_attribute_stats', provariadic => 'any',
+  proisstrict => 'f', provolatile => 'v',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'regclass name bool int4 any',
+  proargnames => '{relation,attname,inherited,version,stats}',
+  proargmodes => '{i,i,i,i,v}',
+  prosrc => 'pg_restore_attribute_stats' },
 ]
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 1dddf96576..9f558948cd 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -129,4 +129,6 @@ extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
 extern Datum pg_set_relation_stats(PG_FUNCTION_ARGS);
 extern Datum pg_set_attribute_stats(PG_FUNCTION_ARGS);
-#endif							/* STATISTICS_H */
+extern Datum pg_restore_relation_stats(PG_FUNCTION_ARGS);
+extern Datum pg_restore_attribute_stats(PG_FUNCTION_ARGS);
+#endif
diff --git a/src/backend/statistics/statistics.c b/src/backend/statistics/statistics.c
index 2a43da7c41..e86b703e3a 100644
--- a/src/backend/statistics/statistics.c
+++ b/src/backend/statistics/statistics.c
@@ -24,6 +24,7 @@
 #include "catalog/pg_database.h"
 #include "catalog/pg_operator.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_type_d.h"
 #include "fmgr.h"
 #include "funcapi.h"
 #include "miscadmin.h"
@@ -43,7 +44,8 @@
 
 /*
  * Names of parameters found in the functions pg_set_relation_stats and
- * pg_set_attribute_stats
+ * pg_set_attribute_stats, as well as the keyword names used in
+ * pg_restore_relation_stats and pg_restore_attribute_stats.
  */
 const char *relation_name = "relation";
 const char *relpages_name = "relpages";
@@ -65,6 +67,13 @@ const char *range_length_hist_name = "range_length_histogram";
 const char *range_empty_frac_name = "range_empty_frac";
 const char *range_bounds_hist_name = "range_bounds_histogram";
 
+typedef struct variadic_args {
+	int			setargidx;
+	Oid			typoid;
+	const char *argname;
+	const char *typname;
+} variadic_args;
+
 /*
  * A role has privileges to set statistics on the relation if any of the
  * following are true:
@@ -93,7 +102,6 @@ can_modify_relation(Relation rel)
 Datum
 pg_set_relation_stats(PG_FUNCTION_ARGS)
 {
-
 	Datum		relation_datum = PG_GETARG_DATUM(0);
 	bool		relation_isnull = PG_ARGISNULL(0);
 	Oid			relation;
@@ -1203,3 +1211,285 @@ pg_set_attribute_stats(PG_FUNCTION_ARGS)
 	relation_close(rel, NoLock);
 	PG_RETURN_BOOL(true);
 }
+
+ /*
+ * Restore statistics for a given pg_class entry.
+ *
+ * This does an in-place (i.e. non-transactional) update of pg_class, just as
+ * is done in ANALYZE.
+ *
+ * The actual modification of stats happesn in a call to pg_set_relation_stats(),
+ * which has a named parameters for each statistic type. This function serves
+ * as a way to allow stats import calls written for a previous version to work
+ * on the current version, regardless of what parameters were introduced or
+ * removed in the time in between.
+ *
+ */
+Datum
+pg_restore_relation_stats(PG_FUNCTION_ARGS)
+{
+	#define NUM_RELARGS 3
+
+	const variadic_args relation_args[NUM_RELARGS] = {
+			{2, INT4OID,   relpages_name,      "integer"},
+			{3, FLOAT4OID, reltuples_name,     "real"},
+			{4, INT4OID,   relallvisible_name, "integer"}
+		};
+
+	bool args_set[NUM_RELARGS] = {false, false, false};
+
+	Datum		relation_datum = PG_GETARG_DATUM(0);
+	bool		relation_isnull = PG_ARGISNULL(0);
+
+	Datum		version_datum = PG_GETARG_DATUM(1);
+	bool		version_isnull = PG_ARGISNULL(1);
+
+	/* Flags to indicate that a datum has been found once already */
+
+	/* build argument values to build the object */
+	Datum	   *args;
+	bool	   *argnulls;
+	Oid		   *types;
+	int			nargs;
+
+	LOCAL_FCINFO(set_fcinfo, 5);
+	Datum		result;
+
+	nargs = extract_variadic_args(fcinfo, 2, true, &args, &types, &argnulls);
+
+	/* if the pairs aren't pairs, something is malformed */
+	if (nargs % 2 == 1)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("stats parameters must be in name-value pairs")));
+		PG_RETURN_BOOL(false);
+	}
+
+	InitFunctionCallInfoData(*set_fcinfo, NULL, 5, InvalidOid, NULL, NULL);
+
+	set_fcinfo->args[0].value = relation_datum;
+	set_fcinfo->args[0].isnull = relation_isnull;
+	set_fcinfo->args[1].value = version_datum;
+	set_fcinfo->args[1].isnull = version_isnull;
+
+	/* Assume an argument is NULL unless matching pair found */
+	for (int i = 2; i < 5; i++)
+	{
+		set_fcinfo->args[i].value = (Datum) 0;
+		set_fcinfo->args[i].isnull = true;
+	}
+
+	/* loop through args, matching params to their arg indexes */
+	for (int i = 0; i < nargs; i += 2)
+	{
+		char   *statname;
+		int		argidx = i + 1;
+		bool	found = false;
+
+		if (types[i] != TEXTOID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("stat names must be of type text")));
+			continue;
+		}
+		statname = TextDatumGetCString(args[i]);
+
+		for (int j = 0; j < NUM_RELARGS; j++)
+		{
+			const variadic_args *varg = &relation_args[j];
+
+			if (strcmp(statname, varg->argname) == 0)
+			{	
+				found = true;
+
+				/* we go with the first set of each arg, duplicates ignored */
+				if (args_set[j])
+				{
+					ereport(WARNING,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("stat name %s already used, subsequent values ignored",
+									statname)));
+					break;
+				}
+				else if (types[argidx] != varg->typoid)
+				{
+					ereport(WARNING,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("%s must be of type %s",
+									statname, varg->typname)));
+					PG_RETURN_BOOL(false);
+				}
+				else
+				{
+					args_set[j] = true;
+					set_fcinfo->args[varg->setargidx].value = args[argidx];
+					set_fcinfo->args[varg->setargidx].isnull = argnulls[argidx];
+				}
+
+				break; /* already matched one */
+			}
+		}
+		if (!found)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("stat name %s is not recognized, values ignored",
+							statname)));
+		pfree(statname);
+	}
+
+	result = (*pg_set_relation_stats) (set_fcinfo);
+
+	PG_RETURN_DATUM(result);
+}
+
+/*
+ * Import statistics for a given relation attribute.
+ *
+ * This will insert/replace a row in pg_statistic for the given combinarion
+ * of relation, attribute, and inherited flag.
+ *
+ * The version parameter is for future use in the events as future versions
+ * may change them meaning of certain parameters.
+ *
+ * The variadic parameters all represent name-value pairs, with the names
+ * corresponding to attributes in pg_stats. Unkown names will generate a
+ * warning.
+ *
+ * Parameters null_frac, avg_width, and n_distinct are required because
+ * those attributes have no default value in pg_statistic.
+ *
+ * The remaining parameters all belong to a specific stakind, and all are
+ * optional. Some stakinds have multiple parameters, and in those cases
+ * both parameters must be specified if one of them is, otherwise a
+ * warning is generated but the rest of the stats may still be imported.
+ *
+ * If there is no attribute with a matching attname in the relation, the
+ * function will raise a warning and return false.
+ *
+ * Parameters corresponding to ANYARRAY columns are instead passed in as text
+ * values, which is a valid input string for an array of the type or element
+ * type of the attribute.
+ */
+Datum
+pg_restore_attribute_stats(PG_FUNCTION_ARGS)
+{
+	#define NUM_ATTARGS 13
+
+	const variadic_args attribute_args[NUM_ATTARGS] = {
+			{4,  FLOAT4OID,      null_frac_name,         "real"},
+			{5,  INT4OID,        avg_width_name,         "integer"},
+			{6,  FLOAT4OID,      n_distinct_name,        "real"},
+			{7,  TEXTOID,        mc_vals_name,           "text"},
+			{8,  FLOAT4ARRAYOID, mc_freqs_name,          "real[]"},
+			{9,  TEXTOID,        histogram_bounds_name,  "text"},
+			{10, FLOAT4OID,      correlation_name,       "real"},
+			{11, TEXTOID,        mc_elems_name,          "text"},
+			{12, FLOAT4ARRAYOID, mc_elem_freqs_name,     "real[]"},
+			{13, FLOAT4ARRAYOID, elem_count_hist_name,   "real[]"},
+			{14, TEXTOID,        range_length_hist_name, "text"},
+			{15, FLOAT4OID,      range_empty_frac_name,  "real"},
+			{16, TEXTOID,        range_bounds_hist_name, "text"},
+		};
+	bool args_set[NUM_ATTARGS] = {false,false,false,false,false,false,false,
+								  false,false,false,false,false,false};
+
+	/* build argument values to build the object */
+	Datum	   *args;
+	bool	   *argnulls;
+	Oid		   *types;
+	int			nargs;
+
+	LOCAL_FCINFO(set_fcinfo, 17);
+	Datum		result;
+
+	InitFunctionCallInfoData(*set_fcinfo, NULL, 17, InvalidOid, NULL, NULL);
+
+	/* line up positional arguments for pg_set_attribute_stats */
+	for (int i = 0; i <= 3; i++)
+	{
+		set_fcinfo->args[i].value = PG_GETARG_DATUM(i);
+		set_fcinfo->args[i].isnull = PG_ARGISNULL(i);
+	}
+
+	/* initialize remaining arguments which may not get set */
+	for (int i = 4; i < 17; i++)
+	{
+		set_fcinfo->args[i].value = (Datum) 0;
+		set_fcinfo->args[i].isnull = true;
+	}
+
+	nargs = extract_variadic_args(fcinfo, 4, true, &args, &types, &argnulls);
+
+	/* if the pairs aren't pairs, something is malformed */
+	if (nargs % 2 == 1)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("stats parameters must be in name-value pairs")));
+		PG_RETURN_BOOL(false);
+	}
+
+	/* loop through args, matching params to their arg indexes */
+	for (int i = 0; i < nargs; i += 2)
+	{
+		char   *statname;
+		int		argidx = i + 1;
+		bool	found = false;
+
+		if (types[i] != TEXTOID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("stat names must be of type text")));
+			continue;
+		}
+		statname = TextDatumGetCString(args[i]);
+
+		for (int j = 0; j < NUM_ATTARGS; j++)
+		{
+			const variadic_args *varg = &attribute_args[j];
+
+			if (strcmp(statname, varg->argname) == 0)
+			{	
+				found = true;
+
+				/* we go with the first set of each arg, duplicates ignored */
+				if (args_set[j])
+				{
+					ereport(WARNING,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("stat name %s already used, subsequent values ignored",
+									statname)));
+					break;
+				}
+				else if (types[argidx] != varg->typoid)
+				{
+					ereport(WARNING,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("%s must be of type %s",
+									statname, varg->typname)));
+					PG_RETURN_BOOL(false);
+				}
+				else
+				{
+					args_set[j] = true;
+					set_fcinfo->args[varg->setargidx].value = args[argidx];
+					set_fcinfo->args[varg->setargidx].isnull = argnulls[argidx];
+				}
+				break; /* already matched one */
+			}
+		}
+		if (!found)
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("stat name %s is not recognized, values ignored",
+							statname)));
+		pfree(statname);
+	}
+
+	result = (*pg_set_attribute_stats) (set_fcinfo);
+
+	PG_RETURN_DATUM(result);
+}
diff --git a/src/test/regress/expected/stats_export_import.out b/src/test/regress/expected/stats_export_import.out
index bc99bf3745..ee188ae783 100644
--- a/src/test/regress/expected/stats_export_import.out
+++ b/src/test/regress/expected/stats_export_import.out
@@ -469,40 +469,6 @@ WARNING:  Relation test attname id is a scalar type, cannot have stats of type m
  t
 (1 row)
 
--- warn: mcelem / mcelem mismatch
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_export_import.test'::regclass,
-    attname => 'tags'::name,
-    inherited => false::boolean,
-    version => 150000::integer,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_elems => '{one,two}'::text
-    );
-WARNING:  most_common_elem_freqs cannot be NULL when most_common_elems is NOT NULL
- pg_set_attribute_stats 
-------------------------
- t
-(1 row)
-
--- warn: mcelem / mcelem null mismatch part 2
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_export_import.test'::regclass,
-    attname => 'tags'::name,
-    inherited => false::boolean,
-    version => 150000::integer,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
-    );
-WARNING:  most_common_elems cannot be NULL when most_common_elem_freqs is NOT NULL
- pg_set_attribute_stats 
-------------------------
- t
-(1 row)
-
 -- ok: mcelem
 SELECT pg_catalog.pg_set_attribute_stats(
     relation => 'stats_export_import.test'::regclass,
@@ -520,6 +486,76 @@ SELECT pg_catalog.pg_set_attribute_stats(
  t
 (1 row)
 
+-- warn: scalars cannot have mcelem
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{1,3}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+WARNING:  Relation test attname id is a scalar type, cannot have stats of type most_common_elems, ignored
+WARNING:  Relation test attname id is a scalar type, cannot have stats of type most_common_elem_freqs, ignored
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,two}'::text
+    );
+WARNING:  most_common_elem_freqs cannot be NULL when most_common_elems is NOT NULL
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
+    );
+WARNING:  most_common_elems cannot be NULL when most_common_elem_freqs is NOT NULL
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- ok: mcelem
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
 SELECT *
 FROM pg_stats
 WHERE schemaname = 'stats_export_import'
@@ -672,23 +708,6 @@ AND attname = 'arange';
  stats_export_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
 (1 row)
 
--- warn: scalars can't have range stats
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_export_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    version => 150000::integer,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
-    );
-WARNING:  Relation test attname id is not a range type, cannot have stats of type range_bounds_histogram
- pg_set_attribute_stats 
-------------------------
- t
-(1 row)
-
 -- ok: range_bounds_histogram
 SELECT pg_catalog.pg_set_attribute_stats(
     relation => 'stats_export_import.test'::regclass,
@@ -934,6 +953,699 @@ WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
 ---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
 (0 rows)
 
+--
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+        1 |         4 |             0
+(1 row)
+
+-- false: object doesn't exist
+SELECT pg_restore_relation_stats('0'::oid,
+                             150000::integer,
+                             'relpages', '17'::integer,
+                             'reltuples', 400::real,
+                             'relallvisible', 4::integer);
+WARNING:  pg_class entry for relid 0 not found
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+-- false: reltuples, relallvisible missing
+SELECT pg_restore_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', '17'::integer);
+WARNING:  reltuples cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+-- false: null value
+SELECT pg_restore_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', '17'::integer,
+                             'reltuples', NULL::real,
+                             'relallvisible', 4::integer);
+WARNING:  reltuples cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+-- false: bad relpages type
+SELECT pg_restore_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+WARNING:  relpages must be of type integer
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+-- ok 
+SELECT pg_restore_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 17::integer,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+ pg_restore_relation_stats 
+---------------------------
+ t
+(1 row)
+
+-- false: object does not exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    '0'::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+WARNING:  Parameter relation OID 0 is invalid
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- false: relation null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    NULL::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+WARNING:  relation cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- false: attname null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    NULL::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+WARNING:  attname cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- false: inherited null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    NULL::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+WARNING:  inherited cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- false: null_frac null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', NULL::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+WARNING:  null_frac cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- false: avg_width null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', NULL::integer,
+    'n_distinct', 0.3::real);
+WARNING:  avg_width cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- false: avg_width null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', NULL::real);
+WARNING:  n_distinct cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- warn: mcv / mcf null mismatch part 1
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_freqs', '{0.1,0.2,0.3}'::real[]
+    );
+WARNING:  most_common_vals cannot be NULL when most_common_freqs is NOT NULL
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{1,2,3}'::text
+    );
+WARNING:  most_common_freqs cannot be NULL when most_common_vals is NOT NULL
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.2,0.1}'::double precision[]
+    );
+WARNING:  most_common_freqs must be of type real[]
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: mcv cast failure
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,four,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+WARNING:  invalid input syntax for type integer: "four"
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- warn: NULL in histogram array
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,NULL,3,4}'::text
+    );
+WARNING:  histogram_bounds array cannot contain NULL values
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,2,3,4}'::text );
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- range stats on a scalar type
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+WARNING:  Relation test attname id is not a range type, cannot have stats of type range_length_histogram
+WARNING:  Relation test attname id is not a range type, cannot have stats of type range_empty_frac
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+WARNING:  range_empty_frac cannot be NULL when range_length_histogram is NOT NULL
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real
+    );
+WARNING:  range_length_histogram cannot be NULL when range_empty_frac is NOT NULL
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+WARNING:  Relation test attname id is not a range type, cannot have stats of type range_bounds_histogram
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- warn: too many stat kinds
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
+    'histogram_bounds', '{1,2,3,4}'::text,
+    'correlation', 1.1::real,
+    'most_common_elems', '{3,1}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    'range_empty_frac', -0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text);
+WARNING:  imported statistics must have a maximum of 5 slots but 6 given
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+SELECT
+    format('SELECT pg_catalog.pg_restore_attribute_stats( '
+            || '%L::regclass::oid, '
+            || '%L::name, '
+            || '%L::boolean, '
+            || '%L::integer, '
+            || '%L, %L::real, '
+            || '%L, %L::integer, '
+            || '%L, %L::real %s)',
+        'stats_export_import.' || s.tablename || '_clone',
+        s.attname,
+        s.inherited,
+        150000,
+        'null_frac', s.null_frac,
+        'avg_width', s.avg_width,
+        'n_distinct', s.n_distinct,
+        CASE
+            WHEN s.most_common_vals IS NULL THEN ''
+            ELSE format(', %L, %L::text, %L, %L::real[]',
+                        'most_common_vals', s.most_common_vals,
+                        'most_common_freqs', s.most_common_freqs)
+        END ||
+        CASE
+            WHEN s.histogram_bounds IS NULL THEN ''
+            ELSE format(', %L, %L::text',
+                        'histogram_bounds', s.histogram_bounds)
+        END ||
+        CASE
+            WHEN s.correlation IS NULL THEN ''
+            ELSE format(', %L, %L::real',
+                        'correlation', s.correlation)
+        END ||
+        CASE
+            WHEN s.most_common_elems IS NULL THEN ''
+            ELSE format(', %L, %L::text, %L, %L::real[]',
+                        'most_common_elems', s.most_common_elems,
+                        'most_common_elem_freqs', s.most_common_elem_freqs)
+        END ||
+        CASE
+            WHEN s.elem_count_histogram IS NULL THEN ''
+            ELSE format(', %L, %L::real[]',
+                        'elem_count_histogram', s.elem_count_histogram)
+        END ||
+        CASE
+            WHEN s.range_bounds_histogram IS NULL THEN ''
+            ELSE format(', %L, %L::text',
+                        'range_bounds_histogram', s.range_bounds_histogram)
+        END ||
+        CASE
+            WHEN s.range_empty_frac IS NULL THEN ''
+            ELSE format(', %L, %L::real, %L, %L::text',
+                        'range_empty_frac', s.range_empty_frac,
+                        'range_length_histogram', s.range_length_histogram)
+        END)
+FROM pg_catalog.pg_stats AS s
+WHERE s.schemaname = 'stats_export_import'
+AND s.tablename IN ('test', 'is_odd')
+\gexec
+SELECT pg_catalog.pg_restore_attribute_stats( 'stats_export_import.is_odd_clone'::regclass::oid, 'expr'::name, 'f'::boolean, '150000'::integer, 'null_frac', '0.25'::real, 'avg_width', '1'::integer, 'n_distinct', '-0.5'::real , 'most_common_vals', '{t}'::text, 'most_common_freqs', '{0.5}'::real[], 'correlation', '0.5'::real)
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT pg_catalog.pg_restore_attribute_stats( 'stats_export_import.test_clone'::regclass::oid, 'name'::name, 'f'::boolean, '150000'::integer, 'null_frac', '0'::real, 'avg_width', '4'::integer, 'n_distinct', '-1'::real , 'histogram_bounds', '{four,one,tre,two}'::text, 'correlation', '-0.4'::real)
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT pg_catalog.pg_restore_attribute_stats( 'stats_export_import.test_clone'::regclass::oid, 'comp'::name, 'f'::boolean, '150000'::integer, 'null_frac', '0.25'::real, 'avg_width', '53'::integer, 'n_distinct', '-0.75'::real , 'histogram_bounds', E'{"(1,1.1,ONE,01-01-2001,\\"{\\"\\"xkey\\"\\": \\"\\"xval\\"\\"}\\")","(2,2.2,TWO,02-02-2002,\\"[true, 4, \\"\\"six\\"\\"]\\")","(3,3.3,TRE,03-03-2003,)"}'::text, 'correlation', '1'::real)
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT pg_catalog.pg_restore_attribute_stats( 'stats_export_import.test_clone'::regclass::oid, 'tags'::name, 'f'::boolean, '150000'::integer, 'null_frac', '0.5'::real, 'avg_width', '2'::integer, 'n_distinct', '-0.1'::real , 'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[])
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT pg_catalog.pg_restore_attribute_stats( 'stats_export_import.test_clone'::regclass::oid, 'id'::name, 'f'::boolean, '150000'::integer, 'null_frac', '0.5'::real, 'avg_width', '2'::integer, 'n_distinct', '-0.1'::real )
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT pg_catalog.pg_restore_attribute_stats( 'stats_export_import.test_clone'::regclass::oid, 'arange'::name, 'f'::boolean, '150000'::integer, 'null_frac', '0.5'::real, 'avg_width', '2'::integer, 'n_distinct', '-0.1'::real , 'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text)
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- restore ECHO to original value
+\set ECHO :orig_ECHO
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+   relname    | num_stats 
+--------------+-----------
+ is_odd       |         1
+ is_odd_clone |         1
+ test         |         5
+ test_clone   |         5
+(4 rows)
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
 DROP SCHEMA stats_export_import CASCADE;
 NOTICE:  drop cascades to 3 other objects
 DETAIL:  drop cascades to type stats_export_import.complex_type
diff --git a/src/test/regress/sql/stats_export_import.sql b/src/test/regress/sql/stats_export_import.sql
index 76a48e0f94..08f63b11da 100644
--- a/src/test/regress/sql/stats_export_import.sql
+++ b/src/test/regress/sql/stats_export_import.sql
@@ -321,30 +321,6 @@ SELECT pg_catalog.pg_set_attribute_stats(
     most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
     );
 
--- warn: mcelem / mcelem mismatch
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_export_import.test'::regclass,
-    attname => 'tags'::name,
-    inherited => false::boolean,
-    version => 150000::integer,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_elems => '{one,two}'::text
-    );
-
--- warn: mcelem / mcelem null mismatch part 2
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_export_import.test'::regclass,
-    attname => 'tags'::name,
-    inherited => false::boolean,
-    version => 150000::integer,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
-    );
-
 -- ok: mcelem
 SELECT pg_catalog.pg_set_attribute_stats(
     relation => 'stats_export_import.test'::regclass,
@@ -358,6 +334,56 @@ SELECT pg_catalog.pg_set_attribute_stats(
     most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
     );
 
+-- warn: scalars cannot have mcelem
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{1,3}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,two}'::text
+    );
+
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
+    );
+
+-- ok: mcelem
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,three}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
 SELECT *
 FROM pg_stats
 WHERE schemaname = 'stats_export_import'
@@ -418,6 +444,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
     range_empty_frac => 0.5::real,
     range_length_histogram => '{399,499,Infinity}'::text
     );
+
 -- warn: range_empty_frac range_length_hist null mismatch
 SELECT pg_catalog.pg_set_attribute_stats(
     relation => 'stats_export_import.test'::regclass,
@@ -429,6 +456,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
     n_distinct => -0.1::real,
     range_length_histogram => '{399,499,Infinity}'::text
     );
+
 -- warn: range_empty_frac range_length_hist null mismatch part 2
 SELECT pg_catalog.pg_set_attribute_stats(
     relation => 'stats_export_import.test'::regclass,
@@ -440,6 +468,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
     n_distinct => -0.1::real,
     range_empty_frac => 0.5::real
     );
+
 -- ok: range_empty_frac + range_length_hist
 SELECT pg_catalog.pg_set_attribute_stats(
     relation => 'stats_export_import.test'::regclass,
@@ -460,17 +489,6 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'arange';
 
--- warn: scalars can't have range stats
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_export_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    version => 150000::integer,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
-    );
 -- ok: range_bounds_histogram
 SELECT pg_catalog.pg_set_attribute_stats(
     relation => 'stats_export_import.test'::regclass,
@@ -715,4 +733,502 @@ FROM pg_statistic s
 JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
 WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
 
+--
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_export_import.test'::regclass;
+
+-- false: object doesn't exist
+SELECT pg_restore_relation_stats('0'::oid,
+                             150000::integer,
+                             'relpages', '17'::integer,
+                             'reltuples', 400::real,
+                             'relallvisible', 4::integer);
+
+-- false: reltuples, relallvisible missing
+SELECT pg_restore_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', '17'::integer);
+
+-- false: null value
+SELECT pg_restore_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', '17'::integer,
+                             'reltuples', NULL::real,
+                             'relallvisible', 4::integer);
+
+-- false: bad relpages type
+SELECT pg_restore_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+
+-- ok 
+SELECT pg_restore_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 17::integer,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+
+-- false: object does not exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    '0'::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- false: relation null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    NULL::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- false: attname null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    NULL::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- false: inherited null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    NULL::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- false: null_frac null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', NULL::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- false: avg_width null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', NULL::integer,
+    'n_distinct', 0.3::real);
+
+-- false: avg_width null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', NULL::real);
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- warn: mcv / mcf null mismatch part 1
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_freqs', '{0.1,0.2,0.3}'::real[]
+    );
+
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{1,2,3}'::text
+    );
+
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.2,0.1}'::double precision[]
+    );
+
+-- warning: mcv cast failure
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,four,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+
+-- warn: NULL in histogram array
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,NULL,3,4}'::text
+    );
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,2,3,4}'::text );
+
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+
+-- range stats on a scalar type
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real
+    );
+
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+
+-- warn: too many stat kinds
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
+    'histogram_bounds', '{1,2,3,4}'::text,
+    'correlation', 1.1::real,
+    'most_common_elems', '{3,1}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    'range_empty_frac', -0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text);
+
+SELECT
+    format('SELECT pg_catalog.pg_restore_attribute_stats( '
+            || '%L::regclass::oid, '
+            || '%L::name, '
+            || '%L::boolean, '
+            || '%L::integer, '
+            || '%L, %L::real, '
+            || '%L, %L::integer, '
+            || '%L, %L::real %s)',
+        'stats_export_import.' || s.tablename || '_clone',
+        s.attname,
+        s.inherited,
+        150000,
+        'null_frac', s.null_frac,
+        'avg_width', s.avg_width,
+        'n_distinct', s.n_distinct,
+        CASE
+            WHEN s.most_common_vals IS NULL THEN ''
+            ELSE format(', %L, %L::text, %L, %L::real[]',
+                        'most_common_vals', s.most_common_vals,
+                        'most_common_freqs', s.most_common_freqs)
+        END ||
+        CASE
+            WHEN s.histogram_bounds IS NULL THEN ''
+            ELSE format(', %L, %L::text',
+                        'histogram_bounds', s.histogram_bounds)
+        END ||
+        CASE
+            WHEN s.correlation IS NULL THEN ''
+            ELSE format(', %L, %L::real',
+                        'correlation', s.correlation)
+        END ||
+        CASE
+            WHEN s.most_common_elems IS NULL THEN ''
+            ELSE format(', %L, %L::text, %L, %L::real[]',
+                        'most_common_elems', s.most_common_elems,
+                        'most_common_elem_freqs', s.most_common_elem_freqs)
+        END ||
+        CASE
+            WHEN s.elem_count_histogram IS NULL THEN ''
+            ELSE format(', %L, %L::real[]',
+                        'elem_count_histogram', s.elem_count_histogram)
+        END ||
+        CASE
+            WHEN s.range_bounds_histogram IS NULL THEN ''
+            ELSE format(', %L, %L::text',
+                        'range_bounds_histogram', s.range_bounds_histogram)
+        END ||
+        CASE
+            WHEN s.range_empty_frac IS NULL THEN ''
+            ELSE format(', %L, %L::real, %L, %L::text',
+                        'range_empty_frac', s.range_empty_frac,
+                        'range_length_histogram', s.range_length_histogram)
+        END)
+FROM pg_catalog.pg_stats AS s
+WHERE s.schemaname = 'stats_export_import'
+AND s.tablename IN ('test', 'is_odd')
+\gexec
+
+-- restore ECHO to original value
+\set ECHO :orig_ECHO
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
+
 DROP SCHEMA stats_export_import CASCADE;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 5115a97ac2..eda608bad0 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29922,6 +29922,215 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          </para>
        </entry>
       </row>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_restore_relation_stats</primary>
+        </indexterm>
+        <function>pg_restore_relation_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>version</parameter> <type>integer</type>,
+         <literal>VARIADIC</literal> <parameter>stats</parameter>
+         <type>"any"</type> )
+        <returnvalue>boolean</returnvalue>
+        </para>
+        <para>
+         This is a variadic wrapper for
+         <function>pg_set_relation_stats</function>, meant for use by
+         <application>pg_dump</application> so as to be future-proof if the
+         signature of <function>pg_set_relation_stats</function> were to change
+         in a future version. The parameter <parameter>stats</parameter> must
+         contain a list of elements organized in key-value pairs. The first
+         element of each pair must be of type <type>text</type> and must match
+         the name of a parameter of <function>pg_set_relation_stats</function>.
+         The second element of each pair must be a value of datatype
+         coresponding to the parameter named in the first element. If the first
+         element of the pair does not name a parameter from 
+         <function>pg_set_relation_stats</function>, or of the datatype of the
+         second element does not correspond to that parameter, then the elements
+         will be rejected with a warning, and the function will continue to process
+         the request, if possible.
+        </para>
+       </entry>
+      </row>
+      <!--
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_restore_attribute_stats</primary>
+        </indexterm>
+        <function>pg_restore_attribute_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>
+         , <parameter>attname</parameter> <type>name</type>
+         , <parameter>inherited</parameter> <type>boolean</type>
+         , <parameter>version</parameter> <type>integer</type>
+         , <literal>VARIADIC</literal> <parameter>stats</parameter> <type>"any"</type> )
+        <returnvalue>boolean</returnvalue>
+        </para>
+        <para>
+         This is a variadic wrapper for
+         <function>pg_set_attribute_stats</function>, meant for use by
+         <application>pg_dump</application> so as to be future-proof if the
+         signature of <function>pg_set_attribute_stats</function> were to change
+         in a future version. The parameter <parameter>stats</parameter> must
+         contain a list of elements organized in key-value pairs. The first
+         element of each pair must be of type <type>text</type> and must match
+         the name of a parameter of <function>pg_set_attribute_stats</function>.
+         The second element of each pair must be a value of datatype
+         coresponding to the parameter named in the first element. If the first
+         element of the pair does not name a parameter from 
+         <function>pg_set_attribute_stats</function>, or of the datatype of the
+         second element does not correspond to that parameter, then the elements
+         will be rejected with a warning, and the function will continue to process
+         the request, if possible.
+        </para>
+       <variablelist>
+        <varlistentry>
+         <term><literal>null_frac</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>avg_width</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>integer</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>n_distinct</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_vals</literal></term>
+         <listitem>
+          <para>
+          <literal>most_common_vals</literal>
+           Must be followed by a parameter of type <type>text</type>. If this
+           parameter is specified then <literal>most_common_freqs</literal>
+           must also be specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_freqs</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of <type>real[]</type>, and can only
+           be specified if <literal>most_common_vals</literal> is also specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>histogram_bounds</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>correlation</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_elems</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>. If this
+           parameter is specified then <literal>most_common_elem_freqs</literal>
+           must also be specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_elem_freqs</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real[]</type>, and can
+           only be specified if <literal>most_common_elems</literal> is also
+           specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>elem_count_histogram</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real[]</type>.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>range_length_histogram</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>. If this
+           parameter is specified then <literal>range_empty_frac</literal>
+           must also be specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>range_empty_frac</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>, and can
+           only be specified if <literal>range_length_histogram</literal>
+           is also specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>range_bounds_histogram</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>
+          </para>
+         </listitem>
+        </varlistentry>
+       </variablelist>
+       <para>
+        Any other parameter names given must also have a value pair, but will emit
+        a warning and thereafter be ignored.
+       </para>
+       <para>
+        The parameters will return true if stats were updated and false if not.
+       </para>
+       <para>
+        This function mimics the behavior of <command>ANALYZE</command> in its
+        effects on the values in <structname>pg_statistic</structname>, except
+        that the values are supplied as parameters rather than derived from
+        table sampling.
+       </para>
+       <para>
+        The purpose of this function is to apply statistics values in an
+        upgrade situation that are "good enough" for system operation until
+        they are replaced by the next <command>ANALYZE</command>, usually via
+        <command>autovacuum</command>.
+       </para>
+       <para>
+        The caller must either be the owner of the relation, or have superuser
+        privileges.
+       </para>
+       </entry>
+      </row>
+      -->
      </tbody>
     </tgroup>
    </table>
-- 
2.45.2

v24-0002-Create-pg_set_attribute_stats-function.patchtext/x-patch; charset=US-ASCII; name=v24-0002-Create-pg_set_attribute_stats-function.patchDownload
From 1f3b450f875debc29ad5840732bb28c1d3b0ed95 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 17 Jul 2024 23:23:23 -0400
Subject: [PATCH v24 2/5] Create pg_set_attribute_stats function.

The function pg_set_attribute_stats is used modify attribute statistics,
allowing the user to inflate chose favorable histograms, inflate the
frequency of certain values, etc, to see what those changes will evoke
from the query planner.

The function takes an oid to identify the relation of the attribute, the
name of the attribute, a boolean flag to indicate if these are inherited
stats or not, and a version flag to indicate if the function should
treat the values given as if they came from an older version of
postgresql. The remaining parameters correspond to statsitics attributes
found in the pg_stats view.

If successful, the entire pg_statistic row is overwritten. Any parameter
values omitted or directly set to NULL mean that that particular type of
statistic will not exist in the new pg_statistic row. There is currently
no way to set just a few statistics in the row and leave others as-is.
This is partly due to the complexity of expressing such an action, and
partly because it would require a complicated reorganization of the
existing arrays of statkinds, and might potentially overflow those
arrays.

While the function does not attempt to validate the statistics given,
certain data errors make rendering certain statistics impossible, and
thus those data errors will generate a warning, and the function will
ignore that statistic and will continue with the operation if the
statistic was not required.

Examples:

- Some statistics kinds come in pairs. For example, the mcv stat
  consists of two parameters: most_common_vals and most_common_freqs,
  they must both be present in order to complete the mcv stat. If one
  is given but not the other, the

- Multi-value statistics such as most_common_elems do not allow for any
  elements within the array provided to be NULL, and any array given
  with NULLs in it will be rejected, which would also cause the
  corresponding -freqs parameter to be rejected, thus rejecting the
  whole stat-kind, but not otherwise affecting other parameters
  provided.

If at least one statistic was written, the pg_statistic row will be
modified and the function will return true. A return value of false
means that the pre-existing pg_statistic row (if any) remains untouched.

The statistics imported by pg_set_attribute_stats are imported
transactionally like any other operation. I mention this only because
the previous function pg_set_relation_stats, does non-transactional
in-place updates (for bloat reasons and because that is how ANALYZE does
it).
---
 src/include/catalog/pg_proc.dat               |   9 +-
 src/include/statistics/statistics.h           |   1 +
 src/backend/catalog/system_functions.sql      |  23 +
 src/backend/statistics/statistics.c           | 976 +++++++++++++++++-
 .../regress/expected/stats_export_import.out  | 783 +++++++++++++-
 src/test/regress/sql/stats_export_import.sql  | 611 +++++++++++
 doc/src/sgml/func.sgml                        |  77 ++
 7 files changed, 2477 insertions(+), 3 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 468fe6549c..5e553939b7 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12215,5 +12215,12 @@
   proparallel => 'u', prorettype => 'bool',
   proargtypes => 'oid int4 int4 float4 int4',
   proargnames => '{relation,version,relpages,reltuples,relallvisible}',
-  prosrc => 'pg_set_relation_stats' }
+  prosrc => 'pg_set_relation_stats' },
+{ oid => '8049',
+  descr => 'set statistics on attribute',
+  proname => 'pg_set_attribute_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'oid name bool int4 float4 int4 float4 text _float4 text float4 text _float4 _float4 text float4 text',
+  proargnames => '{relation,attname,inherited,version,null_frac,avg_width,n_distinct,most_common_vals,most_common_freqs,histogram_bounds,correlation,most_common_elems,most_common_elem_freqs,elem_count_histogram,range_length_histogram,range_empty_frac,range_bounds_histogram}',
+  prosrc => 'pg_set_attribute_stats' }
 ]
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 02e9ad024e..1dddf96576 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -128,4 +128,5 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
 extern Datum pg_set_relation_stats(PG_FUNCTION_ARGS);
+extern Datum pg_set_attribute_stats(PG_FUNCTION_ARGS);
 #endif							/* STATISTICS_H */
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index ae099e328c..e0e9a66aa3 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -636,6 +636,29 @@ LANGUAGE INTERNAL
 CALLED ON NULL INPUT VOLATILE PARALLEL SAFE
 AS 'pg_stat_reset_slru';
 
+CREATE OR REPLACE FUNCTION
+  pg_set_attribute_stats(relation oid,
+                         attname name,
+                         inherited bool,
+                         version integer,
+                         null_frac real,
+                         avg_width integer,
+                         n_distinct real,
+                         most_common_vals text DEFAULT NULL,
+                         most_common_freqs real[] DEFAULT NULL,
+                         histogram_bounds text DEFAULT NULL,
+                         correlation real DEFAULT NULL,
+                         most_common_elems text DEFAULT NULL,
+                         most_common_elem_freqs real[] DEFAULT NULL,
+                         elem_count_histogram real[] DEFAULT NULL,
+                         range_length_histogram text DEFAULT NULL,
+                         range_empty_frac real DEFAULT NULL,
+                         range_bounds_histogram text DEFAULT NULL)
+RETURNS boolean
+LANGUAGE INTERNAL
+CALLED ON NULL INPUT VOLATILE
+AS 'pg_set_attribute_stats';
+
 --
 -- The default permissions for functions mean that anyone can execute them.
 -- A number of functions shouldn't be executable by just anyone, but rather
diff --git a/src/backend/statistics/statistics.c b/src/backend/statistics/statistics.c
index 27fcdb4717..2a43da7c41 100644
--- a/src/backend/statistics/statistics.c
+++ b/src/backend/statistics/statistics.c
@@ -42,12 +42,28 @@
 #include "utils/typcache.h"
 
 /*
- * Names of parameters found in the function pg_set_relation_stats.
+ * Names of parameters found in the functions pg_set_relation_stats and
+ * pg_set_attribute_stats
  */
 const char *relation_name = "relation";
 const char *relpages_name = "relpages";
 const char *reltuples_name = "reltuples";
 const char *relallvisible_name = "relallvisible";
+const char *attname_name = "attname";
+const char *inherited_name = "inherited";
+const char *null_frac_name = "null_frac";
+const char *avg_width_name = "avg_width";
+const char *n_distinct_name = "n_distinct";
+const char *mc_vals_name = "most_common_vals";
+const char *mc_freqs_name = "most_common_freqs";
+const char *histogram_bounds_name = "histogram_bounds";
+const char *correlation_name = "correlation";
+const char *mc_elems_name = "most_common_elems";
+const char *mc_elem_freqs_name = "most_common_elem_freqs";
+const char *elem_count_hist_name = "elem_count_histogram";
+const char *range_length_hist_name = "range_length_histogram";
+const char *range_empty_frac_name = "range_empty_frac";
+const char *range_bounds_hist_name = "range_bounds_histogram";
 
 /*
  * A role has privileges to set statistics on the relation if any of the
@@ -229,3 +245,961 @@ pg_set_relation_stats(PG_FUNCTION_ARGS)
 
 	PG_RETURN_BOOL(true);
 }
+
+/*
+ * Test if the type is a scalar for MCELEM purposes
+ */
+static bool
+type_is_scalar(Oid typid)
+{
+	HeapTuple	tp;
+	bool		result = false;
+
+	tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+	if (HeapTupleIsValid(tp))
+	{
+		Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+
+		result = (!OidIsValid(typtup->typanalyze));
+		ReleaseSysCache(tp);
+	}
+	return result;
+}
+
+/*
+ * If this relation is an index and that index has expressions in it, and
+ * the attnum specified is known to be an expression, then we must walk
+ * the list attributes up to the specified attnum to get the right
+ * expression.
+ */
+static Node *
+get_attr_expr(Relation rel, int attnum)
+{
+	if ((rel->rd_rel->relkind == RELKIND_INDEX
+		 || (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+		&& (rel->rd_indexprs != NIL)
+		&& (rel->rd_index->indkey.values[attnum - 1] == 0))
+	{
+		ListCell   *indexpr_item = list_head(rel->rd_indexprs);
+
+		for (int i = 0; i < attnum - 1; i++)
+			if (rel->rd_index->indkey.values[i] == 0)
+				indexpr_item = lnext(rel->rd_indexprs, indexpr_item);
+
+		if (indexpr_item == NULL)	/* shouldn't happen */
+			elog(ERROR, "too few entries in indexprs list");
+
+		return (Node *) lfirst(indexpr_item);
+	}
+	return NULL;
+}
+
+/*
+ * Fetch datatype information, this is needed to derive the proper staopN
+ * and stacollN values.
+ *
+ */
+static TypeCacheEntry *
+get_attr_stat_type(Relation rel, Name attname,
+				   int16 *attnum, int32 *typmod, Oid *typcoll)
+{
+	Oid			relid = RelationGetRelid(rel);
+	Form_pg_attribute attr;
+	HeapTuple	atup;
+	Oid			typid;
+	Node	   *expr;
+
+	atup = SearchSysCache2(ATTNAME, ObjectIdGetDatum(relid),
+						   NameGetDatum(attname));
+
+	/* Attribute not found */
+	if (!HeapTupleIsValid(atup))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s has no attname %s",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+		return NULL;
+	}
+
+	attr = (Form_pg_attribute) GETSTRUCT(atup);
+	if (attr->attisdropped)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s attname %s is dropped",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+		return NULL;
+	}
+	*attnum = attr->attnum;
+
+	expr = get_attr_expr(rel, attr->attnum);
+
+	if (expr == NULL)
+	{
+		/* regular attribute */
+		typid = attr->atttypid;
+		*typmod = attr->atttypmod;
+		*typcoll = attr->attcollation;
+	}
+	else
+	{
+		typid = exprType(expr);
+		*typmod = exprTypmod(expr);
+
+		/*
+		 * If a collation has been specified for the index column, use that in
+		 * preference to anything else; but if not, fall back to whatever we
+		 * can get from the expression.
+		 */
+		if (OidIsValid(attr->attcollation))
+			*typcoll = attr->attcollation;
+		else
+			*typcoll = exprCollation(expr);
+	}
+	ReleaseSysCache(atup);
+
+	/* if it's a multirange, step down to the range type */
+	if (type_is_multirange(typid))
+		typid = get_multirange_range(typid);
+
+	return lookup_type_cache(typid,
+							 TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR);
+}
+
+/*
+ * Perform the cast of a known TextDatum into the type specified.
+ *
+ * If no errors are found, ok is set to true. Otherwise, set ok
+ * to false, capture the error found, and re-throw at warning level.
+ */
+static Datum
+cast_stavalues(FmgrInfo *flinfo, Datum d, Oid typid, int32 typmod, bool *ok)
+{
+	LOCAL_FCINFO(fcinfo, 8);
+	char	   *s;
+	Datum		result;
+	ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+	escontext.details_wanted = true;
+
+	s = TextDatumGetCString(d);
+
+	InitFunctionCallInfoData(*fcinfo, flinfo, 3, InvalidOid,
+							 (Node *) &escontext, NULL);
+
+	fcinfo->args[0].value = CStringGetDatum(s);
+	fcinfo->args[0].isnull = false;
+	fcinfo->args[1].value = ObjectIdGetDatum(typid);
+	fcinfo->args[1].isnull = false;
+	fcinfo->args[2].value = Int32GetDatum(typmod);
+	fcinfo->args[2].isnull = false;
+
+	result = FunctionCallInvoke(fcinfo);
+
+	if (SOFT_ERROR_OCCURRED(&escontext))
+	{
+		escontext.error_data->elevel = WARNING;
+		ThrowErrorData(escontext.error_data);
+		*ok = false;
+	}
+	else
+		*ok = true;
+
+	pfree(s);
+
+	return result;
+}
+
+
+/*
+ * Check array for any NULLs, and optionally for one-dimensionality.
+ *
+ * Report any failures as warnings.
+ */
+static bool
+array_check(Datum datum, int one_dim, const char *statname)
+{
+	ArrayType  *arr = DatumGetArrayTypeP(datum);
+	int16		elmlen;
+	char		elmalign;
+	bool		elembyval;
+	Datum	   *values;
+	bool	   *nulls;
+	int			nelems;
+
+	if (one_dim && (arr->ndim != 1))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be a multidimensional array", statname)));
+		return false;
+	}
+
+	get_typlenbyvalalign(ARR_ELEMTYPE(arr), &elmlen, &elembyval, &elmalign);
+
+	deconstruct_array(arr, ARR_ELEMTYPE(arr), elmlen, elembyval, elmalign,
+					  &values, &nulls, &nelems);
+
+	for (int i = 0; i < nelems; i++)
+		if (nulls[i])
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s array cannot contain NULL values", statname)));
+			return false;
+		}
+	return true;
+}
+
+/*
+ * Update the pg_statistic record.
+ */
+static void
+update_pg_statistic(Datum values[], bool nulls[])
+{
+	Relation	sd = table_open(StatisticRelationId, RowExclusiveLock);
+	CatalogIndexState indstate = CatalogOpenIndexes(sd);
+	HeapTuple	oldtup;
+	HeapTuple	stup;
+
+	/* Is there already a pg_statistic tuple for this attribute? */
+	oldtup = SearchSysCache3(STATRELATTINH,
+							 values[Anum_pg_statistic_starelid - 1],
+							 values[Anum_pg_statistic_staattnum - 1],
+							 values[Anum_pg_statistic_stainherit - 1]);
+
+	if (HeapTupleIsValid(oldtup))
+	{
+		/* Yes, replace it */
+		bool		replaces[Natts_pg_statistic];
+
+		for (int i = 0; i < Natts_pg_statistic; i++)
+			replaces[i] = true;
+
+		stup = heap_modify_tuple(oldtup, RelationGetDescr(sd),
+								 values, nulls, replaces);
+		ReleaseSysCache(oldtup);
+		CatalogTupleUpdateWithInfo(sd, &stup->t_self, stup, indstate);
+	}
+	else
+	{
+		/* No, insert new tuple */
+		stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
+		CatalogTupleInsertWithInfo(sd, stup, indstate);
+	}
+
+	heap_freetuple(stup);
+	CatalogCloseIndexes(indstate);
+	table_close(sd, NoLock);
+}
+
+
+
+/*
+ * Import statistics for a given relation attribute.
+ *
+ * This will insert/replace a row in pg_statistic for the given relation and
+ * attribute name.
+ *
+ * The function takes input parameters that correspond to columns in the view
+ * pg_stats.
+ *
+ * Of those, the columns attname, inherited, null_frac, avg_width, and
+ * n_distinct all correspond to NOT NULL columns in pg_statistic. These
+ * parameters have no default value and passing NULL to them will result
+ * in an error.
+ *
+ * If there is no attribute with a matching attname in the relation, the
+ * function will raise an error. Likewise for setting inherited statistics
+ * on a table that is not partitioned.
+ *
+ * The remaining parameters all belong to a specific stakind. Some stakinds
+ * have multiple parameters, and in those cases both parameters must be
+ * NOT NULL or both NULL, otherwise an error will be raised.
+ *
+ * Omitting a parameter or explicitly passing NULL means that that particular
+ * stakind is not associated with the attribute.
+ *
+ * Parameters that are NOT NULL will be inspected for consistency checks,
+ * any of which can raise an error.
+ *
+ * Parameters corresponding to ANYARRAY columns are instead passed in as text
+ * values, which is a valid input string for an array of the type or element
+ * type of the attribute. Any error generated by the array_in() function will
+ * in turn fail the function.
+ */
+Datum
+pg_set_attribute_stats(PG_FUNCTION_ARGS)
+{
+	Datum		relation_datum = PG_GETARG_DATUM(0);
+	bool		relation_isnull = PG_ARGISNULL(0);
+	Oid			relation = PG_GETARG_OID(0);
+
+	Name		attname = PG_GETARG_NAME(1);
+	bool		attname_isnull = PG_ARGISNULL(1);
+
+	Datum		inherited_datum = PG_GETARG_DATUM(2);
+	bool		inherited_isnull = PG_ARGISNULL(2);
+
+	int			version = PG_GETARG_INT32(3);
+	int			version_isnull = PG_ARGISNULL(3);
+
+	Datum		null_frac_datum = PG_GETARG_DATUM(4);
+	bool		null_frac_isnull = PG_ARGISNULL(4);
+
+	Datum		avg_width_datum = PG_GETARG_DATUM(5);
+	bool		avg_width_isnull = PG_ARGISNULL(5);
+
+	Datum		n_distinct_datum = PG_GETARG_DATUM(6);
+	bool		n_distinct_isnull = PG_ARGISNULL(6);
+
+	Datum		mc_vals_datum = PG_GETARG_DATUM(7);
+	bool		mc_vals_isnull = PG_ARGISNULL(7);
+
+	Datum		mc_freqs_datum = PG_GETARG_DATUM(8);
+	bool		mc_freqs_isnull = PG_ARGISNULL(8);
+
+	Datum		histogram_bounds_datum = PG_GETARG_DATUM(9);
+	bool		histogram_bounds_isnull = PG_ARGISNULL(9);
+
+	Datum		correlation_datum = PG_GETARG_DATUM(10);
+	bool		correlation_isnull = PG_ARGISNULL(10);
+
+	Datum		mc_elems_datum = PG_GETARG_DATUM(11);
+	bool		mc_elems_isnull = PG_ARGISNULL(11);
+
+	Datum		mc_elem_freqs_datum = PG_GETARG_DATUM(12);
+	bool		mc_elem_freqs_isnull = PG_ARGISNULL(12);
+
+	Datum		elem_count_hist_datum = PG_GETARG_DATUM(13);
+	bool		elem_count_hist_isnull = PG_ARGISNULL(13);
+
+	Datum		range_length_hist_datum = PG_GETARG_DATUM(14);
+	bool		range_length_hist_isnull = PG_ARGISNULL(14);
+
+	Datum		range_empty_frac_datum = PG_GETARG_DATUM(15);
+	bool		range_empty_frac_isnull = PG_ARGISNULL(15);
+
+	Datum		range_bounds_hist_datum = PG_GETARG_DATUM(16);
+	bool		range_bounds_hist_isnull = PG_ARGISNULL(16);
+
+	Relation	rel;
+
+	TypeCacheEntry *typcache;
+	TypeCacheEntry *elemtypcache = NULL;
+
+	int16		attnum;
+	int32		typmod;
+	Oid			typcoll;
+
+	FmgrInfo	finfo;
+
+	int			stakind_count;
+
+	bool		has_mcv = false;
+	bool		has_mc_elems = false;
+	bool		has_rl_hist = false;
+
+	/*
+	 * The statkind index, we have only STATISTIC_NUM_SLOTS to hold these stats
+	 */
+	int			stakindidx = 0;
+
+	Datum		values[Natts_pg_statistic];
+	bool		nulls[Natts_pg_statistic];
+
+	/*
+	 * Initialize output tuple.
+	 *
+	 * All non-repeating attributes should be NOT NULL. Only values for unused
+	 * statistics slots, and certain stakind-specific values for stanumbersN
+	 * and stavaluesN will ever be set NULL.
+	 */
+	for (int i = 0; i < Natts_pg_statistic; i++)
+	{
+		values[i] = (Datum) 0;
+		nulls[i] = false;
+	}
+
+	/*
+	 * Some parameters are "required" in that nothing can happen if any of
+	 * them are NULL.
+	 */
+	if (relation_isnull)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", relation_name)));
+		PG_RETURN_BOOL(false);
+	}
+
+	if (attname_isnull)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", attname_name)));
+		PG_RETURN_BOOL(false);
+	}
+
+	if (inherited_isnull)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", inherited_name)));
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * NULL version means assume current server version
+	 */
+	if (version_isnull)
+		version = PG_VERSION_NUM;
+	else
+	{
+		if (version < 90200)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Cannot export statistics prior to version 9.2")));
+			PG_RETURN_BOOL(false);
+		}
+	}
+
+	if (null_frac_isnull)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", null_frac_name)));
+		PG_RETURN_BOOL(false);
+	}
+
+	if (avg_width_isnull)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", avg_width_name)));
+		PG_RETURN_BOOL(false);
+	}
+
+	if (n_distinct_isnull)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", n_distinct_name)));
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * Params most_common_vals and most_common_freqs are linked
+	 */
+	if ((!mc_vals_isnull) && (!mc_freqs_isnull))
+		has_mcv = true;
+	else if (mc_vals_isnull != mc_freqs_isnull)
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL when %s is NOT NULL",
+						mc_vals_isnull ? mc_vals_name : mc_freqs_name,
+						!mc_vals_isnull ? mc_vals_name : mc_freqs_name)));
+
+	/*
+	 * Params most_common_elems and most_common_elem_freqs are linked
+	 */
+	if ((!mc_elems_isnull) && (!mc_elem_freqs_isnull))
+		has_mc_elems = true;
+	else if (mc_elems_isnull != mc_elem_freqs_isnull)
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL when %s is NOT NULL",
+						mc_elems_isnull ? mc_elems_name : mc_elem_freqs_name,
+						!mc_elems_isnull ? mc_elems_name : mc_elem_freqs_name)));
+
+	/*
+	 * Params range_empty_frac and range_length_histogram are linked
+	 */
+	if ((!range_length_hist_isnull) && (!range_empty_frac_isnull))
+		has_rl_hist = true;
+	else if (range_length_hist_isnull != range_empty_frac_isnull)
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL when %s is NOT NULL",
+						range_length_hist_isnull ?
+						range_length_hist_name : range_empty_frac_name,
+						!range_length_hist_isnull ?
+						range_length_hist_name : range_empty_frac_name)));
+
+	/*
+	 * If a caller specifies more stakind-stats than we have slots to store
+	 * them, reject them all
+	 */
+	stakind_count = (int) has_mcv + (int) has_mc_elems + (int) has_rl_hist +
+		(int) !histogram_bounds_isnull + (int) !correlation_isnull +
+		(int) !elem_count_hist_isnull + (int) !range_bounds_hist_isnull;
+
+	if (stakind_count > STATISTIC_NUM_SLOTS)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("imported statistics must have a maximum of %d slots "
+						"but %d given",
+						STATISTIC_NUM_SLOTS, stakind_count)));
+		PG_RETURN_BOOL(false);
+	}
+
+	rel = try_relation_open(relation, ShareUpdateExclusiveLock);
+
+	if (rel == NULL)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Parameter relation OID %u is invalid", relation)));
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * Many of the values that are set for a particular stakind are entirely
+	 * derived from the attribute itself, or it's expression.
+	 */
+	typcache = get_attr_stat_type(rel, attname, &attnum, &typmod, &typcoll);
+	if (typcache == NULL)
+	{
+		relation_close(rel, NoLock);
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("unexpected typecache error")));
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * Derive element type if we have stat kinds that need it.
+	 *
+	 * This duplicates some type-specific logic found in various typanalyze
+	 * functions which are called from vacuum's examine_attribute(), but using
+	 * that directly has proven awkward.
+	 */
+	if (has_mc_elems || !elem_count_hist_isnull)
+	{
+		Oid			elemtypid;
+
+		if (typcache->type_id == TSVECTOROID)
+		{
+			/*
+			 * tsvectors always have a text oid base type and default
+			 * collation
+			 */
+			elemtypid = TEXTOID;
+			typcoll = DEFAULT_COLLATION_OID;
+		}
+		else if (typcache->typtype == TYPTYPE_RANGE)
+			elemtypid = get_range_subtype(typcache->type_id);
+		else
+			elemtypid = get_base_element_type(typcache->type_id);
+
+		/* not finding a basetype means we already had it */
+		if (elemtypid == InvalidOid)
+			elemtypid = typcache->type_id;
+
+		/* The stats need the eq_opr, any validation would need the lt_opr */
+		elemtypcache = lookup_type_cache(elemtypid, TYPECACHE_EQ_OPR);
+		if (elemtypcache == NULL)
+		{
+			/* warn and ignore any stats that can't be fulfilled */
+			has_mc_elems = false;
+
+			if (!mc_elems_isnull)
+			{
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s cannot accept %s stats, ignored",
+								NameStr(*attname),
+								mc_elems_name)));
+				mc_elems_isnull = true;
+			}
+
+			if (!mc_elem_freqs_isnull)
+			{
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s cannot accept %s stats, ignored",
+								NameStr(*attname),
+								mc_elem_freqs_name)));
+				mc_elem_freqs_isnull = true;
+			}
+
+			if (!elem_count_hist_isnull)
+			{
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s cannot accept %s stats, ignored",
+								NameStr(*attname),
+								elem_count_hist_name)));
+				elem_count_hist_isnull = true;
+			}
+		}
+	}
+
+	/*
+	 * histogram_bounds and correlation must have a type < operator
+	 */
+	if (typcache->lt_opr == InvalidOid)
+	{
+		if (!histogram_bounds_isnull)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+					 errmsg("Relation %s attname %s cannot "
+							"have stats of type %s, ignored.",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							histogram_bounds_name)));
+			histogram_bounds_isnull = true;
+		}
+
+		if (!correlation_isnull)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+					 errmsg("Relation %s attname %s cannot "
+							"have stats of type %s, ignored.",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							correlation_name)));
+			correlation_isnull = true;
+		}
+	}
+
+	/*
+	 * Scalar types can't have most_common_elems, most_common_elem_freqs, or
+	 * element_count_histogram
+	 */
+	if (type_is_scalar(typcache->type_id))
+	{
+		/* warn and ignore any stats that can't be fulfilled */
+		has_mc_elems = false;
+
+		if (!mc_elems_isnull)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+					 errmsg("Relation %s attname %s is a scalar type, "
+							"cannot have stats of type %s, ignored",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							mc_elems_name)));
+			mc_elems_isnull = true;
+		}
+
+		if (!mc_elem_freqs_isnull)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+					 errmsg("Relation %s attname %s is a scalar type, "
+							"cannot have stats of type %s, ignored",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							mc_elem_freqs_name)));
+			mc_elem_freqs_isnull = true;
+		}
+
+		if (!elem_count_hist_isnull)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+					 errmsg("Relation %s attname %s is a scalar type, "
+							"cannot have stats of type %s, ignored",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							elem_count_hist_name)));
+			elem_count_hist_isnull = true;
+		}
+	}
+
+	/* Only range types can have range stats */
+	if ((typcache->typtype != TYPTYPE_RANGE) &&
+		(typcache->typtype != TYPTYPE_MULTIRANGE))
+	{
+		/* warn and ignore any stats that can't be fulfilled */
+		has_rl_hist = false;
+
+		if (!range_length_hist_isnull)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+					 errmsg("Relation %s attname %s is not a range type, "
+							"cannot have stats of type %s",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							range_length_hist_name)));
+			range_length_hist_isnull = true;
+		}
+
+		if (!range_empty_frac_isnull)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+					 errmsg("Relation %s attname %s is not a range type, "
+							"cannot have stats of type %s",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							range_empty_frac_name)));
+			range_empty_frac_isnull = true;
+		}
+		if (!range_bounds_hist_isnull)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+					 errmsg("Relation %s attname %s is not a range type, "
+							"cannot have stats of type %s",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							range_bounds_hist_name)));
+			range_bounds_hist_isnull = true;
+		}
+	}
+
+	if (!can_modify_relation(rel))
+	{
+		relation_close(rel, NoLock);
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						RelationGetRelationName(rel))));
+		PG_RETURN_BOOL(false);
+	}
+
+	/* Populate pg_statistic tuple */
+	values[Anum_pg_statistic_starelid - 1] = relation_datum;
+	values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(attnum);
+	values[Anum_pg_statistic_stainherit - 1] = inherited_datum;
+	values[Anum_pg_statistic_stanullfrac - 1] = null_frac_datum;
+	values[Anum_pg_statistic_stawidth - 1] = avg_width_datum;
+	values[Anum_pg_statistic_stadistinct - 1] = n_distinct_datum;
+
+	fmgr_info(F_ARRAY_IN, &finfo);
+
+	/*
+	 * STATISTIC_KIND_MCV
+	 *
+	 * most_common_vals: ANYARRAY::text most_common_freqs: real[]
+	 */
+	if (has_mcv)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_MCV);
+		Datum		staop = ObjectIdGetDatum(typcache->eq_opr);
+		Datum		stacoll = ObjectIdGetDatum(typcoll);
+		bool		converted = false;
+		Datum		stanumbers = mc_freqs_datum;
+		Datum		stavalues = cast_stavalues(&finfo, mc_vals_datum,
+											   typcache->type_id, typmod,
+											   &converted);
+
+		if (converted &&
+			array_check(stavalues, false, mc_vals_name) &&
+			array_check(stanumbers, true, mc_freqs_name))
+		{
+			values[Anum_pg_statistic_stakind1 - 1 + stakindidx] = stakind;
+			values[Anum_pg_statistic_staop1 - 1 + stakindidx] = staop;
+			values[Anum_pg_statistic_stacoll1 - 1 + stakindidx] = stacoll;
+			values[Anum_pg_statistic_stanumbers1 - 1 + stakindidx] = stanumbers;
+			values[Anum_pg_statistic_stavalues1 - 1 + stakindidx] = stavalues;
+
+			stakindidx++;
+		}
+	}
+
+
+	/*
+	 * STATISTIC_KIND_HISTOGRAM
+	 *
+	 * histogram_bounds: ANYARRAY::text
+	 */
+	if (!histogram_bounds_isnull)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_HISTOGRAM);
+		Datum		staop = ObjectIdGetDatum(typcache->lt_opr);
+		Datum		stacoll = ObjectIdGetDatum(typcoll);
+		Datum		stavalues;
+		bool		converted = false;
+
+		stavalues = cast_stavalues(&finfo, histogram_bounds_datum,
+								   typcache->type_id, typmod, &converted);
+
+		if (converted && array_check(stavalues, false, histogram_bounds_name))
+		{
+			values[Anum_pg_statistic_stakind1 - 1 + stakindidx] = stakind;
+			values[Anum_pg_statistic_staop1 - 1 + stakindidx] = staop;
+			values[Anum_pg_statistic_stacoll1 - 1 + stakindidx] = stacoll;
+			nulls[Anum_pg_statistic_stanumbers1 - 1 + stakindidx] = true;
+			values[Anum_pg_statistic_stavalues1 - 1 + stakindidx] = stavalues;
+
+			stakindidx++;
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_CORRELATION
+	 *
+	 * correlation: real
+	 */
+	if (!correlation_isnull)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_CORRELATION);
+		Datum		staop = ObjectIdGetDatum(typcache->lt_opr);
+		Datum		stacoll = ObjectIdGetDatum(typcoll);
+		Datum		elems[] = {correlation_datum};
+		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+		Datum		stanumbers = PointerGetDatum(arry);
+
+		values[Anum_pg_statistic_stakind1 - 1 + stakindidx] = stakind;
+		values[Anum_pg_statistic_staop1 - 1 + stakindidx] = staop;
+		values[Anum_pg_statistic_stacoll1 - 1 + stakindidx] = stacoll;
+		values[Anum_pg_statistic_stanumbers1 - 1 + stakindidx] = stanumbers;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + stakindidx] = true;
+
+		stakindidx++;
+	}
+
+	/*
+	 * STATISTIC_KIND_MCELEM
+	 *
+	 * most_common_elem_freqs: real[]
+	 *
+	 * most_common_elems     : ANYARRAY::text
+	 */
+	if (has_mc_elems)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_MCELEM);
+		Datum		staop = ObjectIdGetDatum(elemtypcache->eq_opr);
+		Datum		stacoll = ObjectIdGetDatum(typcoll);
+		Datum		stanumbers = mc_elem_freqs_datum;
+		bool		converted = false;
+		Datum		stavalues;
+
+		stavalues = cast_stavalues(&finfo, mc_elems_datum,
+								   elemtypcache->type_id, typmod, &converted);
+
+		if (converted &&
+			array_check(stavalues, false, mc_elems_name) &&
+			array_check(stanumbers, true, mc_elem_freqs_name))
+		{
+			values[Anum_pg_statistic_stakind1 - 1 + stakindidx] = stakind;
+			values[Anum_pg_statistic_staop1 - 1 + stakindidx] = staop;
+			values[Anum_pg_statistic_stacoll1 - 1 + stakindidx] = stacoll;
+			values[Anum_pg_statistic_stanumbers1 - 1 + stakindidx] = stanumbers;
+			values[Anum_pg_statistic_stavalues1 - 1 + stakindidx] = stavalues;
+
+			stakindidx++;
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_DECHIST
+	 *
+	 * elem_count_histogram:	real[]
+	 */
+	if (!elem_count_hist_isnull)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_DECHIST);
+		Datum		staop = ObjectIdGetDatum(elemtypcache->eq_opr);
+		Datum		stacoll = ObjectIdGetDatum(typcoll);
+		Datum		stanumbers = elem_count_hist_datum;
+
+		values[Anum_pg_statistic_stakind1 - 1 + stakindidx] = stakind;
+		values[Anum_pg_statistic_staop1 - 1 + stakindidx] = staop;
+		values[Anum_pg_statistic_stacoll1 - 1 + stakindidx] = stacoll;
+		values[Anum_pg_statistic_stanumbers1 - 1 + stakindidx] = stanumbers;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + stakindidx] = true;
+
+		stakindidx++;
+	}
+
+	/*
+	 * STATISTIC_KIND_BOUNDS_HISTOGRAM
+	 *
+	 * range_bounds_histogram: ANYARRAY::text
+	 *
+	 * This stakind appears before STATISTIC_KIND_BOUNDS_HISTOGRAM even though
+	 * it is numerically greater, and all other stakinds appear in numerical
+	 * order. We duplicate this quirk to make before/after tests of
+	 * pg_statistic records easier.
+	 */
+	if (!range_bounds_hist_isnull)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_BOUNDS_HISTOGRAM);
+		Datum		staop = ObjectIdGetDatum(InvalidOid);
+		Datum		stacoll = ObjectIdGetDatum(InvalidOid);
+
+		bool		converted = false;
+		Datum		stavalues;
+
+		stavalues = cast_stavalues(&finfo, range_bounds_hist_datum,
+								   typcache->type_id, typmod, &converted);
+
+		if (converted &&
+			array_check(stavalues, false, "range_bounds_histogram"))
+		{
+			values[Anum_pg_statistic_stakind1 - 1 + stakindidx] = stakind;
+			values[Anum_pg_statistic_staop1 - 1 + stakindidx] = staop;
+			values[Anum_pg_statistic_stacoll1 - 1 + stakindidx] = stacoll;
+			nulls[Anum_pg_statistic_stanumbers1 - 1 + stakindidx] = true;
+			values[Anum_pg_statistic_stavalues1 - 1 + stakindidx] = stavalues;
+
+			stakindidx++;
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM
+	 *
+	 * range_empty_frac: real
+	 *
+	 * range_length_histogram:  double precision[]::text
+	 */
+	if (has_rl_hist)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM);
+		Datum		staop = ObjectIdGetDatum(Float8LessOperator);
+		Datum		stacoll = ObjectIdGetDatum(InvalidOid);
+
+		/* The anyarray is always a float8[] for this stakind */
+		Datum		elems[] = {range_empty_frac_datum};
+		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+		Datum		stanumbers = PointerGetDatum(arry);
+
+		bool		converted = false;
+		Datum		stavalues;
+
+		stavalues = cast_stavalues(&finfo, range_length_hist_datum, FLOAT8OID,
+								   0, &converted);
+
+		if (converted &&
+			array_check(stavalues, false, range_length_hist_name))
+		{
+			values[Anum_pg_statistic_staop1 - 1 + stakindidx] = staop;
+			values[Anum_pg_statistic_stakind1 - 1 + stakindidx] = stakind;
+			values[Anum_pg_statistic_stacoll1 - 1 + stakindidx] = stacoll;
+			values[Anum_pg_statistic_stanumbers1 - 1 + stakindidx] = stanumbers;
+			values[Anum_pg_statistic_stavalues1 - 1 + stakindidx] = stavalues;
+			stakindidx++;
+		}
+	}
+
+	/* fill in all remaining slots */
+	while (stakindidx < STATISTIC_NUM_SLOTS)
+	{
+		values[Anum_pg_statistic_stakind1 - 1 + stakindidx] = Int16GetDatum(0);
+		values[Anum_pg_statistic_staop1 - 1 + stakindidx] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_stacoll1 - 1 + stakindidx] = ObjectIdGetDatum(InvalidOid);
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + stakindidx] = true;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + stakindidx] = true;
+
+		stakindidx++;
+	}
+
+	update_pg_statistic(values, nulls);
+
+	relation_close(rel, NoLock);
+	PG_RETURN_BOOL(true);
+}
diff --git a/src/test/regress/expected/stats_export_import.out b/src/test/regress/expected/stats_export_import.out
index 9559f8d524..bc99bf3745 100644
--- a/src/test/regress/expected/stats_export_import.out
+++ b/src/test/regress/expected/stats_export_import.out
@@ -154,7 +154,788 @@ WHERE oid = 'stats_export_import.test'::regclass;
        19 |       402 |             6
 (1 row)
 
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => '0'::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+WARNING:  Parameter relation OID 0 is invalid
+ pg_set_attribute_stats 
+------------------------
+ f
+(1 row)
+
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => NULL::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+WARNING:  relation cannot be NULL
+ pg_set_attribute_stats 
+------------------------
+ f
+(1 row)
+
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => NULL::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+WARNING:  attname cannot be NULL
+ pg_set_attribute_stats 
+------------------------
+ f
+(1 row)
+
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => NULL::boolean,
+    version => 150000::integer,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+WARNING:  inherited cannot be NULL
+ pg_set_attribute_stats 
+------------------------
+ f
+(1 row)
+
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => NULL::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+WARNING:  null_frac cannot be NULL
+ pg_set_attribute_stats 
+------------------------
+ f
+(1 row)
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.1::real,
+    avg_width => NULL::integer,
+    n_distinct => 0.3::real);
+WARNING:  avg_width cannot be NULL
+ pg_set_attribute_stats 
+------------------------
+ f
+(1 row)
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => NULL::real);
+WARNING:  n_distinct cannot be NULL
+ pg_set_attribute_stats 
+------------------------
+ f
+(1 row)
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT stanullfrac, stawidth, stadistinct
+FROM pg_statistic
+WHERE starelid = 'stats_export_import.test'::regclass;
+ stanullfrac | stawidth | stadistinct 
+-------------+----------+-------------
+         0.1 |        2 |         0.3
+(1 row)
+
+-- warn: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_freqs => '{0.1,0.2,0.3}'::real[]
+    );
+WARNING:  most_common_vals cannot be NULL when most_common_freqs is NOT NULL
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{1,2,3}'::text
+    );
+WARNING:  most_common_freqs cannot be NULL when most_common_vals is NOT NULL
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2023-09-30,2024-10-31,3}'::text,
+    most_common_freqs => '{0.2,0.1}'::real[]
+    );
+WARNING:  invalid input syntax for type integer: "2023-09-30"
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warning: mcv cast failure
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,four,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+WARNING:  invalid input syntax for type integer: "four"
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: histogram elements null value
+-- this generates no warnings, but perhaps it should
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,NULL,3,4}'::text
+    );
+WARNING:  histogram_bounds array cannot contain NULL values
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,2,3,4}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 0.5::real);
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |         0.5 |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have mcelem
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{1,3}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+WARNING:  Relation test attname id is a scalar type, cannot have stats of type most_common_elems, ignored
+WARNING:  Relation test attname id is a scalar type, cannot have stats of type most_common_elem_freqs, ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,two}'::text
+    );
+WARNING:  most_common_elem_freqs cannot be NULL when most_common_elems is NOT NULL
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
+    );
+WARNING:  most_common_elems cannot be NULL when most_common_elem_freqs is NOT NULL
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: mcelem
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             | {one,three}       | {0.3,0.2,0.2,0.3,0}    |                      |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+WARNING:  Relation test attname id is a scalar type, cannot have stats of type elem_count_histogram, ignored
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+WARNING:  Relation test attname id is not a range type, cannot have stats of type range_length_histogram
+WARNING:  Relation test attname id is not a range type, cannot have stats of type range_empty_frac
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+WARNING:  range_empty_frac cannot be NULL when range_length_histogram is NOT NULL
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real
+    );
+WARNING:  range_length_histogram cannot be NULL when range_empty_frac is NOT NULL
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_export_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
+(1 row)
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+WARNING:  Relation test attname id is not a range type, cannot have stats of type range_bounds_histogram
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+     schemaname      | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
+---------------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
+ stats_export_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      |                        |                  | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+(1 row)
+
+-- warn: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[],
+    histogram_bounds => '{1,2,3,4}'::text,
+    correlation => 1.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    range_empty_frac => -0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+WARNING:  imported statistics must have a maximum of 5 slots but 6 given
+ pg_set_attribute_stats 
+------------------------
+ f
+(1 row)
+
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+   relname    | num_stats 
+--------------+-----------
+ is_odd       |         1
+ is_odd_clone |         1
+ test         |         5
+ test_clone   |         5
+(4 rows)
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
 DROP SCHEMA stats_export_import CASCADE;
-NOTICE:  drop cascades to 2 other objects
+NOTICE:  drop cascades to 3 other objects
 DETAIL:  drop cascades to type stats_export_import.complex_type
 drop cascades to table stats_export_import.test
+drop cascades to table stats_export_import.test_clone
diff --git a/src/test/regress/sql/stats_export_import.sql b/src/test/regress/sql/stats_export_import.sql
index 65d647830c..76a48e0f94 100644
--- a/src/test/regress/sql/stats_export_import.sql
+++ b/src/test/regress/sql/stats_export_import.sql
@@ -104,4 +104,615 @@ SELECT relpages, reltuples, relallvisible
 FROM pg_class
 WHERE oid = 'stats_export_import.test'::regclass;
 
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => '0'::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => NULL::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => NULL::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => NULL::boolean,
+    version => 150000::integer,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => NULL::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.1::real,
+    avg_width => NULL::integer,
+    n_distinct => 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => NULL::real);
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+SELECT stanullfrac, stawidth, stadistinct
+FROM pg_statistic
+WHERE starelid = 'stats_export_import.test'::regclass;
+
+-- warn: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_freqs => '{0.1,0.2,0.3}'::real[]
+    );
+
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{1,2,3}'::text
+    );
+
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2023-09-30,2024-10-31,3}'::text,
+    most_common_freqs => '{0.2,0.1}'::real[]
+    );
+
+-- warning: mcv cast failure
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,four,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: histogram elements null value
+-- this generates no warnings, but perhaps it should
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,NULL,3,4}'::text
+    );
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,2,3,4}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 0.5::real);
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: scalars can't have mcelem
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{1,3}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,two}'::text
+    );
+
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
+    );
+
+-- ok: mcelem
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- warn: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real
+    );
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    version => 150000::integer,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[],
+    histogram_bounds => '{1,2,3,4}'::text,
+    correlation => 1.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    range_empty_frac => -0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+
+SELECT
+    format('SELECT pg_catalog.pg_set_attribute_stats( '
+            || 'relation => %L::regclass, '
+            || 'attname => %L::name, '
+            || 'inherited => %L::boolean, '
+            || 'version => %L::integer, '
+            || 'null_frac => %L::real, '
+            || 'avg_width => %L::integer, '
+            || 'n_distinct => %L::real %s)',
+        'stats_export_import.' || s.tablename || '_clone',
+        s.attname,
+        s.inherited,
+        150000,
+        s.null_frac,
+        s.avg_width,
+        s.n_distinct,
+        CASE
+            WHEN s.most_common_vals IS NULL THEN ''
+            ELSE format(', most_common_vals => %L::text, most_common_freqs => %L::real[]',
+                        s.most_common_vals, s.most_common_freqs)
+        END ||
+        CASE
+            WHEN s.histogram_bounds IS NULL THEN ''
+            ELSE format(', histogram_bounds => %L::text', s.histogram_bounds)
+        END ||
+        CASE
+            WHEN s.correlation IS NULL THEN ''
+            ELSE format(', correlation => %L::real', s.correlation)
+        END ||
+        CASE
+            WHEN s.most_common_elems IS NULL THEN ''
+            ELSE format(', most_common_elems => %L::text, most_common_elem_freqs => %L::real[]',
+                        s.most_common_elems, s.most_common_elem_freqs)
+        END ||
+        CASE
+            WHEN s.elem_count_histogram IS NULL THEN ''
+            ELSE format(', elem_count_histogram => %L::real[]',
+                        s.elem_count_histogram)
+        END ||
+        CASE
+            WHEN s.range_bounds_histogram IS NULL THEN ''
+            ELSE format(',  range_bounds_histogram => %L::text', s.range_bounds_histogram)
+        END ||
+        CASE
+            WHEN s.range_empty_frac IS NULL THEN ''
+            ELSE format(', range_empty_frac => %L::real, range_length_histogram => %L::text',
+                        s.range_empty_frac, s.range_length_histogram)
+        END)
+FROM pg_catalog.pg_stats AS s
+WHERE s.schemaname = 'stats_export_import'
+AND s.tablename IN ('test', 'is_odd')
+\gexec
+
+-- restore ECHO to original value
+\set ECHO :orig_ECHO
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass;
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.test'::regclass;
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass;
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_export_import.is_odd'::regclass;
+
 DROP SCHEMA stats_export_import CASCADE;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 4ee0cdb47b..5115a97ac2 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29845,6 +29845,83 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          </para>
        </entry>
       </row>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_attribute_stats</primary>
+        </indexterm>
+        <function>pg_set_attribute_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>
+         , <parameter>attname</parameter> <type>name</type>
+         , <parameter>inherited</parameter> <type>boolean</type>
+         , <parameter>version</parameter> <type>integer</type>
+         , <parameter>null_frac</parameter> <type>real</type>
+         , <parameter>avg_width</parameter> <type>integer</type>
+         , <parameter>n_distinct</parameter> <type>real</type>
+         [, <parameter>most_common_vals</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_freqs</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>histogram_bounds</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>correlation</parameter> <type>real</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_elems</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_elem_freqs</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>elem_count_histogram</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_length_histogram</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_empty_frac</parameter> <type>real</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_bounds_histogram</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ] )
+        <returnvalue>void</returnvalue>
+         </para>
+         <para>
+          Replaces the <structname>pg_statistic</structname> row for the
+          <structname>pg_attribute</structname> row specified by
+          <parameter>relation</parameter>, <parameter>attname</parameter>
+          and <parameter>inherited</parameter>.
+         </para>
+         <para>
+          The value of <structfield>version</structfield> represents the
+          integer <varname>SERVER_VERSION_NUM</varname> of the
+          database that was the source of these values. A value of
+          <literal>NULL</literal> means to use the
+          <varname>SERVER_VERSION_NUM</varname> of the
+          current database. It should be noted that presently this value does
+          not alter the behavior of the function, but it could in future
+          versions.
+         </para>
+         <para>
+          The remaining parameters all correspond to attributes of the same name
+          found in <link linkend="view-pg-stats"><structname>pg_stats</structname></link>,
+          and the values supplied in the parameter must meet the requirements of
+          the corresponding attribute. Any parameters not supplied are assumed to be NULL.
+         </para>
+         <para>
+          This function mimics the behavior of <command>ANALYZE</command> in its
+          effects on the values in <structname>pg_statistic</structname>, except
+          that the values are supplied as parameters rather than derived from
+          table sampling.
+         </para>
+         <para>
+          The purpose of this function is to apply statistics values in an
+          upgrade situation that are "good enough" for system operation until
+          they are replaced by the next <command>ANALYZE</command>, usually via
+          <command>autovacuum</command> This function is used by
+          <command>pg_upgrade</command> and <command>pg_restore</command> to
+          convey the statistics from the old system version into the new one.
+         </para>
+         <para>
+          The caller must either be the owner of the relation, or have superuser
+          privileges.
+         </para>
+       </entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
-- 
2.45.2

v24-0005-Enable-dumping-of-table-index-stats-in-pg_dump.patchtext/x-patch; charset=US-ASCII; name=v24-0005-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From 966c0d1c638c6f58b40e7554898f89fa44eba269 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH v24 5/5] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will generate a statement that
calls pg_set_relation_stats(), and it will generate a series of
statements that call pg_set_attribute_stats(), one per attribute. These
statements will restore the statistics of the current system onto the
destination system.

Adds the command-line options -X / --statistics-only, which are mutually
exclusive to --schema-only and --data-only.

Statistics are not dumped when --schema-only is specified, except during
a binary upgrade.

As is the pattern with pg_dump options, staistics can be disabled using
--no-statistics.

Table statistics are dumped in the data section. This is true even if
dumping stats in a binary upgrade. Index and Materialized View
statistics are dumped in the post-data section.
---
 src/bin/pg_dump/pg_backup.h          |   6 +
 src/bin/pg_dump/pg_backup_archiver.c |   5 +
 src/bin/pg_dump/pg_dump.c            | 397 ++++++++++++++++++++++++++-
 src/bin/pg_dump/pg_dump.h            |   8 +
 src/bin/pg_dump/pg_dump_sort.c       |   5 +
 src/bin/pg_dump/pg_dumpall.c         |   5 +
 src/bin/pg_dump/pg_restore.c         |   8 +
 src/bin/pg_dump/t/001_basic.pl       |  10 +-
 doc/src/sgml/ref/pg_dump.sgml        |  36 ++-
 doc/src/sgml/ref/pg_restore.sgml     |  31 ++-
 10 files changed, 493 insertions(+), 18 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index d942a6a256..25d386f5bb 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -112,11 +112,13 @@ typedef struct _restoreOptions
 	int			no_publications;	/* Skip publication entries */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
 	int			dataOnly;
 	int			schemaOnly;
+	int			statisticsOnly;
 	int			dumpSections;
 	int			verbose;
 	int			aclsSkip;
@@ -161,6 +163,7 @@ typedef struct _restoreOptions
 	/* flags derived entirely from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } RestoreOptions;
 
 typedef struct _dumpOptions
@@ -172,6 +175,7 @@ typedef struct _dumpOptions
 	/* various user-settable parameters */
 	bool		schemaOnly;
 	bool		dataOnly;
+	bool		statisticsOnly;
 	int			dumpSections;	/* bitmask of chosen sections */
 	bool		aclsSkip;
 	const char *lockWaitTimeout;
@@ -185,6 +189,7 @@ typedef struct _dumpOptions
 	int			no_security_labels;
 	int			no_publications;
 	int			no_subscriptions;
+	int			no_statistics;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
@@ -211,6 +216,7 @@ typedef struct _dumpOptions
 	/* flags derived entirely from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } DumpOptions;
 
 /*
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 68e321212d..9bfa2f1326 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2958,6 +2958,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's a stats dump, maybe ignore it */
+	if (ropt->no_statistics && strcmp(te->desc, "STATISTICS") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2987,6 +2991,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 5f7cd2b29e..bc94b1b4ff 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -424,6 +424,7 @@ main(int argc, char **argv)
 		{"encoding", required_argument, NULL, 'E'},
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
+		{"statistics-only", no_argument, NULL, 'X'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -452,6 +453,7 @@ main(int argc, char **argv)
 		{"no-comments", no_argument, &dopt.no_comments, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -497,7 +499,7 @@ main(int argc, char **argv)
 
 	InitDumpOptions(&dopt);
 
-	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:RsS:t:T:U:vwWxZ:",
+	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:PRsS:t:T:U:vwWxXZ:",
 							long_options, &optindex)) != -1)
 	{
 		switch (c)
@@ -571,6 +573,10 @@ main(int argc, char **argv)
 				dopt.cparams.pgport = pg_strdup(optarg);
 				break;
 
+			case 'X':			/* Dump statistics only */
+				dopt.statisticsOnly = true;
+				break;
+
 			case 'R':
 				/* no-op, still accepted for backwards compatibility */
 				break;
@@ -740,8 +746,11 @@ main(int argc, char **argv)
 	if (dopt.binary_upgrade)
 		dopt.sequence_data = 1;
 
-	if (dopt.dataOnly && dopt.schemaOnly)
-		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (((int)dopt.dataOnly + (int) dopt.schemaOnly + (int) dopt.statisticsOnly) > 1)
+		pg_fatal("options -s/--schema-only, -a/--data-only, and -X/--statistics-only cannot be used together");
+
+	if (dopt.statisticsOnly && dopt.no_statistics)
+		pg_fatal("options -X/--statistics-only and --no-statistics cannot be used together");
 
 	if (dopt.schemaOnly && foreign_servers_include_patterns.head != NULL)
 		pg_fatal("options -s/--schema-only and --include-foreign-data cannot be used together");
@@ -756,8 +765,23 @@ main(int argc, char **argv)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
 	/* set derivative flags */
-	dopt.dumpSchema = (!dopt.dataOnly);
-	dopt.dumpData = (!dopt.schemaOnly);
+	dopt.dumpSchema = dopt.schemaOnly ||
+		!(dopt.dataOnly || dopt.statisticsOnly);
+
+	dopt.dumpData = dopt.dataOnly ||
+		!(dopt.schemaOnly || dopt.statisticsOnly);
+
+	if (dopt.statisticsOnly)
+		/* stats are only thing wanted */
+		dopt.dumpStatistics = true;
+	else if (dopt.no_statistics)
+		/* stats specifically excluded */
+		dopt.dumpStatistics = false;
+	else if (dopt.binary_upgrade)
+		/* binary upgrade and not specifically excluded */
+		dopt.dumpStatistics = true;
+	else
+		dopt.dumpStatistics = !(dopt.schemaOnly || dopt.dataOnly);
 
 	/*
 	 * --inserts are already implied above if --column-inserts or
@@ -1054,6 +1078,7 @@ main(int argc, char **argv)
 	ropt->dropSchema = dopt.outputClean;
 	ropt->dataOnly = dopt.dataOnly;
 	ropt->schemaOnly = dopt.schemaOnly;
+	ropt->statisticsOnly = dopt.statisticsOnly;
 	ropt->if_exists = dopt.if_exists;
 	ropt->column_inserts = dopt.column_inserts;
 	ropt->dumpSections = dopt.dumpSections;
@@ -1132,7 +1157,7 @@ help(const char *progname)
 	printf(_("  -?, --help                   show this help, then exit\n"));
 
 	printf(_("\nOptions controlling the output content:\n"));
-	printf(_("  -a, --data-only              dump only the data, not the schema\n"));
+	printf(_("  -a, --data-only              dump only the data, not the schema or statistics\n"));
 	printf(_("  -b, --large-objects          include large objects in dump\n"));
 	printf(_("  --blobs                      (same as --large-objects, deprecated)\n"));
 	printf(_("  -B, --no-large-objects       exclude large objects in dump\n"));
@@ -1145,11 +1170,12 @@ help(const char *progname)
 	printf(_("  -N, --exclude-schema=PATTERN do NOT dump the specified schema(s)\n"));
 	printf(_("  -O, --no-owner               skip restoration of object ownership in\n"
 			 "                               plain-text format\n"));
-	printf(_("  -s, --schema-only            dump only the schema, no data\n"));
+	printf(_("  -s, --schema-only            dump only the schema, no data or statistics\n"));
 	printf(_("  -S, --superuser=NAME         superuser user name to use in plain-text format\n"));
 	printf(_("  -t, --table=PATTERN          dump only the specified table(s)\n"));
 	printf(_("  -T, --exclude-table=PATTERN  do NOT dump the specified table(s)\n"));
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
+	printf(_("  -X, --statistics-only        dump only the statistics, not schema or data\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
@@ -1176,6 +1202,7 @@ help(const char *progname)
 	printf(_("  --no-comments                do not dump comments\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -6673,6 +6700,42 @@ getFuncs(Archive *fout)
 	destroyPQExpBuffer(query);
 }
 
+/*
+ * getRelationStatistics
+ *    register the statistics object as a dependent of the relation.
+ *
+ */
+static RelStatsInfo *
+getRelationStatistics(Archive *fout, DumpableObject *rel, char relkind)
+{
+	if ((relkind == RELKIND_RELATION) ||
+		(relkind == RELKIND_PARTITIONED_TABLE) ||
+		(relkind == RELKIND_INDEX) ||
+		(relkind == RELKIND_PARTITIONED_INDEX) ||
+		(relkind == RELKIND_MATVIEW))
+	{
+		RelStatsInfo	   *info = pg_malloc0(sizeof(RelStatsInfo));
+		DumpableObject	   *dobj = &info->dobj;
+
+		dobj->objType = DO_REL_STATS;
+		dobj->catId.tableoid = 0;
+		dobj->catId.oid = 0;
+		AssignDumpId(dobj);
+		dobj->dependencies = (DumpId *) pg_malloc(sizeof(DumpId));
+		dobj->dependencies[0] = rel->dumpId;
+		dobj->nDeps = 1;
+		dobj->allocDeps = 1;
+		dobj->components |= DUMP_COMPONENT_STATISTICS;
+		dobj->dump = rel->dump;
+		dobj->name = pg_strdup(rel->name);
+		dobj->namespace = rel->namespace;
+		info->relkind = relkind;
+
+		return info;
+	}
+	return NULL;
+}
+
 /*
  * getTables
  *	  read all the tables (no indexes) in the system catalogs,
@@ -7050,6 +7113,7 @@ getTables(Archive *fout, int *numTables)
 
 		/* Tables have data */
 		tblinfo[i].dobj.components |= DUMP_COMPONENT_DATA;
+		tblinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
 
 		/* Mark whether table has an ACL */
 		if (!PQgetisnull(res, i, i_relacl))
@@ -7098,6 +7162,8 @@ getTables(Archive *fout, int *numTables)
 				}
 			}
 		}
+		if (tblinfo[i].interesting)
+			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relkind);
 	}
 
 	if (query->len != 0)
@@ -7533,11 +7599,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 		for (int c = 0; c < numinds; c++, j++)
 		{
 			char		contype;
+			char		indexkind;
+			RelStatsInfo   *relstats;
 
 			indxinfo[j].dobj.objType = DO_INDEX;
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -7560,7 +7629,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			{
 				NULL, NULL
 			};
+
+			if (indxinfo[j].parentidx == 0)
+				indexkind = RELKIND_INDEX;
+			else
+				indexkind = RELKIND_PARTITIONED_INDEX;
+
 			contype = *(PQgetvalue(res, j, i_contype));
+			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, indexkind);
 
 			if (contype == 'p' || contype == 'u' || contype == 'x')
 			{
@@ -7593,6 +7669,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				constrinfo->separate = true;
 
 				indxinfo[j].indexconstraint = constrinfo->dobj.dumpId;
+				if (relstats != NULL)
+					addObjectDependency(&relstats->dobj, constrinfo->dobj.dumpId);
 			}
 			else
 			{
@@ -10030,6 +10108,296 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * Tabular description of the parameters to pg_restore_relation_stats()
+ * required, param_name, param_type
+ */
+static const char *rel_stats_arginfo[][3] = {
+	{"t", "relation", "regclass"},
+	{"t", "version", "integer"},
+	{"f", "relpages", "integer"},
+	{"f", "reltuples", "real"},
+	{"f", "relallvisible", "integer"},
+};
+
+/*
+ * Tabular description of the parameters to pg_restore_attribute_stats()
+ * required, param_name, param_type
+ */
+static const char *att_stats_arginfo[][3] = {
+	{"t", "relation", "regclass"},
+	{"t", "attname", "name"},
+	{"t", "inherited", "boolean"},
+	{"t", "version", "integer"},
+	{"f", "null_frac", "float4"},
+	{"f", "avg_width", "integer"},
+	{"f", "n_distinct", "float4"},
+	{"f", "most_common_vals", "text"},
+	{"f", "most_common_freqs", "float4[]"},
+	{"f", "histogram_bounds", "text"},
+	{"f", "correlation", "float4"},
+	{"f", "most_common_elems", "text"},
+	{"f", "most_common_elem_freqs", "float4[]"},
+	{"f", "elem_count_histogram", "float4[]"},
+	{"f", "range_length_histogram", "text"},
+	{"f", "range_empty_frac", "float4"},
+	{"f", "range_bounds_histogram", "text"},
+};
+
+/*
+ * getRelStatsExportQuery --
+ *
+ * Generate a query that will fetch all relation (e.g. pg_class)
+ * stats for a given relation.
+ */
+static void
+getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "current_setting('server_version_num') AS version, "
+						 "c.relpages, c.reltuples, c.relallvisible "
+						 "FROM pg_class c "
+						 "JOIN pg_namespace n "
+						 "ON n.oid = c.relnamespace "
+						 "WHERE n.nspname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND c.relname = ");
+	appendStringLiteralAH(query, relname, fout);
+}
+
+/*
+ * getAttStatsExportQuery --
+ *
+ * Generate a query that will fetch all attribute (e.g. pg_statistic)
+ * stats for a given relation.
+ */
+static void
+getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "s.attname,"
+						 "s.inherited,"
+						 "current_setting('server_version_num') AS version, "
+						 "s.null_frac,"
+						 "s.avg_width,"
+						 "s.n_distinct,"
+						 "s.most_common_vals,"
+						 "s.most_common_freqs,"
+						 "s.histogram_bounds,"
+						 "s.correlation,"
+						 "s.most_common_elems,"
+						 "s.most_common_elem_freqs,"
+						 "s.elem_count_histogram,");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 "s.range_length_histogram,"
+							 "s.range_empty_frac,"
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(query,
+							 "NULL AS range_length_histogram,"
+							 "NULL AS range_empty_frac,"
+							 "NULL AS range_bounds_histogram ");
+
+	appendPQExpBufferStr(query,
+						 "FROM pg_stats s "
+						 "JOIN pg_namespace n "
+						 "ON n.nspname = s.schemaname "
+						 "JOIN pg_class c "
+						 "ON c.relname = s.tablename "
+						 "AND c.relnamespace = n.oid "
+						 "WHERE s.schemaname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND s.tablename = ");
+	appendStringLiteralAH(query, relname, fout);
+	appendPQExpBufferStr(query, " ORDER BY s.attname, s.inherited");
+}
+
+
+/*
+ * appendNamedArgument --
+ *
+ * Convenience routine for constructing parameters of the form:
+ * 'paraname', 'value'::type
+ */
+static void
+appendNamedArgument(PQExpBuffer out, Archive *fout,
+					const char *argname, bool positional,
+					const char *argval, const char *argtype)
+{
+	appendPQExpBufferStr(out, "\t");
+
+	if (!positional)
+	{
+		appendStringLiteralAH(out, argname, fout);
+		appendPQExpBufferStr(out, ", ");
+	}
+
+	appendStringLiteralAH(out, argval, fout);
+	appendPQExpBuffer(out, "::%s", argtype);
+}
+
+/*
+ * appendRelStatsImport --
+ *
+ * Append a formatted pg_restore_relation_stats statement.
+ */
+static void
+appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	const char *sep = "";
+
+	if (PQntuples(res) == 0)
+		return;
+
+	appendPQExpBufferStr(out, "SELECT pg_catalog.pg_restore_relation_stats(\n");
+
+	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
+	{
+		bool		positional = (rel_stats_arginfo[argno][0][0] == 't');
+		const char *argname = rel_stats_arginfo[argno][1];
+		const char *argtype = rel_stats_arginfo[argno][2];
+		int			fieldno = PQfnumber(res, argname);
+
+		if (fieldno < 0)
+			pg_fatal("relation stats export query missing field '%s'",
+					 argname);
+
+		if (PQgetisnull(res, 0, fieldno))
+		{
+			if (!positional)
+				pg_fatal("relation stats export query unexpected NULL in '%s'",
+						 argname);
+			else
+				continue;
+		}
+
+		appendPQExpBufferStr(out, sep);
+		appendNamedArgument(out, fout, argname, positional,
+							PQgetvalue(res, 0, fieldno), argtype);
+
+		sep = ",\n";
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+/*
+ * appendAttStatsImport --
+ *
+ * Append a series of formatted pg_restore_attribute_stats statements.
+ */
+static void
+appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	{
+		const char *sep = "";
+
+		appendPQExpBufferStr(out, "SELECT pg_catalog.pg_restore_attribute_stats(\n");
+		for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
+		{
+			bool		positional = (att_stats_arginfo[argno][0][0] == 't');
+			const char *argname = att_stats_arginfo[argno][1];
+			const char *argtype = att_stats_arginfo[argno][2];
+			int			fieldno = PQfnumber(res, argname);
+
+			if (fieldno < 0)
+				pg_fatal("attribute stats export query missing field '%s'",
+						 argname);
+
+			if (PQgetisnull(res, rownum, fieldno))
+			{
+				if (positional)
+					pg_fatal("attribute stats export query unexpected NULL in '%s'",
+							 argname);
+				else
+					continue;
+			}
+
+			appendPQExpBufferStr(out, sep);
+			appendNamedArgument(out, fout, argname, positional,
+								PQgetvalue(res, rownum, fieldno), argtype);
+			sep = ",\n";
+		}
+		appendPQExpBufferStr(out, "\n);\n");
+	}
+}
+
+/*
+ * get statistics dump section, which depends on the parent object type
+ *
+ * objects created in SECTION_PRE_DATA have stats in SECTION_DATA
+ * objects created in SECTION_POST_DATA have stats in SECTION_POST_DATA
+ */
+static teSection
+statisticsDumpSection(const RelStatsInfo *rsinfo)
+{
+
+	if ((rsinfo->relkind == RELKIND_MATVIEW) ||
+		(rsinfo->relkind == RELKIND_INDEX) ||
+		(rsinfo->relkind == RELKIND_PARTITIONED_INDEX))
+			return SECTION_POST_DATA;
+
+	return SECTION_DATA;
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	PGresult	   *res;
+	PQExpBuffer		query;
+	PQExpBuffer		out;
+	PQExpBuffer		tag;
+	DumpableObject *dobj = (DumpableObject *) &rsinfo->dobj;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", "STATISTICS DATA", fmtId(dobj->name));
+
+	query = createPQExpBuffer();
+	out = createPQExpBuffer();
+
+	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendRelStatsImport(out, fout, res);
+	PQclear(res);
+
+	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendAttStatsImport(out, fout, res);
+	PQclear(res);
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATISTICS DATA",
+							  .section = statisticsDumpSection(rsinfo),
+							  .createStmt = out->data,
+							  .deps = dobj->dependencies,
+							  .nDeps = dobj->nDeps));
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(out);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -10478,6 +10846,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_SUBSCRIPTION_REL:
 			dumpSubscriptionTable(fout, (const SubRelInfo *) dobj);
 			break;
+		case DO_REL_STATS:
+			dumpRelationStats(fout, (const RelStatsInfo *) dobj);
+			break;
 		case DO_PRE_DATA_BOUNDARY:
 		case DO_POST_DATA_BOUNDARY:
 			/* never dumped, nothing to do */
@@ -16748,6 +17119,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 		free(indstatvalsarray);
 	}
 
+	/* Comments and stats share same .dep */
+
 	/* Dump Index Comments */
 	if (indxinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
 		dumpComment(fout, "INDEX", qindxname,
@@ -18430,6 +18803,16 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 				/* must come after the pre-data boundary */
 				addObjectDependency(dobj, preDataBound->dumpId);
 				break;
+			case DO_REL_STATS:
+				/* stats section varies by parent object type, DATA or POST */
+				if (statisticsDumpSection((RelStatsInfo *) dobj) == SECTION_DATA)
+				{
+					addObjectDependency(dobj, preDataBound->dumpId);
+					addObjectDependency(postDataBound, dobj->dumpId);
+				}
+				else
+					addObjectDependency(dobj, postDataBound->dumpId);
+				break;
 		}
 	}
 }
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 4b2e5870a9..602f3e4417 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -84,6 +84,7 @@ typedef enum
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION,
 	DO_SUBSCRIPTION_REL,		/* see note for SubRelInfo */
+	DO_REL_STATS,
 } DumpableObjectType;
 
 /*
@@ -101,6 +102,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
@@ -416,6 +418,12 @@ typedef struct _indexAttachInfo
 	IndxInfo   *partitionIdx;	/* link to index on partition */
 } IndexAttachInfo;
 
+typedef struct _relStatsInfo
+{
+	DumpableObject	dobj;
+	char			relkind;		/* 'r', 'v', 'c', etc */
+} RelStatsInfo;
+
 typedef struct _statsExtInfo
 {
 	DumpableObject dobj;
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 4cb754caa5..cfe277f5ce 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -1490,6 +1490,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "POST-DATA BOUNDARY  (ID %d)",
 					 obj->dumpId);
 			return;
+		case DO_REL_STATS:
+			snprintf(buf, bufsize,
+					 "RELATION STATISTICS FOR %s  (ID %d OID %u)",
+					 obj->name, obj->dumpId, obj->catId.oid);
+			return;
 	}
 	/* shouldn't get here */
 	snprintf(buf, bufsize,
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 882dbf8e86..aea4aeb189 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -103,6 +103,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -172,6 +173,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -451,6 +453,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -666,6 +670,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 3475168a64..e285b2828f 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -75,6 +75,7 @@ main(int argc, char **argv)
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
 	static int	no_subscriptions = 0;
+	static int  no_statistics = 0;
 	static int	strict_names = 0;
 
 	struct option cmdopts[] = {
@@ -107,6 +108,7 @@ main(int argc, char **argv)
 		{"username", 1, NULL, 'U'},
 		{"verbose", 0, NULL, 'v'},
 		{"single-transaction", 0, NULL, '1'},
+		{"statistics-only", no_argument, NULL, 'P'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -127,6 +129,7 @@ main(int argc, char **argv)
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
+		{"no-statistics", no_argument, &no_statistics, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -270,6 +273,10 @@ main(int argc, char **argv)
 				opts->aclsSkip = 1;
 				break;
 
+			case 'X':			/* Restore statistics only */
+				opts->statisticsOnly = 1;
+				break;
+
 			case '1':			/* Restore data in a single transaction */
 				opts->single_txn = true;
 				opts->exit_on_error = true;
@@ -374,6 +381,7 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
diff --git a/src/bin/pg_dump/t/001_basic.pl b/src/bin/pg_dump/t/001_basic.pl
index b9d13a0e1d..b1c7a10919 100644
--- a/src/bin/pg_dump/t/001_basic.pl
+++ b/src/bin/pg_dump/t/001_basic.pl
@@ -46,8 +46,8 @@ command_fails_like(
 
 command_fails_like(
 	[ 'pg_dump', '-s', '-a' ],
-	qr/\Qpg_dump: error: options -s\/--schema-only and -a\/--data-only cannot be used together\E/,
-	'pg_dump: options -s/--schema-only and -a/--data-only cannot be used together'
+	qr/\Qpg_dump: error: options -s\/--schema-only, -a\/--data-only, and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: options -s/--schema-only, -a/--data-only, and -X/--statistics-only cannot be used together'
 );
 
 command_fails_like(
@@ -56,6 +56,12 @@ command_fails_like(
 	'pg_dump: options -s/--schema-only and --include-foreign-data cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '--statistics-only', '--no-statistics' ],
+	qr/\Qpg_dump: error: options -X\/--statistics-only and --no-statistics cannot be used together\E/,
+	'pg_dump: options -X\/--statistics-only and --no-statistics cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-j2', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: option --include-foreign-data is not supported with parallel backup\E/,
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index b95ed87517..aee50b37a1 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -119,7 +119,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Dump only the data, not the schema (data definitions).
+        Dump only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are dumped.
        </para>
 
@@ -137,8 +137,9 @@ PostgreSQL documentation
       <listitem>
        <para>
         Include large objects in the dump.  This is the default behavior
-        except when <option>--schema</option>, <option>--table</option>, or
-        <option>--schema-only</option> is specified.  The <option>-b</option>
+        except when <option>--schema</option>, <option>--table</option>,
+        <option>--schema-only</option>, or
+        <option>--statistics-only</option> is specified.  The <option>-b</option>
         switch is therefore only useful to add large objects to dumps
         where a specific schema or table has been requested.  Note that
         large objects are considered data and therefore will be included when
@@ -512,10 +513,11 @@ PostgreSQL documentation
       <term><option>--schema-only</option></term>
       <listitem>
        <para>
-        Dump only the object definitions (schema), not data.
+        Dump only the object definitions (schema), not data or statistics.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive to <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
         <option>--section=pre-data --section=post-data</option>.
@@ -648,6 +650,18 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Dump only the statistics, not the schema (data definitions) or data.
+        Statistics for tables, materialized views, and indexes are dumped.
+       </para>
+
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-Z <replaceable class="parameter">level</replaceable></option></term>
       <term><option>-Z <replaceable class="parameter">method</replaceable></option>[:<replaceable>detail</replaceable>]</term>
@@ -829,7 +843,8 @@ PostgreSQL documentation
         though you do not need the data in it.
        </para>
        <para>
-        To exclude data for all tables in the database, see <option>--schema-only</option>.
+        To exclude data for all tables in the database, see <option>--schema-only</option>
+        or <option>--statistics-only</option>.
        </para>
       </listitem>
      </varlistentry>
@@ -1086,6 +1101,15 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index 2e3ba80258..8b1658e648 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -94,7 +94,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Restore only the data, not the schema (data definitions).
+        Restore only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are restored,
         if present in the archive.
        </para>
@@ -483,10 +483,11 @@ PostgreSQL documentation
         to the extent that schema entries are present in the archive.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive of <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
-        <option>--section=pre-data --section=post-data</option>.
+        <option>--section=pre-data --section=post-data --no-statistics</option>.
        </para>
        <para>
         (Do not confuse this with the <option>--schema</option> option, which
@@ -599,6 +600,20 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Restore only the statistics, not schema or data.
+       </para>
+       <para>
+        (Do not confuse this with the <option>--schema</option> option, which
+        uses the word <quote>schema</quote> in a different meaning.)
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-1</option></term>
       <term><option>--single-transaction</option></term>
@@ -723,6 +738,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore statistics, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
-- 
2.45.2

v24-0001-Create-pg_set_relation_stats-function.patchtext/x-patch; charset=US-ASCII; name=v24-0001-Create-pg_set_relation_stats-function.patchDownload
From 804d765e9cb10fad4aae029b0e89f243263af62f Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 17 Jul 2024 23:23:23 -0400
Subject: [PATCH v24 1/5] Create pg_set_relation_stats function.

This function is used to tweak statistics on any relation that the user
owns.

The first parameter, relation, is used to identify the the relation to be
modified.

The next parameter, version, is a version number corresponding to
the PG_VERSION_NUM of the system from which the remaining values were
drawn. A NULL value means to use the PG_VERSION_NUM of the current
system. Currently, there is no difference in how the stats are imported
for any supported versions, however, versions below 9.2 will be
rejected.

The remaining parameters correspond to the statistics attributes in
pg_class: relpages, reltuples, and relallisvible.

This function allows the user to tweak pg_class values in-place,
allowing the user to inflate rowcounts, table size, and visibility to see
what effect those changes will have on the the query planner.

The updates to pg_class are NON-transactional. This is done to mimic the
existig behavior of ANALYZE, which does so to avoid bloating pg_class.
---
 src/include/catalog/pg_proc.dat               |   8 +
 src/include/statistics/statistics.h           |   1 +
 src/backend/statistics/Makefile               |   3 +-
 src/backend/statistics/meson.build            |   1 +
 src/backend/statistics/statistics.c           | 231 ++++++++++++++++++
 .../regress/expected/stats_export_import.out  | 160 ++++++++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/stats_export_import.sql  | 107 ++++++++
 doc/src/sgml/func.sgml                        |  77 ++++++
 9 files changed, 588 insertions(+), 2 deletions(-)
 create mode 100644 src/backend/statistics/statistics.c
 create mode 100644 src/test/regress/expected/stats_export_import.out
 create mode 100644 src/test/regress/sql/stats_export_import.sql

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 73d9cf8582..468fe6549c 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12208,4 +12208,12 @@
   proargnames => '{summarized_tli,summarized_lsn,pending_lsn,summarizer_pid}',
   prosrc => 'pg_get_wal_summarizer_state' },
 
+# Statistics Import
+{ oid => '8048',
+  descr => 'set statistics on relation',
+  proname => 'pg_set_relation_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'oid int4 int4 float4 int4',
+  proargnames => '{relation,version,relpages,reltuples,relallvisible}',
+  prosrc => 'pg_set_relation_stats' }
 ]
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 7f2bf18716..02e9ad024e 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -127,4 +127,5 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												int nclauses);
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
+extern Datum pg_set_relation_stats(PG_FUNCTION_ARGS);
 #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..27fcdb4717
--- /dev/null
+++ b/src/backend/statistics/statistics.c
@@ -0,0 +1,231 @@
+/*-------------------------------------------------------------------------
+ * statistics.c
+ *
+ *	  POSTGRES statistics import
+ *
+ * Code supporting the direct importation of relation statistics, similar to
+ * what is done by the ANALYZE command.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *       src/backend/statistics/statistics.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_collation.h"
+#include "catalog/pg_database.h"
+#include "catalog/pg_operator.h"
+#include "catalog/pg_type.h"
+#include "fmgr.h"
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "nodes/nodeFuncs.h"
+#include "statistics/statistics.h"
+#include "storage/lockdefs.h"
+#include "utils/acl.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/elog.h"
+#include "utils/float.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/rangetypes.h"
+#include "utils/syscache.h"
+#include "utils/typcache.h"
+
+/*
+ * Names of parameters found in the function pg_set_relation_stats.
+ */
+const char *relation_name = "relation";
+const char *relpages_name = "relpages";
+const char *reltuples_name = "reltuples";
+const char *relallvisible_name = "relallvisible";
+
+/*
+ * A role has privileges to set statistics on the relation if any of the
+ * following are true:
+ *   - the role owns the current database and the relation is not shared
+ *   - the role has the MAINTAIN privilege on the relation
+ *
+ */
+static bool
+can_modify_relation(Relation rel)
+{
+	Oid			relid = RelationGetRelid(rel);
+	Form_pg_class reltuple = rel->rd_rel;
+
+	return ((object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId())
+			 && !reltuple->relisshared) ||
+			pg_class_aclcheck(relid, GetUserId(), ACL_MAINTAIN) == ACLCHECK_OK);
+}
+
+/*
+ * Set statistics for a given pg_class entry.
+ *
+ * This does an in-place (i.e. non-transactional) update of pg_class, just as
+ * is done in ANALYZE.
+ *
+ */
+Datum
+pg_set_relation_stats(PG_FUNCTION_ARGS)
+{
+
+	Datum		relation_datum = PG_GETARG_DATUM(0);
+	bool		relation_isnull = PG_ARGISNULL(0);
+	Oid			relation;
+
+	Datum		version_datum = PG_GETARG_DATUM(1);
+	bool		version_isnull = PG_ARGISNULL(1);
+	int			version;
+
+	Datum		relpages_datum = PG_GETARG_DATUM(2);
+	bool		relpages_isnull = PG_ARGISNULL(2);
+	int			relpages;
+
+	Datum		reltuples_datum = PG_GETARG_DATUM(3);
+	bool		reltuples_isnull = PG_ARGISNULL(3);
+	float4		reltuples;
+
+	Datum		relallvisible_datum = PG_GETARG_DATUM(4);
+	bool		relallvisible_isnull = PG_ARGISNULL(4);
+	int			relallvisible;
+
+	Relation	rel;
+	HeapTuple	ctup;
+	Form_pg_class pgcform;
+
+	/* If we don't know what relation we're modifying, give up */
+	if (relation_isnull)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Parameter %s cannot be null", relation_name)));
+		PG_RETURN_BOOL(false);
+	}
+	else
+		relation = DatumGetObjectId(relation_datum);
+
+	/* NULL version means assume current server version */
+	if (version_isnull)
+		version = PG_VERSION_NUM;
+	else
+	{
+		version = DatumGetInt32(version_datum);
+		if (version < 90200)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Cannot export statistics prior to version 9.2")));
+			PG_RETURN_BOOL(false);
+		}
+	}
+
+	if (relpages_isnull)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", relpages_name)));
+		PG_RETURN_BOOL(false);
+	}
+	else
+	{
+		relpages = DatumGetInt32(relpages_datum);
+		if (relpages < -1)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be < -1", relpages_name)));
+			PG_RETURN_BOOL(false);
+		}
+	}
+
+	if (reltuples_isnull)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", reltuples_name)));
+		PG_RETURN_BOOL(false);
+	}
+	else
+	{
+		reltuples = DatumGetFloat4(reltuples_datum);
+		if (reltuples < -1.0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be < -1.0", reltuples_name)));
+			PG_RETURN_BOOL(false);
+		}
+	}
+
+	if (relallvisible_isnull)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", relallvisible_name)));
+		PG_RETURN_BOOL(false);
+	}
+	else
+	{
+		relallvisible = DatumGetInt32(relallvisible_datum);
+		if (relallvisible < -1)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be < -1", relallvisible_name)));
+			PG_RETURN_BOOL(false);
+		}
+	}
+
+	/*
+	 * Open the relation, getting ShareUpdateExclusiveLock to ensure that no
+	 * other stat-setting operation can run on it concurrently.
+	 */
+	rel = table_open(RelationRelationId, ShareUpdateExclusiveLock);
+
+	ctup = SearchSysCacheCopy1(RELOID, relation_datum);
+	if (!HeapTupleIsValid(ctup))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_OBJECT_IN_USE),
+				 errmsg("pg_class entry for relid %u not found", relation)));
+		table_close(rel, NoLock);
+		PG_RETURN_BOOL(false);
+	}
+
+	pgcform = (Form_pg_class) GETSTRUCT(ctup);
+
+	if (!can_modify_relation(rel))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						RelationGetRelationName(rel))));
+		table_close(rel, NoLock);
+		PG_RETURN_BOOL(false);
+	}
+
+	/* Only update pg_class if there is a meaningful change */
+	if ((pgcform->reltuples != reltuples)
+		|| (pgcform->relpages != relpages)
+		|| (pgcform->relallvisible != relallvisible))
+	{
+		pgcform->relpages = relpages;
+		pgcform->reltuples = reltuples;
+		pgcform->relallvisible = relallvisible;
+
+		heap_inplace_update(rel, ctup);
+	}
+
+	table_close(rel, NoLock);
+
+	PG_RETURN_BOOL(true);
+}
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..9559f8d524
--- /dev/null
+++ b/src/test/regress/expected/stats_export_import.out
@@ -0,0 +1,160 @@
+CREATE SCHEMA stats_export_import;
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+-- starting stats
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+        0 |        -1 |             0
+(1 row)
+
+-- false: regclass not found
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 0::Oid,
+        version => 150000::integer,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+WARNING:  pg_class entry for relid 0 not found
+ pg_set_relation_stats 
+-----------------------
+ f
+(1 row)
+
+-- false: relpages NULL
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_export_import.test'::regclass,
+        version => 150000::integer,
+        relpages => NULL::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+WARNING:  relpages cannot be NULL
+ pg_set_relation_stats 
+-----------------------
+ f
+(1 row)
+
+-- false: reltuples NULL
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_export_import.test'::regclass,
+        version => 150000::integer,
+        relpages => 17::integer,
+        reltuples => NULL::real,
+        relallvisible => 4::integer);
+WARNING:  reltuples cannot be NULL
+ pg_set_relation_stats 
+-----------------------
+ f
+(1 row)
+
+-- false: relallvisible NULL
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_export_import.test'::regclass,
+        version => 150000::integer,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => NULL::integer);
+WARNING:  relallvisible cannot be NULL
+ pg_set_relation_stats 
+-----------------------
+ f
+(1 row)
+
+-- false: version too old
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_export_import.test'::regclass,
+        version => 2::integer,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+WARNING:  Cannot export statistics prior to version 9.2
+ pg_set_relation_stats 
+-----------------------
+ f
+(1 row)
+
+-- true: all named
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_export_import.test'::regclass,
+        version => 150000::integer,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+ pg_set_relation_stats 
+-----------------------
+ t
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       17 |       400 |             4
+(1 row)
+
+-- true: all positional
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        'stats_export_import.test'::regclass,
+        150000::integer,
+        18::integer,
+        401.0::real,
+        5::integer);
+ pg_set_relation_stats 
+-----------------------
+ t
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       18 |       401 |             5
+(1 row)
+
+-- true: version NULL
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_export_import.test'::regclass,
+        version => NULL,
+        relpages => 19::integer,
+        reltuples => 402.0::real,
+        relallvisible => 6::integer);
+ pg_set_relation_stats 
+-----------------------
+ t
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       19 |       402 |             6
+(1 row)
+
+DROP SCHEMA stats_export_import CASCADE;
+NOTICE:  drop cascades to 2 other objects
+DETAIL:  drop cascades to type stats_export_import.complex_type
+drop cascades to table stats_export_import.test
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 2429ec2bba..ea99302741 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,7 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_export_import
 
 # ----------
 # Load huge amounts of data
diff --git a/src/test/regress/sql/stats_export_import.sql b/src/test/regress/sql/stats_export_import.sql
new file mode 100644
index 0000000000..65d647830c
--- /dev/null
+++ b/src/test/regress/sql/stats_export_import.sql
@@ -0,0 +1,107 @@
+CREATE SCHEMA stats_export_import;
+
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+
+-- starting stats
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_export_import.test'::regclass;
+
+-- false: regclass not found
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 0::Oid,
+        version => 150000::integer,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+
+-- false: relpages NULL
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_export_import.test'::regclass,
+        version => 150000::integer,
+        relpages => NULL::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+
+-- false: reltuples NULL
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_export_import.test'::regclass,
+        version => 150000::integer,
+        relpages => 17::integer,
+        reltuples => NULL::real,
+        relallvisible => 4::integer);
+
+-- false: relallvisible NULL
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_export_import.test'::regclass,
+        version => 150000::integer,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => NULL::integer);
+
+-- false: version too old
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_export_import.test'::regclass,
+        version => 2::integer,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+
+-- true: all named
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_export_import.test'::regclass,
+        version => 150000::integer,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_export_import.test'::regclass;
+
+-- true: all positional
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        'stats_export_import.test'::regclass,
+        150000::integer,
+        18::integer,
+        401.0::real,
+        5::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_export_import.test'::regclass;
+
+-- true: version NULL
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_export_import.test'::regclass,
+        version => NULL,
+        relpages => 19::integer,
+        reltuples => 402.0::real,
+        relallvisible => 6::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_export_import.test'::regclass;
+
+DROP SCHEMA stats_export_import CASCADE;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index fd5699f4d8..4ee0cdb47b 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29772,6 +29772,83 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
     in identifying the specific disk files associated with database objects.
    </para>
 
+   <table id="functions-admin-statsimport">
+    <title>Database Object Statistics Import Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry">
+         <para role="func_signature">
+           <indexterm>
+            <primary>pg_set_relation_stats</primary>
+           </indexterm>
+           <function>pg_set_relation_stats</function> (
+            <parameter>relation</parameter> <type>regclass</type>
+            , <parameter>version</parameter> <type>integer</type>,
+            , <parameter>relpages</parameter> <type>integer</type>,
+            , <parameter>reltuples</parameter> <type>real</type>,
+            , <parameter>relallvisible</parameter> <type>integer</type> )
+           <returnvalue>void</returnvalue>
+         </para>
+         <para>
+          Updates the <structname>pg_class</structname> row for the specified
+          <parameter>relation</parameter>, setting the values for the columns
+          <structfield>reltuples</structfield>,
+          <structfield>relpages</structfield>, and
+          <structfield>relallvisible</structfield>.
+          To avoid table bloat in <structname>pg_class</structname>, this change
+          is made with an in-place update, and therefore cannot be rolled back
+          through normal transaction processing.
+         </para>
+         <para>
+          This function mimics the behavior of <command>ANALYZE</command> in its
+          effects on the values in <structname>pg_class</structname>, except that
+          the values are supplied as parameters rather than derived from table
+          sampling.
+         </para>
+         <para>
+          The caller must either be the owner of the relation, or have superuser
+          privileges.
+         </para>
+         <para>
+          The value of <structfield>version</structfield> represents the
+          integer <varname>SERVER_VERSION_NUM</varname> of the
+          database that was the source of these values. A value of
+          <literal>NULL</literal> means to use the
+          <varname>SERVER_VERSION_NUM</varname> of the
+          current database. It should be noted that presently this value does
+          not alter the behavior of the function, but it could in future
+          versions.
+         </para>
+         <para>
+          The value of <structfield>relpages</structfield> must not be less than
+          0.
+         </para>
+         <para>
+          The value of <structfield>reltuples</structfield> must not be less than
+          -1.0.
+         </para>
+         <para>
+          The value of <structfield>relallvisible</structfield> must not be less
+          than 0.
+         </para>
+       </entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
    <table id="functions-admin-dblocation">
     <title>Database Object Location Functions</title>
     <tgroup cols="1">
-- 
2.45.2

#174Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#173)
Re: Statistics Import and Export

On Mon, 2024-07-22 at 12:05 -0400, Corey Huinker wrote:

Attached is v24, incorporating Jeff's feedback - looping an arg data
structure rather than individually checking each param type being the
biggest of them.

Thank you for splitting up the patches more finely.

v24-0001:

* pg_set_relation_stats(): the warning: "cannot export statistics
prior to version 9.2" doesn't make sense because the function is for
importing. Reword.

* I really think there should be a transactional option, just another
boolean, and if it has a default it should be true. This clearly has
use cases for testing plans, etc., and often transactions will be the
right thing there. This should be a trivial code change, and it will
also be easier to document.

* The return type is documented as 'void'? Please change to bool and
be clear about what true/false returns really mean. I think false means
"no updates happened at all, and a WARNING was printed indicating why"
whereas true means "all updates were applied successfully".

* An alternative would be to have an 'error_ok' parameter to say
whether to issue WARNINGs or ERRORs. I think we already discussed that
and agreed on the boolean return, but I just want to confirm that this
was a conscious choice?

* tests should be called stats_import.sql; there's no exporting going
on

* Aside from the above comments and some other cleanup, I think this
is a simple patch and independently useful. I am looking to commit this
one soon.

v24-0002:

* Documented return type is 'void'

* I'm not totally sure what should be returned in the event that some
updates were applied and some not. I'm inclined to say that true should
mean that all updates were applied -- otherwise it's hard to
automatically detect some kind of typo.

* Can you describe your approach to error checking? What kinds of
errors are worth checking, and which should we just put into the
catalog and let the planner deal with?

* I'd check stakindidx at the time that it's incremented rather than
summing boolean values cast to integers.

v24-0003:

* I'm not convinced that we should continue when a stat name is not
text. The argument for being lenient is that statistics may change over
time, and we might have to ignore something that can't be imported from
an old version into a new version because it's either gone or the
meaning has changed too much. But that argument doesn't apply to a
bogus call, where the name/value pairs get misaligned or something.

Regards,
Jeff Davis

#175Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#174)
Re: Statistics Import and Export

* pg_set_relation_stats(): the warning: "cannot export statistics
prior to version 9.2" doesn't make sense because the function is for
importing. Reword.

+1

* I really think there should be a transactional option, just another
boolean, and if it has a default it should be true. This clearly has
use cases for testing plans, etc., and often transactions will be the
right thing there. This should be a trivial code change, and it will
also be easier to document.

For it to have a default, the parameter would have to be at the end of the
list, and it's a parameter list that will grow in the future. And when that
happens we have a jumbled parameter list, which is fine if we only ever
call params by name, but I know some people won't do that. Which means it's
up front right after `version`. Since `version` is already in there, and we
can't default that, I feel ok about moving it there, but alas no default.

If there was some way that the function could detect that it was in a
binary upgrade, then we could use that to determine if it should update
inplace or transactionally.

* The return type is documented as 'void'? Please change to bool and

be clear about what true/false returns really mean. I think false means
"no updates happened at all, and a WARNING was printed indicating why"
whereas true means "all updates were applied successfully".

Good point, that's a holdover.

* An alternative would be to have an 'error_ok' parameter to say
whether to issue WARNINGs or ERRORs. I think we already discussed that
and agreed on the boolean return, but I just want to confirm that this
was a conscious choice?

That had been discussed as well. If we're adding parameters, then we could
add one for that too. It's making the function call progressively more
unwieldy, but anyone who chooses to wield these on a regular basis can
certainly write a SQL wrapper function to reduce the function call to their
presets, I suppose.

* tests should be called stats_import.sql; there's no exporting going
on

Sigh. True.

* Aside from the above comments and some other cleanup, I think this
is a simple patch and independently useful. I am looking to commit this
one soon.

v24-0002:

* Documented return type is 'void'

* I'm not totally sure what should be returned in the event that some
updates were applied and some not. I'm inclined to say that true should
mean that all updates were applied -- otherwise it's hard to
automatically detect some kind of typo.

Me either. Suggestions welcome.

I suppose we could return two integers: number of stats input, and number
of stats applied. But that could be confusing, as some parameter pairs form
one stat ( MCV, ELEM_MCV, etc).

I suppose we could return a set of (param_name text, was_set boolean,
applied boolean), without trying to organize them into their pairs, but
that would get really verbose.

We should decide on something soon, because we'd want relation stats to
follow a similar signature.

* Can you describe your approach to error checking? What kinds of
errors are worth checking, and which should we just put into the
catalog and let the planner deal with?

1. When the parameters given make for something nonsensical Such as
providing most_common_elems with no corresponding most_common_freqs, then
you can't form an MCV stat, so you must throw out the one you did receive.
That gets a warning.

2. When the data provided is antithetical to the type of statistic. For
instance, most array-ish parameters can't have NULL values in them (there
are separate stats for nulls (null-frac, empty_frac). I don't remember if
doing so crashes the server or just creates a hard error, but it's a big
no-no, and we have to reject such stats, which for now means a warning and
trying to carry on with the stats that remain.

3. When the stats provided would overflow the data structure. We attack
this from two directions: First, we eliminate stat kinds that are
meaningless for the data type (scalars can't have most-common-elements,
only ranges can have range stats, etc), issue warnings for those and move
on with the remaining stats. If, however, the number of those statkinds
exceeds the number of statkind slots available, then we give up because now
we'd have to CHOOSE which N-5 stats to ignore, and the caller is clearly
just having fun with us.

We let the planner have fun with other error-like things:

1. most-common-element arrays where the elements are not sorted per spec.

2. frequency histograms where the numbers are not monotonically
non-increasing per spec.

3. frequency histograms that have corresponding low bound and high bound
values embedded in the array, and the other values in that array must be
between the low-high.

* I'd check stakindidx at the time that it's incremented rather than
summing boolean values cast to integers.

Which means that we're checking that and potentially raising the same error
in 3-4 places (and growing, unless we raise the max slots), rather than 1.
That struck me as worse.

v24-0003:

* I'm not convinced that we should continue when a stat name is not
text. The argument for being lenient is that statistics may change over
time, and we might have to ignore something that can't be imported from
an old version into a new version because it's either gone or the
meaning has changed too much. But that argument doesn't apply to a
bogus call, where the name/value pairs get misaligned or something.

I agree with that.

#176Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#175)
Re: Statistics Import and Export

Giving the parameter lists more thought, the desire for a return code more
granular than true/false/null, and the likelihood that each function will
inevitably get more parameters both stats and non-stats, I'm proposing the
following:

Two functions:

pg_set_relation_stats(
out schemaname name,
out relname name,
out row_written boolean,
out params_rejected text[],
kwargs any[]) RETURNS RECORD

and

pg_set_attribute_stats(
out schemaname name,
out relname name,
out inherited bool,
out row_written boolean,
out params_accepted text[],
out params_rejected text[],
kwargs any[]) RETURNS RECORD

The leading OUT parameters tell us the rel/attribute/inh affected (if any),
and which params had to be rejected for whatever reason. The kwargs is the
variadic key-value pairs that we were using for all stat functions, but now
we will be using it for all parameters, both statistics and control, the
control parameters will be:

relation - the oid of the relation
attname - the attribute name (does not apply for relstats)
inherited - true false for attribute stats, defaults false, does not apply
for relstats
warnings, boolean, if supplied AND set to true, then all ERROR that can be
stepped down to WARNINGS will be. This is "binary upgrade mode".
version - the numeric version (a la PG_VERSION_NUM) of the statistics
given. If NULL or omitted assume current PG_VERSION_NUM of server.
actual stats columns.

This allows casual users to set only the params they want for their needs,
and get proper errors, while pg_upgrade can set

'warnings', 'true', 'version', 120034

and get the upgrade behavior we need.

and pg_set_attribute_stats.
pg_set_relation_stats(out schemaname name, out relname name,, out
row_written boolean, out params_entered int, out params_accepted int,
kwargs any[])

#177Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#176)
Re: Statistics Import and Export

and pg_set_attribute_stats.
pg_set_relation_stats(out schemaname name, out relname name,, out
row_written boolean, out params_entered int, out params_accepted int,
kwargs any[])

Oops, didn't hit undo fast enough. Disregard this last bit.

#178Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#176)
Re: Statistics Import and Export

On Tue, 2024-07-23 at 17:48 -0400, Corey Huinker wrote:

Two functions:

I see that you moved back to a combination function to serve both the
"restore" use case as well as the "ad-hoc stats hacking" use case.

The "restore" use case is the primary point of your patch, and that
should be as simple and future-proof as possible. The parameters should
be name/value pairs and there shouldn't be any "control" parameters --
it's not the job of pg_dump to specify whether the restore should be
transactional or in-place, it should just output the necessary stats.

That restore function might be good enough to satisfy the "ad-hoc stats
hacking" use case as well, but I suspect we want slightly different
behavior. Specifically, I think we'd want the updates to be
transactional rather than in-place, or at least optional.

The leading OUT parameters tell us the rel/attribute/inh affected (if
any), and which params had to be rejected for whatever reason. The
kwargs is the variadic key-value pairs that we were using for all
stat functions, but now we will be using it for all parameters, both
statistics and control, the control parameters will be:

I don't like the idea of mixing statistics and control parameters in
the same list.

I do like the idea of returning a set, but I think it should be the
positive set (effectively a representation of what is now in the
pg_stats view) and any ignored settings would be output as WARNINGs.

Regards,
Jeff Davis

#179Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#178)
5 attachment(s)
Re: Statistics Import and Export

The "restore" use case is the primary point of your patch, and that
should be as simple and future-proof as possible. The parameters should
be name/value pairs and there shouldn't be any "control" parameters --
it's not the job of pg_dump to specify whether the restore should be
transactional or in-place, it should just output the necessary stats.

That restore function might be good enough to satisfy the "ad-hoc stats
hacking" use case as well, but I suspect we want slightly different
behavior. Specifically, I think we'd want the updates to be
transactional rather than in-place, or at least optional.

Point well taken.

Both function pairs now call a generic internal function.

Which is to say that pg_set_relation_stats and pg_restore_relation_stats
both accept parameters in their own way, and both call
an internal function relation_statistics_update(), each with their own
defaults.

pg_set_relation_stats always leaves "version" NULL, does transactional
updates, and treats any data quality issue as an ERROR. This is is in line
with a person manually tweaking stats to check against a query to see if
the plan changes.

pg_restore_relation_stats does in-place updates, and steps down all errors
to warnings. The stats may not write, but at least it won't fail the
pg_upgrade for you.

pg_set_attribute_stats is error-maximalist like pg_set_relation_stats.
pg_restore_attribute_stats never had an in-place option to begin with.

The leading OUT parameters tell us the rel/attribute/inh affected (if
any), and which params had to be rejected for whatever reason. The
kwargs is the variadic key-value pairs that we were using for all
stat functions, but now we will be using it for all parameters, both
statistics and control, the control parameters will be:

I don't like the idea of mixing statistics and control parameters in
the same list.

There's no way around it, at least now we need never worry about a
confusing order for the parameters in the _restore_ functions because they
can now be in any order you like. But that speaks to another point: there
is no "you" in using the restore functions, those function calls will
almost exclusively be generated by pg_dump and we can all live rich and
productive lives never having seen one written down. I kid, but they're
actually not that gross.

Here is a -set function taken from the regression tests:

SELECT pg_catalog.pg_set_attribute_stats(
relation => 'stats_import.test'::regclass::oid,
attname => 'arange'::name,
inherited => false::boolean,
null_frac => 0.5::real,
avg_width => 2::integer,
n_distinct => -0.1::real,
range_empty_frac => 0.5::real,
range_length_histogram => '{399,499,Infinity}'::text
);
pg_set_attribute_stats
------------------------

(1 row)

and here is a restore function

-- warning: mcv cast failure
SELECT *
FROM pg_catalog.pg_restore_attribute_stats(
'relation', 'stats_import.test'::regclass::oid,
'attname', 'id'::name,
'inherited', false::boolean,
'version', 150000::integer,
'null_frac', 0.5::real,
'avg_width', 2::integer,
'n_distinct', -0.4::real,
'most_common_vals', '{2,four,3}'::text,
'most_common_freqs', '{0.3,0.25,0.05}'::real[]
);
WARNING: invalid input syntax for type integer: "four"
row_written | stats_applied | stats_rejected
| params_rejected
-------------+----------------------------------+--------------------------------------+-----------------
t | {null_frac,avg_width,n_distinct} |
{most_common_vals,most_common_freqs} |
(1 row)

There's a few things going on here:

1. An intentionally bad, impossible to write, value was put in
'most_common_vals'. 'four' cannot cast to integer, so the value fails, and
we get a warning
2. Because most_common_values failed, we can no longer construct a legit
STAKIND_MCV, so we have to throw out most_common_freqs with it.
3. Those failures aren't enough to prevent us from writing the other stats,
so we write the record, and report the row written, the stats we could
write, the stats we couldn't, and a list of other parameters we entered
that didn't make sense and had to be rejected (empty).

Overall, I'd say the format is on the pedantic side, but it's far from
unreadable, and mixing control parameters (version) with stats parameters
isn't that big a deal.

I do like the idea of returning a set, but I think it should be the

positive set (effectively a representation of what is now in the
pg_stats view) and any ignored settings would be output as WARNINGs.

Displaying the actual stats in pg_stats could get very, very big. So I
wouldn't recommend that.

What do you think of the example presented earlier?

Attached is v25.

Key changes:
- Each set/restore function pair now each call a common function that does
the heavy lifting, and the callers mostly marshall parameters into the
right spot and form the result set (really just one row).
- The restore functions now have all parameters passed in via a variadic
any[].
- the set functions now error out on just about any discrepancy, and do not
have a result tuple.
- test cases simplified a bit. There's still a lot of them, and I think
that's a good thing.
- Documentation to reflect significant reorganization.
- pg_dump modified to generate new function signatures.

Attachments:

v25-0001-Create-function-pg_set_relation_stats.patchtext/x-patch; charset=US-ASCII; name=v25-0001-Create-function-pg_set_relation_stats.patchDownload
From 5755338b53cac18d893b0b4bff24d2e6ccfbc8bb Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 24 Jul 2024 23:45:26 -0400
Subject: [PATCH v25 1/5] Create function pg_set_relation_stats.

This function is used to tweak statistics on any relation that the user
owns.

The first parameter, relation, is used to identify the the relation to be
modified.

The remaining parameters correspond to the statistics attributes in
pg_class: relpages, reltuples, and relallisvible.

This function allows the user to tweak pg_class statistics values,
allowing the user to inflate rowcounts, table size, and visibility to see
what effect those changes will have on the the query planner.

The function has no return value.
---
 src/include/catalog/pg_proc.dat            |   9 +
 src/include/statistics/statistics.h        |   2 +
 src/backend/statistics/Makefile            |   3 +-
 src/backend/statistics/meson.build         |   1 +
 src/backend/statistics/statistics.c        | 278 +++++++++++++++++++++
 src/test/regress/expected/stats_import.out |  99 ++++++++
 src/test/regress/parallel_schedule         |   2 +-
 src/test/regress/sql/stats_import.sql      |  79 ++++++
 doc/src/sgml/func.sgml                     |  63 +++++
 9 files changed, 534 insertions(+), 2 deletions(-)
 create mode 100644 src/backend/statistics/statistics.c
 create mode 100644 src/test/regress/expected/stats_import.out
 create mode 100644 src/test/regress/sql/stats_import.sql

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 06b2f4ba66..7ebcf612ca 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12235,4 +12235,13 @@
   proargnames => '{summarized_tli,summarized_lsn,pending_lsn,summarizer_pid}',
   prosrc => 'pg_get_wal_summarizer_state' },
 
+# Statistics Import
+{ oid => '8048',
+  descr => 'set statistics on relation',
+  proname => 'pg_set_relation_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'void',
+  proargtypes => 'oid int4 float4 int4',
+  proargnames => '{relation,relpages,reltuples,relallvisible}',
+  prosrc => 'pg_set_relation_stats' },
+
 ]
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 7f2bf18716..68441dfc16 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -127,4 +127,6 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												int nclauses);
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
+extern Datum pg_set_relation_stats(PG_FUNCTION_ARGS);
+
 #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..3c72633686
--- /dev/null
+++ b/src/backend/statistics/statistics.c
@@ -0,0 +1,278 @@
+/*-------------------------------------------------------------------------
+ * statistics.c
+ *
+ *	  PostgreSQL statistics import
+ *
+ * Code supporting the direct importation of relation statistics, similar to
+ * what is done by the ANALYZE command.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *       src/backend/statistics/statistics.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_class_d.h"
+#include "catalog/pg_collation.h"
+#include "catalog/pg_database.h"
+#include "catalog/pg_operator.h"
+#include "catalog/pg_type.h"
+#include "fmgr.h"
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "nodes/nodeFuncs.h"
+#include "statistics/statistics.h"
+#include "storage/lockdefs.h"
+#include "utils/acl.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/elog.h"
+#include "utils/float.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/rangetypes.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/typcache.h"
+
+/*
+ * Names of parameters found in the function pg_set_relation_stats.
+ */
+const char *relation_name = "relation";
+const char *relpages_name = "relpages";
+const char *reltuples_name = "reltuples";
+const char *relallvisible_name = "relallvisible";
+
+/*
+ * A role has privileges to set statistics on the relation if any of the
+ * following are true:
+ *   - the role owns the current database and the relation is not shared
+ *   - the role has the MAINTAIN privilege on the relation
+ *
+ */
+static bool
+can_modify_relation(Relation rel)
+{
+	Oid			relid = RelationGetRelid(rel);
+	Form_pg_class reltuple = rel->rd_rel;
+
+	return ((object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId())
+			 && !reltuple->relisshared) ||
+			pg_class_aclcheck(relid, GetUserId(), ACL_MAINTAIN) == ACLCHECK_OK);
+}
+
+/*
+ * Internal function for modifying statistics for a relation.
+ */
+static bool
+relation_statistics_update(Datum relation_datum, bool relation_isnull,
+						   Datum version_datum, bool version_isnull,
+						   Datum relpages_datum, bool relpages_isnull,
+						   Datum reltuples_datum, bool reltuples_isnull,
+						   Datum relallvisible_datum, bool relallvisible_isnull,
+						   bool raise_errors, bool in_place)
+{
+	int			elevel = (raise_errors) ? ERROR : WARNING;
+	Oid			relation;
+	int			version;
+	int			relpages;
+	float4		reltuples;
+	int			relallvisible;
+
+	Relation	rel;
+	HeapTuple	ctup;
+	Form_pg_class pgcform;
+
+	/* If we don't know what relation we're modifying, give up */
+	if (relation_isnull)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Parameter %s cannot be null", relation_name)));
+		return false;
+	}
+	else
+		relation = DatumGetObjectId(relation_datum);
+
+	/* NULL version means assume current server version */
+	if (version_isnull)
+		version = PG_VERSION_NUM;
+	else
+	{
+		version = DatumGetInt32(version_datum);
+		if (version < 90200)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Cannot export statistics prior to version 9.2")));
+			return false;
+		}
+	}
+
+	if (relpages_isnull)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", relpages_name)));
+		return false;
+	}
+	else
+	{
+		relpages = DatumGetInt32(relpages_datum);
+		if (relpages < -1)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be < -1", relpages_name)));
+			return false;
+		}
+	}
+
+	if (reltuples_isnull)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", reltuples_name)));
+		return false;
+	}
+	else
+	{
+		reltuples = DatumGetFloat4(reltuples_datum);
+		if (reltuples < -1.0)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be < -1.0", reltuples_name)));
+			return false;
+		}
+	}
+
+	if (relallvisible_isnull)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", relallvisible_name)));
+		return false;
+	}
+	else
+	{
+		relallvisible = DatumGetInt32(relallvisible_datum);
+		if (relallvisible < -1)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be < -1", relallvisible_name)));
+			return false;
+		}
+	}
+
+	/*
+	 * Open the relation, getting ShareUpdateExclusiveLock to ensure that no
+	 * other stat-setting operation can run on it concurrently.
+	 */
+	rel = table_open(RelationRelationId, ShareUpdateExclusiveLock);
+
+	ctup = SearchSysCacheCopy1(RELOID, relation_datum);
+	if (!HeapTupleIsValid(ctup))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_OBJECT_IN_USE),
+				 errmsg("pg_class entry for relid %u not found", relation)));
+		table_close(rel, NoLock);
+		return false;
+	}
+
+	pgcform = (Form_pg_class) GETSTRUCT(ctup);
+
+	if (!can_modify_relation(rel))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						RelationGetRelationName(rel))));
+		table_close(rel, NoLock);
+		return false;
+	}
+
+	/* Only update pg_class if there is a meaningful change */
+	if ((pgcform->reltuples != reltuples)
+		|| (pgcform->relpages != relpages)
+		|| (pgcform->relallvisible != relallvisible))
+	{
+		if (in_place)
+		{
+			/*
+			 * In place update for upgrades when we expect to touch most
+			 * relations in the database and don't want to bloat pg_class.
+			 */
+			pgcform->relpages = relpages;
+			pgcform->reltuples = reltuples;
+			pgcform->relallvisible = relallvisible;
+			heap_inplace_update(rel, ctup);
+		}
+		else
+		{
+			/*
+			 * Regular transactional update.
+			 */
+			int			cols[3] = {Anum_pg_class_relpages,
+								   Anum_pg_class_reltuples,
+								   Anum_pg_class_relallvisible};
+
+			Datum		values[3] = {relpages_datum, reltuples_datum,
+									 relallvisible_datum};
+
+			bool		nulls[3] = {false, false, false};
+
+			TupleDesc	tupdesc = RelationGetDescr(rel);
+			HeapTuple	ntup;
+
+			CatalogIndexState	indstate = CatalogOpenIndexes(rel);
+
+			ntup = heap_modify_tuple_by_cols(ctup, tupdesc, 3, cols, values, nulls);
+
+			CatalogTupleUpdateWithInfo(rel, &ntup->t_self, ntup, indstate);
+			heap_freetuple(ntup);
+			CatalogCloseIndexes(indstate);
+		}
+	}
+
+	table_close(rel, NoLock);
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * Set statistics for a given pg_class entry.
+ *
+ * Use a regular, transactional update.
+ *
+ * Statistics are assumed to be presented in format friendly to the current
+ * server version.
+ */
+Datum
+pg_set_relation_stats(PG_FUNCTION_ARGS)
+{
+	Datum	version_datum = (Datum) 0;
+	bool	version_isnull = true;
+	bool	raise_errors = true;
+	bool	in_place = false;
+
+	relation_statistics_update(PG_GETARG_DATUM(0), PG_ARGISNULL(0),
+							   version_datum, version_isnull,
+							   PG_GETARG_DATUM(1), PG_ARGISNULL(1),
+							   PG_GETARG_DATUM(2), PG_ARGISNULL(2),
+							   PG_GETARG_DATUM(3), PG_ARGISNULL(3),
+							   raise_errors, in_place);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
new file mode 100644
index 0000000000..e596e858d6
--- /dev/null
+++ b/src/test/regress/expected/stats_import.out
@@ -0,0 +1,99 @@
+CREATE SCHEMA stats_import;
+CREATE TYPE stats_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+CREATE TABLE stats_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+-- starting stats
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+        0 |        -1 |             0
+(1 row)
+
+-- ERROR: regclass not found
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 0::Oid,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+ERROR:  pg_class entry for relid 0 not found
+-- ERROR: relpages NULL
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => NULL::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+ERROR:  relpages cannot be NULL
+-- ERROR: reltuples NULL
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => NULL::real,
+        relallvisible => 4::integer);
+ERROR:  reltuples cannot be NULL
+-- ERROR: relallvisible NULL
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => NULL::integer);
+ERROR:  relallvisible cannot be NULL
+-- true: all named
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+ pg_set_relation_stats 
+-----------------------
+ 
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       17 |       400 |             4
+(1 row)
+
+-- true: all positional
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        'stats_import.test'::regclass,
+        18::integer,
+        401.0::real,
+        5::integer);
+ pg_set_relation_stats 
+-----------------------
+ 
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       18 |       401 |             5
+(1 row)
+
+DROP SCHEMA stats_import CASCADE;
+NOTICE:  drop cascades to 2 other objects
+DETAIL:  drop cascades to type stats_import.complex_type
+drop cascades to table stats_import.test
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 2429ec2bba..85fc85bfa0 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,7 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import
 
 # ----------
 # Load huge amounts of data
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
new file mode 100644
index 0000000000..282c031335
--- /dev/null
+++ b/src/test/regress/sql/stats_import.sql
@@ -0,0 +1,79 @@
+CREATE SCHEMA stats_import;
+
+CREATE TYPE stats_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+
+CREATE TABLE stats_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+
+-- starting stats
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+-- ERROR: regclass not found
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 0::Oid,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+
+-- ERROR: relpages NULL
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => NULL::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+
+-- ERROR: reltuples NULL
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => NULL::real,
+        relallvisible => 4::integer);
+
+-- ERROR: relallvisible NULL
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => NULL::integer);
+
+-- true: all named
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+-- true: all positional
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        'stats_import.test'::regclass,
+        18::integer,
+        401.0::real,
+        5::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+DROP SCHEMA stats_import CASCADE;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index b669ab7f97..eda8a039e5 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29778,6 +29778,69 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
     in identifying the specific disk files associated with database objects.
    </para>
 
+   <table id="functions-admin-statsimport">
+    <title>Database Object Statistics Import Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry">
+         <para role="func_signature">
+           <indexterm>
+            <primary>pg_set_relation_stats</primary>
+           </indexterm>
+           <function>pg_set_relation_stats</function> (
+            <parameter>relation</parameter> <type>regclass</type>
+            , <parameter>relpages</parameter> <type>integer</type>,
+            , <parameter>reltuples</parameter> <type>real</type>,
+            , <parameter>relallvisible</parameter> <type>integer</type> )
+           <returnvalue>void</returnvalue>
+         </para>
+         <para>
+          Updates the <structname>pg_class</structname> row for the specified
+          <parameter>relation</parameter>, setting the values for the columns
+          <structfield>reltuples</structfield>,
+          <structfield>relpages</structfield>, and
+          <structfield>relallvisible</structfield>.
+         </para>
+         <para>
+          This function mimics the behavior of <command>ANALYZE</command> in its
+          effects on the values in <structname>pg_class</structname>, except that
+          the values are supplied as parameters rather than derived from table
+          sampling.
+         </para>
+         <para>
+          The caller must either be the owner of the relation, or have superuser
+          privileges.
+         </para>
+         <para>
+          The value of <structfield>relpages</structfield> must not be less than
+          0.
+         </para>
+         <para>
+          The value of <structfield>reltuples</structfield> must not be less than
+          -1.0.
+         </para>
+         <para>
+          The value of <structfield>relallvisible</structfield> must not be less
+          than 0.
+         </para>
+       </entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
    <table id="functions-admin-dblocation">
     <title>Database Object Location Functions</title>
     <tgroup cols="1">
-- 
2.45.2

v25-0004-Add-derivative-flags-dumpSchema-dumpData.patchtext/x-patch; charset=US-ASCII; name=v25-0004-Add-derivative-flags-dumpSchema-dumpData.patchDownload
From 845acd061215349d32b3c6324487e79dd3fcacd7 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 4 May 2024 04:52:38 -0400
Subject: [PATCH v25 4/5] Add derivative flags dumpSchema, dumpData.

User-set flags --schema-only and --data-only are often consulted by
various operations to determine if they should be skipped or not. While
this logic works when there are only two mutually-exclusive -only
options, it will get progressively more confusing when more are added.

In anticipation of this, create the flags dumpSchema and dumpData which
are derivative of the existing options schemaOnly and dataOnly. This
allows us to restate current skip-this-section tests in terms of what is
enabled, rather than checking if the other -only mode is turned off.
---
 src/bin/pg_dump/pg_backup.h  |   8 ++
 src/bin/pg_dump/pg_dump.c    | 186 ++++++++++++++++++-----------------
 src/bin/pg_dump/pg_restore.c |   4 +
 3 files changed, 107 insertions(+), 91 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index fbf5f1c515..d942a6a256 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -157,6 +157,10 @@ typedef struct _restoreOptions
 	int			enable_row_security;
 	int			sequence_data;	/* dump sequence data even in schema-only mode */
 	int			binary_upgrade;
+
+	/* flags derived entirely from the user-settable flags */
+	bool		dumpSchema;
+	bool		dumpData;
 } RestoreOptions;
 
 typedef struct _dumpOptions
@@ -203,6 +207,10 @@ typedef struct _dumpOptions
 
 	int			sequence_data;	/* dump sequence data even in schema-only mode */
 	int			do_nothing;
+
+	/* flags derived entirely from the user-settable flags */
+	bool		dumpSchema;
+	bool		dumpData;
 } DumpOptions;
 
 /*
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index b8b1888bd3..5f7cd2b29e 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -755,6 +755,10 @@ main(int argc, char **argv)
 	if (dopt.if_exists && !dopt.outputClean)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
+	/* set derivative flags */
+	dopt.dumpSchema = (!dopt.dataOnly);
+	dopt.dumpData = (!dopt.schemaOnly);
+
 	/*
 	 * --inserts are already implied above if --column-inserts or
 	 * --rows-per-insert were specified.
@@ -937,7 +941,7 @@ main(int argc, char **argv)
 	 * -s means "schema only" and LOs are data, not schema, so we never
 	 * include LOs when -s is used.
 	 */
-	if (dopt.include_everything && !dopt.schemaOnly && !dopt.dontOutputLOs)
+	if (dopt.include_everything && dopt.dumpData && !dopt.dontOutputLOs)
 		dopt.outputLOs = true;
 
 	/*
@@ -951,15 +955,15 @@ main(int argc, char **argv)
 	 */
 	tblinfo = getSchemaData(fout, &numTables);
 
-	if (!dopt.schemaOnly)
+	if (dopt.dumpData)
 	{
 		getTableData(&dopt, tblinfo, numTables, 0);
 		buildMatViewRefreshDependencies(fout);
-		if (dopt.dataOnly)
+		if (!dopt.dumpSchema)
 			getTableDataFKConstraints();
 	}
 
-	if (dopt.schemaOnly && dopt.sequence_data)
+	if (!dopt.dumpData && dopt.sequence_data)
 		getTableData(&dopt, tblinfo, numTables, RELKIND_SEQUENCE);
 
 	/*
@@ -4094,8 +4098,8 @@ dumpPolicy(Archive *fout, const PolicyInfo *polinfo)
 	const char *cmd;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
@@ -4307,8 +4311,8 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 	char	   *qpubname;
 	bool		first = true;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	delq = createPQExpBuffer();
@@ -4622,8 +4626,8 @@ dumpPublicationNamespace(Archive *fout, const PublicationSchemaInfo *pubsinfo)
 	PQExpBuffer query;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	tag = psprintf("%s %s", pubinfo->dobj.name, schemainfo->dobj.name);
@@ -4665,8 +4669,8 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 	PQExpBuffer query;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	tag = psprintf("%s %s", pubinfo->dobj.name, tbinfo->dobj.name);
@@ -5029,8 +5033,8 @@ dumpSubscriptionTable(Archive *fout, const SubRelInfo *subrinfo)
 	PQExpBuffer query;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	Assert(fout->dopt->binary_upgrade && fout->remoteVersion >= 170000);
@@ -5103,8 +5107,8 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo)
 	int			i;
 	char		two_phase_disabled[] = {LOGICALREP_TWOPHASE_STATE_DISABLED, '\0'};
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	delq = createPQExpBuffer();
@@ -7251,8 +7255,8 @@ getPartitioningInfo(Archive *fout)
 	/* hash partitioning didn't exist before v11 */
 	if (fout->remoteVersion < 110000)
 		return;
-	/* needn't bother if schema-only dump */
-	if (fout->dopt->schemaOnly)
+	/* needn't bother if not dumping data */
+	if (!fout->dopt->dumpData)
 		return;
 
 	query = createPQExpBuffer();
@@ -8894,7 +8898,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * Now get info about column defaults.  This is skipped for a data-only
 	 * dump, as it is only needed for table schemas.
 	 */
-	if (!dopt->dataOnly && tbloids->len > 1)
+	if (dopt->dumpSchema && tbloids->len > 1)
 	{
 		AttrDefInfo *attrdefs;
 		int			numDefaults;
@@ -9024,7 +9028,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * Get info about table CHECK constraints.  This is skipped for a
 	 * data-only dump, as it is only needed for table schemas.
 	 */
-	if (!dopt->dataOnly && checkoids->len > 2)
+	if (dopt->dumpSchema && checkoids->len > 2)
 	{
 		ConstraintInfo *constrs;
 		int			numConstrs;
@@ -9934,13 +9938,13 @@ dumpCommentExtended(Archive *fout, const char *type,
 	/* Comments are schema not data ... except LO comments are data */
 	if (strcmp(type, "LARGE OBJECT") != 0)
 	{
-		if (dopt->dataOnly)
+		if (!dopt->dumpSchema)
 			return;
 	}
 	else
 	{
 		/* We do dump LO comments in binary-upgrade mode */
-		if (dopt->schemaOnly && !dopt->binary_upgrade)
+		if (!dopt->dumpData && !dopt->binary_upgrade)
 			return;
 	}
 
@@ -10047,7 +10051,7 @@ dumpTableComment(Archive *fout, const TableInfo *tbinfo,
 		return;
 
 	/* Comments are SCHEMA not data */
-	if (dopt->dataOnly)
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Search for comments associated with relation, using table */
@@ -10493,8 +10497,8 @@ dumpNamespace(Archive *fout, const NamespaceInfo *nspinfo)
 	PQExpBuffer delq;
 	char	   *qnspname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -10570,8 +10574,8 @@ dumpExtension(Archive *fout, const ExtensionInfo *extinfo)
 	PQExpBuffer delq;
 	char	   *qextname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -10695,8 +10699,8 @@ dumpType(Archive *fout, const TypeInfo *tyinfo)
 {
 	DumpOptions *dopt = fout->dopt;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Dump out in proper style */
@@ -11806,8 +11810,8 @@ dumpShellType(Archive *fout, const ShellTypeInfo *stinfo)
 	DumpOptions *dopt = fout->dopt;
 	PQExpBuffer q;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -11858,8 +11862,8 @@ dumpProcLang(Archive *fout, const ProcLangInfo *plang)
 	FuncInfo   *inlineInfo = NULL;
 	FuncInfo   *validatorInfo = NULL;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
@@ -12066,8 +12070,8 @@ dumpFunc(Archive *fout, const FuncInfo *finfo)
 	int			nconfigitems = 0;
 	const char *keyword;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -12458,8 +12462,8 @@ dumpCast(Archive *fout, const CastInfo *cast)
 	const char *sourceType;
 	const char *targetType;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Cannot dump if we don't have the cast function's info */
@@ -12564,8 +12568,8 @@ dumpTransform(Archive *fout, const TransformInfo *transform)
 	char	   *lanname;
 	const char *transformType;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Cannot dump if we don't have the transform functions' info */
@@ -12713,8 +12717,8 @@ dumpOpr(Archive *fout, const OprInfo *oprinfo)
 	char	   *oprregproc;
 	char	   *oprref;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
@@ -13000,8 +13004,8 @@ dumpAccessMethod(Archive *fout, const AccessMethodInfo *aminfo)
 	PQExpBuffer delq;
 	char	   *qamname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -13103,8 +13107,8 @@ dumpOpclass(Archive *fout, const OpclassInfo *opcinfo)
 	bool		needComma;
 	int			i;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -13374,8 +13378,8 @@ dumpOpfamily(Archive *fout, const OpfamilyInfo *opfinfo)
 	bool		needComma;
 	int			i;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -13581,8 +13585,8 @@ dumpCollation(Archive *fout, const CollInfo *collinfo)
 	const char *colllocale;
 	const char *collicurules;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -13835,8 +13839,8 @@ dumpConversion(Archive *fout, const ConvInfo *convinfo)
 	const char *conproc;
 	bool		condefault;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -13983,8 +13987,8 @@ dumpAgg(Archive *fout, const AggInfo *agginfo)
 	const char *proparallel;
 	char		defaultfinalmodify;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -14313,8 +14317,8 @@ dumpTSParser(Archive *fout, const TSParserInfo *prsinfo)
 	PQExpBuffer delq;
 	char	   *qprsname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14381,8 +14385,8 @@ dumpTSDictionary(Archive *fout, const TSDictInfo *dictinfo)
 	char	   *nspname;
 	char	   *tmplname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14457,8 +14461,8 @@ dumpTSTemplate(Archive *fout, const TSTemplateInfo *tmplinfo)
 	PQExpBuffer delq;
 	char	   *qtmplname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14523,8 +14527,8 @@ dumpTSConfig(Archive *fout, const TSConfigInfo *cfginfo)
 	int			i_tokenname;
 	int			i_dictname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14635,8 +14639,8 @@ dumpForeignDataWrapper(Archive *fout, const FdwInfo *fdwinfo)
 	PQExpBuffer delq;
 	char	   *qfdwname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14708,8 +14712,8 @@ dumpForeignServer(Archive *fout, const ForeignServerInfo *srvinfo)
 	char	   *qsrvname;
 	char	   *fdwname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14899,8 +14903,8 @@ dumpDefaultACL(Archive *fout, const DefaultACLInfo *daclinfo)
 	PQExpBuffer tag;
 	const char *type;
 
-	/* Do nothing in data-only dump, or if we're skipping ACLs */
-	if (dopt->dataOnly || dopt->aclsSkip)
+	/* Do nothing if not dumping schema, or if we're skipping ACLs */
+	if (!dopt->dumpSchema || dopt->aclsSkip)
 		return;
 
 	q = createPQExpBuffer();
@@ -15000,7 +15004,7 @@ dumpACL(Archive *fout, DumpId objDumpId, DumpId altDumpId,
 		return InvalidDumpId;
 
 	/* --data-only skips ACLs *except* large object ACLs */
-	if (dopt->dataOnly && strcmp(type, "LARGE OBJECT") != 0)
+	if (!dopt->dumpSchema && strcmp(type, "LARGE OBJECT") != 0)
 		return InvalidDumpId;
 
 	sql = createPQExpBuffer();
@@ -15129,13 +15133,13 @@ dumpSecLabel(Archive *fout, const char *type, const char *name,
 	 */
 	if (strcmp(type, "LARGE OBJECT") != 0)
 	{
-		if (dopt->dataOnly)
+		if (!dopt->dumpSchema)
 			return;
 	}
 	else
 	{
 		/* We do dump large object security labels in binary-upgrade mode */
-		if (dopt->schemaOnly && !dopt->binary_upgrade)
+		if (!dopt->dumpData && !dopt->binary_upgrade)
 			return;
 	}
 
@@ -15203,7 +15207,7 @@ dumpTableSecLabel(Archive *fout, const TableInfo *tbinfo, const char *reltypenam
 		return;
 
 	/* SecLabel are SCHEMA not data */
-	if (dopt->dataOnly)
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Search for comments associated with relation, using table */
@@ -15442,8 +15446,8 @@ dumpTable(Archive *fout, const TableInfo *tbinfo)
 	DumpId		tableAclDumpId = InvalidDumpId;
 	char	   *namecopy;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	if (tbinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
@@ -16472,8 +16476,8 @@ dumpTableAttach(Archive *fout, const TableAttachInfo *attachinfo)
 	PGresult   *res;
 	char	   *partbound;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -16544,8 +16548,8 @@ dumpAttrDef(Archive *fout, const AttrDefInfo *adinfo)
 	char	   *tag;
 	char	   *foreign;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Skip if not "separate"; it was dumped in the table's definition */
@@ -16633,8 +16637,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 	char	   *qindxname;
 	char	   *qqindxname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -16766,8 +16770,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 static void
 dumpIndexAttach(Archive *fout, const IndexAttachInfo *attachinfo)
 {
-	/* Do nothing in data-only dump */
-	if (fout->dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!fout->dopt->dumpSchema)
 		return;
 
 	if (attachinfo->partitionIdx->dobj.dump & DUMP_COMPONENT_DEFINITION)
@@ -16813,8 +16817,8 @@ dumpStatisticsExt(Archive *fout, const StatsExtInfo *statsextinfo)
 	PGresult   *res;
 	char	   *stxdef;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -16889,8 +16893,8 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
 	char	   *tag = NULL;
 	char	   *foreign;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -17521,8 +17525,8 @@ dumpTrigger(Archive *fout, const TriggerInfo *tginfo)
 	char	   *qtabname;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -17643,8 +17647,8 @@ dumpEventTrigger(Archive *fout, const EventTriggerInfo *evtinfo)
 	PQExpBuffer delqry;
 	char	   *qevtname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -17734,8 +17738,8 @@ dumpRule(Archive *fout, const RuleInfo *rinfo)
 	PGresult   *res;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index df119591cc..3475168a64 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -360,6 +360,10 @@ main(int argc, char **argv)
 	if (opts->single_txn && numWorkers > 1)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
+	/* set derivative flags */
+	opts->dumpSchema = (opts->dataOnly != 1);
+	opts->dumpData = (opts->schemaOnly != 1);
+
 	opts->disable_triggers = disable_triggers;
 	opts->enable_row_security = enable_row_security;
 	opts->noDataForFailedTables = no_data_for_failed_tables;
-- 
2.45.2

v25-0002-Create-function-pg_set_attribute_stats.patchtext/x-patch; charset=US-ASCII; name=v25-0002-Create-function-pg_set_attribute_stats.patchDownload
From c86203cafacc0fd6a5fca40f72693150300e9f2e Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 25 Jul 2024 03:34:45 -0400
Subject: [PATCH v25 2/5] Create function pg_set_attribute_stats.

The function pg_set_attribute_stats is used modify attribute statistics,
allowing the user to inflate chose favorable histograms, inflate the
frequency of certain values, etc, to see what those changes will evoke
from the query planner.

The function takes an oid to identify the relation of the attribute, the
name of the attribute, and a boolean flag to indicate if these are
inherited stats or not. The remaining parameters correspond to
statsitics attributes found in the pg_stats view.

If successful, the entire pg_statistic row is overwritten. Any parameter
values omitted or directly set to NULL mean that that particular type of
statistic will not exist in the new pg_statistic row. There is currently
no way to set just a few statistics in the row and leave others as-is.
This is partly due to the complexity of expressing such an action, and
partly because it would require a complicated reorganization of the
existing arrays of statkinds, which might potentially overflow those
arrays.

While the function does not attempt to validate the statistics given,
certain data errors make rendering those statistics impossible, and
thus those data errors will cause the function to error.

Examples:

- Some statistics kinds come in pairs. For example, the mcv stat
  consists of two parameters: most_common_vals and most_common_freqs,
  they must both be present in order to complete the mcv stat. If one
  is given but not the other, the operation will raise an error.

- Multi-value statistics such as most_common_elems do not allow for any
  elements within the array provided to be NULL, and any array given
  with NULLs in it will be rejected, which would also cause the
  corresponding -freqs parameter to be rejected, thus rejecting the
  whole stat-kind, but not otherwise affecting other parameters
  provided.
---
 src/include/catalog/pg_proc.dat            |   7 +
 src/include/statistics/statistics.h        |   1 +
 src/backend/catalog/system_functions.sql   |  22 +
 src/backend/statistics/statistics.c        | 985 ++++++++++++++++++++-
 src/test/regress/expected/stats_import.out | 645 +++++++++++++-
 src/test/regress/sql/stats_import.sql      | 544 ++++++++++++
 doc/src/sgml/func.sgml                     |  63 ++
 7 files changed, 2265 insertions(+), 2 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 7ebcf612ca..11cbd9eded 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12243,5 +12243,12 @@
   proargtypes => 'oid int4 float4 int4',
   proargnames => '{relation,relpages,reltuples,relallvisible}',
   prosrc => 'pg_set_relation_stats' },
+{ oid => '8049',
+  descr => 'set statistics on attribute',
+  proname => 'pg_set_attribute_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'void',
+  proargtypes => 'oid name bool float4 int4 float4 text _float4 text float4 text _float4 _float4 text float4 text',
+  proargnames => '{relation,attname,inherited,null_frac,avg_width,n_distinct,most_common_vals,most_common_freqs,histogram_bounds,correlation,most_common_elems,most_common_elem_freqs,elem_count_histogram,range_length_histogram,range_empty_frac,range_bounds_histogram}',
+  prosrc => 'pg_set_attribute_stats' },
 
 ]
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 68441dfc16..73d3b541dd 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -128,5 +128,6 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
 extern Datum pg_set_relation_stats(PG_FUNCTION_ARGS);
+extern Datum pg_set_attribute_stats(PG_FUNCTION_ARGS);
 
 #endif							/* STATISTICS_H */
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index ae099e328c..e7e8abde10 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -636,6 +636,28 @@ LANGUAGE INTERNAL
 CALLED ON NULL INPUT VOLATILE PARALLEL SAFE
 AS 'pg_stat_reset_slru';
 
+CREATE OR REPLACE FUNCTION
+  pg_set_attribute_stats(relation oid,
+                         attname name,
+                         inherited bool,
+                         null_frac real,
+                         avg_width integer,
+                         n_distinct real,
+                         most_common_vals text DEFAULT NULL,
+                         most_common_freqs real[] DEFAULT NULL,
+                         histogram_bounds text DEFAULT NULL,
+                         correlation real DEFAULT NULL,
+                         most_common_elems text DEFAULT NULL,
+                         most_common_elem_freqs real[] DEFAULT NULL,
+                         elem_count_histogram real[] DEFAULT NULL,
+                         range_length_histogram text DEFAULT NULL,
+                         range_empty_frac real DEFAULT NULL,
+                         range_bounds_histogram text DEFAULT NULL)
+RETURNS void
+LANGUAGE INTERNAL
+CALLED ON NULL INPUT VOLATILE
+AS 'pg_set_attribute_stats';
+
 --
 -- The default permissions for functions mean that anyone can execute them.
 -- A number of functions shouldn't be executable by just anyone, but rather
diff --git a/src/backend/statistics/statistics.c b/src/backend/statistics/statistics.c
index 3c72633686..bfabe0b563 100644
--- a/src/backend/statistics/statistics.c
+++ b/src/backend/statistics/statistics.c
@@ -45,12 +45,28 @@
 #include "utils/typcache.h"
 
 /*
- * Names of parameters found in the function pg_set_relation_stats.
+ * Names of parameters found in the functions pg_set_relation_stats and
+ * pg_set_attribute_stats
  */
 const char *relation_name = "relation";
 const char *relpages_name = "relpages";
 const char *reltuples_name = "reltuples";
 const char *relallvisible_name = "relallvisible";
+const char *attname_name = "attname";
+const char *inherited_name = "inherited";
+const char *null_frac_name = "null_frac";
+const char *avg_width_name = "avg_width";
+const char *n_distinct_name = "n_distinct";
+const char *mc_vals_name = "most_common_vals";
+const char *mc_freqs_name = "most_common_freqs";
+const char *histogram_bounds_name = "histogram_bounds";
+const char *correlation_name = "correlation";
+const char *mc_elems_name = "most_common_elems";
+const char *mc_elem_freqs_name = "most_common_elem_freqs";
+const char *elem_count_hist_name = "elem_count_histogram";
+const char *range_length_hist_name = "range_length_histogram";
+const char *range_empty_frac_name = "range_empty_frac";
+const char *range_bounds_hist_name = "range_bounds_histogram";
 
 /*
  * A role has privileges to set statistics on the relation if any of the
@@ -276,3 +292,970 @@ pg_set_relation_stats(PG_FUNCTION_ARGS)
 
 	PG_RETURN_VOID();
 }
+
+/*
+ * Test if the type is a scalar for MCELEM purposes
+ */
+static bool
+type_is_scalar(Oid typid)
+{
+	HeapTuple	tp;
+	bool		result = false;
+
+	tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+	if (HeapTupleIsValid(tp))
+	{
+		Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+
+		result = (!OidIsValid(typtup->typanalyze));
+		ReleaseSysCache(tp);
+	}
+	return result;
+}
+
+/*
+ * If this relation is an index and that index has expressions in it, and
+ * the attnum specified is known to be an expression, then we must walk
+ * the list attributes up to the specified attnum to get the right
+ * expression.
+ */
+static Node *
+get_attr_expr(Relation rel, int attnum)
+{
+	if ((rel->rd_rel->relkind == RELKIND_INDEX
+		 || (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+		&& (rel->rd_indexprs != NIL)
+		&& (rel->rd_index->indkey.values[attnum - 1] == 0))
+	{
+		ListCell   *indexpr_item = list_head(rel->rd_indexprs);
+
+		for (int i = 0; i < attnum - 1; i++)
+			if (rel->rd_index->indkey.values[i] == 0)
+				indexpr_item = lnext(rel->rd_indexprs, indexpr_item);
+
+		if (indexpr_item == NULL)	/* shouldn't happen */
+			elog(ERROR, "too few entries in indexprs list");
+
+		return (Node *) lfirst(indexpr_item);
+	}
+	return NULL;
+}
+
+/*
+ * Fetch datatype information, this is needed to derive the proper staopN
+ * and stacollN values.
+ *
+ */
+static TypeCacheEntry *
+get_attr_stat_type(Relation rel, Name attname, int elevel,
+				   int16 *attnum, int32 *typmod, Oid *typcoll)
+{
+	Oid			relid = RelationGetRelid(rel);
+	Form_pg_attribute attr;
+	HeapTuple	atup;
+	Oid			typid;
+	Node	   *expr;
+
+	atup = SearchSysCache2(ATTNAME, ObjectIdGetDatum(relid),
+						   NameGetDatum(attname));
+
+	/* Attribute not found */
+	if (!HeapTupleIsValid(atup))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s has no attname %s",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+		return NULL;
+	}
+
+	attr = (Form_pg_attribute) GETSTRUCT(atup);
+	if (attr->attisdropped)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s attname %s is dropped",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+		return NULL;
+	}
+	*attnum = attr->attnum;
+
+	expr = get_attr_expr(rel, attr->attnum);
+
+	if (expr == NULL)
+	{
+		/* regular attribute */
+		typid = attr->atttypid;
+		*typmod = attr->atttypmod;
+		*typcoll = attr->attcollation;
+	}
+	else
+	{
+		typid = exprType(expr);
+		*typmod = exprTypmod(expr);
+
+		/*
+		 * If a collation has been specified for the index column, use that in
+		 * preference to anything else; but if not, fall back to whatever we
+		 * can get from the expression.
+		 */
+		if (OidIsValid(attr->attcollation))
+			*typcoll = attr->attcollation;
+		else
+			*typcoll = exprCollation(expr);
+	}
+	ReleaseSysCache(atup);
+
+	/* if it's a multirange, step down to the range type */
+	if (type_is_multirange(typid))
+		typid = get_multirange_range(typid);
+
+	return lookup_type_cache(typid,
+							 TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR);
+}
+
+/*
+ * Perform the cast of a known TextDatum into the type specified.
+ *
+ * If no errors are found, ok is set to true.
+ *
+ * Otherwise, set ok to false, capture the error found, and re-throw at the
+ * level specified by elevel.
+ */
+static Datum
+cast_stavalues(FmgrInfo *flinfo, Datum d, Oid typid, int32 typmod, 
+			   int elevel, bool *ok)
+{
+	LOCAL_FCINFO(fcinfo, 8);
+	char	   *s;
+	Datum		result;
+	ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+	escontext.details_wanted = true;
+
+	s = TextDatumGetCString(d);
+
+	InitFunctionCallInfoData(*fcinfo, flinfo, 3, InvalidOid,
+							 (Node *) &escontext, NULL);
+
+	fcinfo->args[0].value = CStringGetDatum(s);
+	fcinfo->args[0].isnull = false;
+	fcinfo->args[1].value = ObjectIdGetDatum(typid);
+	fcinfo->args[1].isnull = false;
+	fcinfo->args[2].value = Int32GetDatum(typmod);
+	fcinfo->args[2].isnull = false;
+
+	result = FunctionCallInvoke(fcinfo);
+
+	if (SOFT_ERROR_OCCURRED(&escontext))
+	{
+		if (elevel != ERROR)
+			escontext.error_data->elevel = elevel;
+		ThrowErrorData(escontext.error_data);
+		*ok = false;
+	}
+	else
+		*ok = true;
+
+	pfree(s);
+
+	return result;
+}
+
+
+/*
+ * Check array for any NULLs, and optionally for one-dimensionality.
+ *
+ * Report any failures at the level of elevel.
+ */
+static bool
+array_check(Datum datum, int one_dim, const char *statname, int elevel)
+{
+	ArrayType  *arr = DatumGetArrayTypeP(datum);
+	int16		elmlen;
+	char		elmalign;
+	bool		elembyval;
+	Datum	   *values;
+	bool	   *nulls;
+	int			nelems;
+
+	if (one_dim && (arr->ndim != 1))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be a multidimensional array", statname)));
+		return false;
+	}
+
+	get_typlenbyvalalign(ARR_ELEMTYPE(arr), &elmlen, &elembyval, &elmalign);
+
+	deconstruct_array(arr, ARR_ELEMTYPE(arr), elmlen, elembyval, elmalign,
+					  &values, &nulls, &nelems);
+
+	for (int i = 0; i < nelems; i++)
+		if (nulls[i])
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s array cannot contain NULL values", statname)));
+			return false;
+		}
+	return true;
+}
+
+/*
+ * Update the pg_statistic record.
+ */
+static void
+update_pg_statistic(Datum values[], bool nulls[])
+{
+	Relation	sd = table_open(StatisticRelationId, RowExclusiveLock);
+	CatalogIndexState indstate = CatalogOpenIndexes(sd);
+	HeapTuple	oldtup;
+	HeapTuple	stup;
+
+	/* Is there already a pg_statistic tuple for this attribute? */
+	oldtup = SearchSysCache3(STATRELATTINH,
+							 values[Anum_pg_statistic_starelid - 1],
+							 values[Anum_pg_statistic_staattnum - 1],
+							 values[Anum_pg_statistic_stainherit - 1]);
+
+	if (HeapTupleIsValid(oldtup))
+	{
+		/* Yes, replace it */
+		bool		replaces[Natts_pg_statistic];
+
+		for (int i = 0; i < Natts_pg_statistic; i++)
+			replaces[i] = true;
+
+		stup = heap_modify_tuple(oldtup, RelationGetDescr(sd),
+								 values, nulls, replaces);
+		ReleaseSysCache(oldtup);
+		CatalogTupleUpdateWithInfo(sd, &stup->t_self, stup, indstate);
+	}
+	else
+	{
+		/* No, insert new tuple */
+		stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
+		CatalogTupleInsertWithInfo(sd, stup, indstate);
+	}
+
+	heap_freetuple(stup);
+	CatalogCloseIndexes(indstate);
+	table_close(sd, NoLock);
+}
+
+/*
+ * Insert or Update Attribute Statistics
+ */
+static bool
+attribute_statistics_update(Datum relation_datum, bool relation_isnull,
+							Datum attname_datum, bool attname_isnull,
+							Datum inherited_datum, bool inherited_isnull,
+							Datum version_datum, bool version_isnull,
+							Datum null_frac_datum, bool null_frac_isnull,
+							Datum avg_width_datum, bool avg_width_isnull,
+							Datum n_distinct_datum, bool n_distinct_isnull,
+							Datum mc_vals_datum, bool mc_vals_isnull,
+							Datum mc_freqs_datum, bool mc_freqs_isnull, 
+							Datum histogram_bounds_datum, bool histogram_bounds_isnull,
+							Datum correlation_datum, bool correlation_isnull,
+							Datum mc_elems_datum, bool mc_elems_isnull,
+							Datum mc_elem_freqs_datum, bool mc_elem_freqs_isnull,
+							Datum elem_count_hist_datum, bool elem_count_hist_isnull,
+							Datum range_length_hist_datum, bool range_length_hist_isnull,
+							Datum range_empty_frac_datum, bool range_empty_frac_isnull,
+							Datum range_bounds_hist_datum, bool range_bounds_hist_isnull,
+							bool raise_errors)
+{
+	int			elevel = (raise_errors) ? ERROR : WARNING;
+
+	Oid			relation;
+	Name		attname;
+	int			version;
+
+	Relation	rel;
+
+	TypeCacheEntry *typcache;
+	TypeCacheEntry *elemtypcache = NULL;
+
+	int16		attnum;
+	int32		typmod;
+	Oid			typcoll;
+
+	FmgrInfo	finfo;
+
+	int			stakind_count;
+
+	Datum		values[Natts_pg_statistic];
+	bool		nulls[Natts_pg_statistic];
+
+	/*
+	 * The statkind index, we have only STATISTIC_NUM_SLOTS to hold these stats
+	 */
+	int			stakindidx = 0;
+
+	/*
+	 * Initialize output tuple.
+	 *
+	 * All non-repeating attributes should be NOT NULL. Only values for unused
+	 * statistics slots, and certain stakind-specific values for stanumbersN
+	 * and stavaluesN will ever be set NULL.
+	 */
+	for (int i = 0; i < Natts_pg_statistic; i++)
+	{
+		values[i] = (Datum) 0;
+		nulls[i] = false;
+	}
+
+	/*
+	 * Some parameters are "required" in that nothing can happen if any of
+	 * them are NULL.
+	 */
+	if (relation_isnull)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", relation_name)));
+		return false;
+	}
+	relation = DatumGetObjectId(relation_datum);
+
+	if (attname_isnull)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", attname_name)));
+		return false;
+	}
+	attname = DatumGetName(attname_datum);
+
+	if (inherited_isnull)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", inherited_name)));
+		return false;
+	}
+
+	/*
+	 * NULL version means assume current server version
+	 */
+	version = (version_isnull) ? PG_VERSION_NUM : DatumGetInt32(version_datum);
+	if (version < 90200)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Cannot export statistics prior to version 9.2")));
+		return false;
+	}
+
+	if (null_frac_isnull)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", null_frac_name)));
+		return false;
+	}
+
+	if (avg_width_isnull)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", avg_width_name)));
+		return false;
+	}
+
+	if (n_distinct_isnull)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", n_distinct_name)));
+		return false;
+	}
+
+	/*
+	 * Some parameters are linked, should both be NULL or NOT NULL.
+	 * Disagreement means that the statistic pair will fail so the 
+	 * NOT NULL one must be abandoned (set NULL) after an
+	 * ERROR/WARNING. By ensuring that the values are aligned it is 
+	 * possible to use one as a proxy for the other later.
+	 */
+
+	/*
+	 * STATISTIC_KIND_MCV = mc_vals + mc_freqs
+	 */
+	if (mc_vals_isnull != mc_freqs_isnull)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL when %s is NOT NULL",
+						mc_vals_isnull ? mc_vals_name : mc_freqs_name,
+						!mc_vals_isnull ? mc_vals_name : mc_freqs_name)));
+
+		mc_vals_isnull = true;
+		mc_freqs_isnull = true;
+	}
+
+	/*
+	 * STATISTIC_KIND_MCELEM = mc_elems + mc_elem_freqs
+	 */
+	if (mc_elems_isnull != mc_elem_freqs_isnull)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL when %s is NOT NULL",
+						mc_elems_isnull ? mc_elems_name : mc_elem_freqs_name,
+						!mc_elems_isnull ? mc_elems_name : mc_elem_freqs_name)));
+		mc_elems_isnull = true;
+		mc_elem_freqs_isnull = true;
+	}
+
+	/*
+	 * STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM =
+	 * range_length_histogram + range_empty_frac
+	 */
+	else if (range_length_hist_isnull != range_empty_frac_isnull)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL when %s is NOT NULL",
+						range_length_hist_isnull ?
+						range_length_hist_name : range_empty_frac_name,
+						!range_length_hist_isnull ?
+						range_length_hist_name : range_empty_frac_name)));
+		range_length_hist_isnull = true;
+		range_empty_frac_isnull = true;
+	}
+
+	/*
+	 * If a caller specifies more stakind-stats than we have slots to store
+	 * them, reject them all.
+	 */
+	stakind_count = (int) !mc_vals_isnull +
+		(int) !mc_elems_isnull +
+		(int) (!range_length_hist_isnull) +
+		(int) !histogram_bounds_isnull +
+		(int) !correlation_isnull +
+		(int) !elem_count_hist_isnull +
+		(int) !range_bounds_hist_isnull;
+
+	if (stakind_count > STATISTIC_NUM_SLOTS)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("imported statistics must have a maximum of %d slots "
+						"but %d given",
+						STATISTIC_NUM_SLOTS, stakind_count)));
+		return false;
+	}
+
+	rel = try_relation_open(relation, ShareUpdateExclusiveLock);
+
+	if (rel == NULL)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Parameter relation OID %u is invalid", relation)));
+		return false;
+	}
+
+	/*
+	 * Many of the values that are set for a particular stakind are entirely
+	 * derived from the attribute itself, or it's expression.
+	 */
+	typcache = get_attr_stat_type(rel, attname, elevel, &attnum, &typmod, &typcoll);
+	if (typcache == NULL)
+	{
+		relation_close(rel, NoLock);
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("unexpected typecache error")));
+		return false;
+	}
+
+	/*
+	 * Derive element type if we have stat kinds that need it.
+	 *
+	 * This duplicates some type-specific logic found in various typanalyze
+	 * functions which are called from vacuum's examine_attribute(), but using
+	 * that directly has proven awkward.
+	 */
+	if (!mc_elems_isnull || !elem_count_hist_isnull)
+	{
+		Oid			elemtypid;
+
+		if (typcache->type_id == TSVECTOROID)
+		{
+			/*
+			 * tsvectors always have a text oid base type and default
+			 * collation
+			 */
+			elemtypid = TEXTOID;
+			typcoll = DEFAULT_COLLATION_OID;
+		}
+		else if (typcache->typtype == TYPTYPE_RANGE)
+			elemtypid = get_range_subtype(typcache->type_id);
+		else
+			elemtypid = get_base_element_type(typcache->type_id);
+
+		/* not finding a basetype means we already had it */
+		if (elemtypid == InvalidOid)
+			elemtypid = typcache->type_id;
+
+		/* The stats need the eq_opr, any validation would need the lt_opr */
+		elemtypcache = lookup_type_cache(elemtypid, TYPECACHE_EQ_OPR);
+		if (elemtypcache == NULL)
+		{
+			/* warn and ignore any stats that can't be fulfilled */
+			if (!mc_elems_isnull)
+			{
+				ereport(elevel,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s cannot accept %s stats, ignored",
+								NameStr(*attname),
+								mc_elems_name)));
+				mc_elems_isnull = true;
+				mc_elem_freqs_isnull = true;
+			}
+
+			if (!elem_count_hist_isnull)
+			{
+				ereport(elevel,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s cannot accept %s stats, ignored",
+								NameStr(*attname),
+								elem_count_hist_name)));
+				elem_count_hist_isnull = true;
+			}
+		}
+	}
+
+	/*
+	 * histogram_bounds and correlation must have a type < operator
+	 */
+	if (typcache->lt_opr == InvalidOid)
+	{
+		if (!histogram_bounds_isnull)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Relation %s attname %s cannot "
+							"have stats of type %s, ignored.",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							histogram_bounds_name)));
+			histogram_bounds_isnull = true;
+		}
+
+		if (!correlation_isnull)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Relation %s attname %s cannot "
+							"have stats of type %s, ignored.",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							correlation_name)));
+			correlation_isnull = true;
+		}
+	}
+
+	/*
+	 * Scalar types can't have most_common_elems, most_common_elem_freqs, or
+	 * element_count_histogram
+	 */
+	if (type_is_scalar(typcache->type_id))
+	{
+		/* warn and ignore any stats that can't be fulfilled */
+		if (!mc_elems_isnull)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Relation %s attname %s is a scalar type, "
+							"cannot have stats of type %s, ignored",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							mc_elems_name)));
+			mc_elems_isnull = true;
+			mc_elem_freqs_isnull = true;
+		}
+
+		if (!elem_count_hist_isnull)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Relation %s attname %s is a scalar type, "
+							"cannot have stats of type %s, ignored",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							elem_count_hist_name)));
+			elem_count_hist_isnull = true;
+		}
+	}
+
+	/* Only range types can have range stats */
+	if ((typcache->typtype != TYPTYPE_RANGE) &&
+		(typcache->typtype != TYPTYPE_MULTIRANGE))
+	{
+		/* warn and ignore any stats that can't be fulfilled */
+		if (!range_length_hist_isnull)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Relation %s attname %s is not a range type, "
+							"cannot have stats of type %s",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							range_length_hist_name)));
+			range_length_hist_isnull = true;
+			range_empty_frac_isnull = true;
+		}
+
+		if (!range_bounds_hist_isnull)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Relation %s attname %s is not a range type, "
+							"cannot have stats of type %s",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							range_bounds_hist_name)));
+			range_bounds_hist_isnull = true;
+		}
+	}
+
+	if (!can_modify_relation(rel))
+	{
+		relation_close(rel, NoLock);
+		ereport(elevel,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						RelationGetRelationName(rel))));
+		return false;
+	}
+
+	/* Populate pg_statistic tuple */
+	values[Anum_pg_statistic_starelid - 1] = relation_datum;
+	values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(attnum);
+	values[Anum_pg_statistic_stainherit - 1] = inherited_datum;
+	values[Anum_pg_statistic_stanullfrac - 1] = null_frac_datum;
+	values[Anum_pg_statistic_stawidth - 1] = avg_width_datum;
+	values[Anum_pg_statistic_stadistinct - 1] = n_distinct_datum;
+
+	fmgr_info(F_ARRAY_IN, &finfo);
+
+	/*
+	 * STATISTIC_KIND_MCV
+	 *
+	 * most_common_vals: ANYARRAY::text most_common_freqs: real[]
+	 */
+	if (!mc_vals_isnull)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_MCV);
+		Datum		staop = ObjectIdGetDatum(typcache->eq_opr);
+		Datum		stacoll = ObjectIdGetDatum(typcoll);
+		bool		converted = false;
+		Datum		stanumbers = mc_freqs_datum;
+		Datum		stavalues = cast_stavalues(&finfo, mc_vals_datum,
+											   typcache->type_id, typmod,
+											   elevel, &converted);
+
+		if (converted &&
+			array_check(stavalues, false, mc_vals_name, elevel) &&
+			array_check(stanumbers, true, mc_freqs_name, elevel))
+		{
+			values[Anum_pg_statistic_stakind1 - 1 + stakindidx] = stakind;
+			values[Anum_pg_statistic_staop1 - 1 + stakindidx] = staop;
+			values[Anum_pg_statistic_stacoll1 - 1 + stakindidx] = stacoll;
+			values[Anum_pg_statistic_stanumbers1 - 1 + stakindidx] = stanumbers;
+			values[Anum_pg_statistic_stavalues1 - 1 + stakindidx] = stavalues;
+
+			stakindidx++;
+		}
+		else
+		{
+			/* Mark as skipped */
+			mc_vals_isnull = true;
+			mc_freqs_isnull = true;
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_HISTOGRAM
+	 *
+	 * histogram_bounds: ANYARRAY::text
+	 */
+	if (!histogram_bounds_isnull)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_HISTOGRAM);
+		Datum		staop = ObjectIdGetDatum(typcache->lt_opr);
+		Datum		stacoll = ObjectIdGetDatum(typcoll);
+		Datum		stavalues;
+		bool		converted = false;
+
+		stavalues = cast_stavalues(&finfo, histogram_bounds_datum,
+								   typcache->type_id, typmod, elevel,
+								   &converted);
+
+		if (converted &&
+			array_check(stavalues, false, histogram_bounds_name, elevel))
+		{
+			values[Anum_pg_statistic_stakind1 - 1 + stakindidx] = stakind;
+			values[Anum_pg_statistic_staop1 - 1 + stakindidx] = staop;
+			values[Anum_pg_statistic_stacoll1 - 1 + stakindidx] = stacoll;
+			nulls[Anum_pg_statistic_stanumbers1 - 1 + stakindidx] = true;
+			values[Anum_pg_statistic_stavalues1 - 1 + stakindidx] = stavalues;
+
+			stakindidx++;
+		}
+		else
+			histogram_bounds_isnull = true;
+	}
+
+	/*
+	 * STATISTIC_KIND_CORRELATION
+	 *
+	 * correlation: real
+	 */
+	if (!correlation_isnull)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_CORRELATION);
+		Datum		staop = ObjectIdGetDatum(typcache->lt_opr);
+		Datum		stacoll = ObjectIdGetDatum(typcoll);
+		Datum		elems[] = {correlation_datum};
+		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+		Datum		stanumbers = PointerGetDatum(arry);
+
+		values[Anum_pg_statistic_stakind1 - 1 + stakindidx] = stakind;
+		values[Anum_pg_statistic_staop1 - 1 + stakindidx] = staop;
+		values[Anum_pg_statistic_stacoll1 - 1 + stakindidx] = stacoll;
+		values[Anum_pg_statistic_stanumbers1 - 1 + stakindidx] = stanumbers;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + stakindidx] = true;
+
+		stakindidx++;
+	}
+
+	/*
+	 * STATISTIC_KIND_MCELEM
+	 *
+	 * most_common_elem_freqs: real[]
+	 *
+	 * most_common_elems     : ANYARRAY::text
+	 */
+	if (!mc_elems_isnull)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_MCELEM);
+		Datum		staop = ObjectIdGetDatum(elemtypcache->eq_opr);
+		Datum		stacoll = ObjectIdGetDatum(typcoll);
+		Datum		stanumbers = mc_elem_freqs_datum;
+		bool		converted = false;
+		Datum		stavalues;
+
+		stavalues = cast_stavalues(&finfo, mc_elems_datum,
+								   elemtypcache->type_id, typmod,
+								   elevel, &converted);
+
+		if (converted &&
+			array_check(stavalues, false, mc_elems_name, elevel) &&
+			array_check(stanumbers, true, mc_elem_freqs_name, elevel))
+		{
+			values[Anum_pg_statistic_stakind1 - 1 + stakindidx] = stakind;
+			values[Anum_pg_statistic_staop1 - 1 + stakindidx] = staop;
+			values[Anum_pg_statistic_stacoll1 - 1 + stakindidx] = stacoll;
+			values[Anum_pg_statistic_stanumbers1 - 1 + stakindidx] = stanumbers;
+			values[Anum_pg_statistic_stavalues1 - 1 + stakindidx] = stavalues;
+
+			stakindidx++;
+		}
+		else
+		{
+			/* the mc_elem stat did not write */
+			mc_elems_isnull = true;
+			mc_elem_freqs_isnull = true;
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_DECHIST
+	 *
+	 * elem_count_histogram:	real[]
+	 */
+	if (!elem_count_hist_isnull)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_DECHIST);
+		Datum		staop = ObjectIdGetDatum(elemtypcache->eq_opr);
+		Datum		stacoll = ObjectIdGetDatum(typcoll);
+		Datum		stanumbers = elem_count_hist_datum;
+
+		values[Anum_pg_statistic_stakind1 - 1 + stakindidx] = stakind;
+		values[Anum_pg_statistic_staop1 - 1 + stakindidx] = staop;
+		values[Anum_pg_statistic_stacoll1 - 1 + stakindidx] = stacoll;
+		values[Anum_pg_statistic_stanumbers1 - 1 + stakindidx] = stanumbers;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + stakindidx] = true;
+
+		stakindidx++;
+	}
+
+	/*
+	 * STATISTIC_KIND_BOUNDS_HISTOGRAM
+	 *
+	 * range_bounds_histogram: ANYARRAY::text
+	 *
+	 * This stakind appears before STATISTIC_KIND_BOUNDS_HISTOGRAM even though
+	 * it is numerically greater, and all other stakinds appear in numerical
+	 * order. We duplicate this quirk to make before/after tests of
+	 * pg_statistic records easier.
+	 */
+	if (!range_bounds_hist_isnull)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_BOUNDS_HISTOGRAM);
+		Datum		staop = ObjectIdGetDatum(InvalidOid);
+		Datum		stacoll = ObjectIdGetDatum(InvalidOid);
+
+		bool		converted = false;
+		Datum		stavalues;
+
+		stavalues = cast_stavalues(&finfo, range_bounds_hist_datum,
+								   typcache->type_id, typmod,
+								   elevel, &converted);
+
+		if (converted &&
+			array_check(stavalues, false, "range_bounds_histogram", elevel))
+		{
+			values[Anum_pg_statistic_stakind1 - 1 + stakindidx] = stakind;
+			values[Anum_pg_statistic_staop1 - 1 + stakindidx] = staop;
+			values[Anum_pg_statistic_stacoll1 - 1 + stakindidx] = stacoll;
+			nulls[Anum_pg_statistic_stanumbers1 - 1 + stakindidx] = true;
+			values[Anum_pg_statistic_stavalues1 - 1 + stakindidx] = stavalues;
+
+			stakindidx++;
+		}
+		else
+			range_bounds_hist_isnull = true;
+	}
+
+	/*
+	 * STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM
+	 *
+	 * range_empty_frac: real
+	 *
+	 * range_length_histogram:  double precision[]::text
+	 */
+	if (!range_length_hist_isnull)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM);
+		Datum		staop = ObjectIdGetDatum(Float8LessOperator);
+		Datum		stacoll = ObjectIdGetDatum(InvalidOid);
+
+		/* The anyarray is always a float8[] for this stakind */
+		Datum		elems[] = {range_empty_frac_datum};
+		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+		Datum		stanumbers = PointerGetDatum(arry);
+
+		bool		converted = false;
+		Datum		stavalues;
+
+		stavalues = cast_stavalues(&finfo, range_length_hist_datum, FLOAT8OID,
+								   0, elevel, &converted);
+
+		if (converted &&
+			array_check(stavalues, false, range_length_hist_name, elevel))
+		{
+			values[Anum_pg_statistic_staop1 - 1 + stakindidx] = staop;
+			values[Anum_pg_statistic_stakind1 - 1 + stakindidx] = stakind;
+			values[Anum_pg_statistic_stacoll1 - 1 + stakindidx] = stacoll;
+			values[Anum_pg_statistic_stanumbers1 - 1 + stakindidx] = stanumbers;
+			values[Anum_pg_statistic_stavalues1 - 1 + stakindidx] = stavalues;
+			stakindidx++;
+		}
+		else
+		{
+			range_empty_frac_isnull = true;
+			range_length_hist_isnull = true;
+		}
+	}
+
+	/* fill in all remaining slots */
+	while (stakindidx < STATISTIC_NUM_SLOTS)
+	{
+		values[Anum_pg_statistic_stakind1 - 1 + stakindidx] = Int16GetDatum(0);
+		values[Anum_pg_statistic_staop1 - 1 + stakindidx] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_stacoll1 - 1 + stakindidx] = ObjectIdGetDatum(InvalidOid);
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + stakindidx] = true;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + stakindidx] = true;
+
+		stakindidx++;
+	}
+
+	update_pg_statistic(values, nulls);
+
+	relation_close(rel, NoLock);
+
+	return true;
+}
+
+/*
+ * Import statistics for a given relation attribute.
+ *
+ * This will insert/replace a row in pg_statistic for the given relation and
+ * attribute name.
+ *
+ * The function takes input parameters that correspond to columns in the view
+ * pg_stats.
+ *
+ * Of those, the columns attname, inherited, null_frac, avg_width, and
+ * n_distinct all correspond to NOT NULL columns in pg_statistic. These
+ * parameters have no default value and passing NULL to them will result
+ * in an error.
+ *
+ * If there is no attribute with a matching attname in the relation, the
+ * function will raise an error. Likewise for setting inherited statistics
+ * on a table that is not partitioned.
+ *
+ * The remaining parameters all belong to a specific stakind. Some stakinds
+ * have multiple parameters, and in those cases both parameters must be
+ * NOT NULL or both NULL, otherwise an error will be raised.
+ *
+ * Omitting a parameter or explicitly passing NULL means that that particular
+ * stakind is not associated with the attribute.
+ *
+ * Parameters that are NOT NULL will be inspected for consistency checks,
+ * any of which can raise an error.
+ *
+ * Parameters corresponding to ANYARRAY columns are instead passed in as text
+ * values, which is a valid input string for an array of the type or element
+ * type of the attribute. Any error generated by the array_in() function will
+ * in turn fail the function.
+ */
+Datum
+pg_set_attribute_stats(PG_FUNCTION_ARGS)
+{
+	Datum	version_datum = (Datum) 0;
+	bool	version_isnull = true;
+	bool	raise_errors = true;
+
+	attribute_statistics_update(
+		PG_GETARG_DATUM(0), PG_ARGISNULL(0),
+		PG_GETARG_DATUM(1), PG_ARGISNULL(1),
+		PG_GETARG_DATUM(2), PG_ARGISNULL(2),
+		version_datum, version_isnull,
+		PG_GETARG_DATUM(3), PG_ARGISNULL(3),
+		PG_GETARG_DATUM(4), PG_ARGISNULL(4),
+		PG_GETARG_DATUM(5), PG_ARGISNULL(5),
+		PG_GETARG_DATUM(6), PG_ARGISNULL(6),
+		PG_GETARG_DATUM(7), PG_ARGISNULL(7),
+		PG_GETARG_DATUM(8), PG_ARGISNULL(8),
+		PG_GETARG_DATUM(9), PG_ARGISNULL(9),
+		PG_GETARG_DATUM(10), PG_ARGISNULL(10),
+		PG_GETARG_DATUM(11), PG_ARGISNULL(11),
+		PG_GETARG_DATUM(12), PG_ARGISNULL(12),
+		PG_GETARG_DATUM(13), PG_ARGISNULL(13),
+		PG_GETARG_DATUM(14), PG_ARGISNULL(14),
+		PG_GETARG_DATUM(15), PG_ARGISNULL(15),
+		raise_errors);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index e596e858d6..834950da72 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -93,7 +93,650 @@ WHERE oid = 'stats_import.test'::regclass;
        18 |       401 |             5
 (1 row)
 
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => '0'::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  Parameter relation OID 0 is invalid
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => NULL::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  relation cannot be NULL
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => NULL::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  attname cannot be NULL
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => NULL::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  inherited cannot be NULL
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => NULL::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  null_frac cannot be NULL
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => NULL::integer,
+    n_distinct => 0.3::real);
+ERROR:  avg_width cannot be NULL
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => NULL::real);
+ERROR:  n_distinct cannot be NULL
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT stanullfrac, stawidth, stadistinct
+FROM pg_statistic
+WHERE starelid = 'stats_import.test'::regclass;
+ stanullfrac | stawidth | stadistinct 
+-------------+----------+-------------
+         0.1 |        2 |         0.3
+(1 row)
+
+-- warn: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_freqs => '{0.1,0.2,0.3}'::real[]
+    );
+ERROR:  most_common_vals cannot be NULL when most_common_freqs is NOT NULL
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{1,2,3}'::text
+    );
+ERROR:  most_common_freqs cannot be NULL when most_common_vals is NOT NULL
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2023-09-30,2024-10-31,3}'::text,
+    most_common_freqs => '{0.2,0.1}'::real[]
+    );
+ERROR:  invalid input syntax for type integer: "2023-09-30"
+-- warning: mcv cast failure
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,four,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+ERROR:  invalid input syntax for type integer: "four"
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: histogram elements null value
+-- this generates no warnings, but perhaps it should
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,NULL,3,4}'::text
+    );
+ERROR:  histogram_bounds array cannot contain NULL values
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,2,3,4}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 0.5::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |         0.5 |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have mcelem
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{1,3}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+ERROR:  Relation test attname id is a scalar type, cannot have stats of type most_common_elems, ignored
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,two}'::text
+    );
+ERROR:  most_common_elem_freqs cannot be NULL when most_common_elems is NOT NULL
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
+    );
+ERROR:  most_common_elems cannot be NULL when most_common_elem_freqs is NOT NULL
+-- ok: mcelem
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             | {one,three}       | {0.3,0.2,0.2,0.3,0}    |                      |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ERROR:  Relation test attname id is a scalar type, cannot have stats of type elem_count_histogram, ignored
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
+ stats_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ERROR:  Relation test attname id is not a range type, cannot have stats of type range_length_histogram
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ERROR:  range_empty_frac cannot be NULL when range_length_histogram is NOT NULL
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real
+    );
+ERROR:  range_length_histogram cannot be NULL when range_empty_frac is NOT NULL
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
+(1 row)
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ERROR:  Relation test attname id is not a range type, cannot have stats of type range_bounds_histogram
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
+ stats_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      |                        |                  | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+(1 row)
+
+-- warn: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[],
+    histogram_bounds => '{1,2,3,4}'::text,
+    correlation => 1.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    range_empty_frac => -0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ERROR:  imported statistics must have a maximum of 5 slots but 6 given
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
+-- Generate statistics on table with data
+ANALYZE stats_import.test;
+CREATE TABLE stats_import.test_clone ( LIKE stats_import.test );
+CREATE INDEX is_odd_clone ON stats_import.test_clone(((comp).a % 2 = 1));
+--
+-- Copy stats from test to test_clone, and is_odd to is_odd_clone
+--
+SELECT s.schemaname, s.tablename, s.attname, s.inherited
+FROM pg_catalog.pg_stats AS s
+CROSS JOIN LATERAL
+    pg_catalog.pg_set_attribute_stats(
+        relation => ('stats_import.' || s.tablename || '_clone')::regclass::oid,
+        attname => s.attname,
+        inherited => s.inherited,
+        null_frac => s.null_frac,
+        avg_width => s.avg_width,
+        n_distinct => s.n_distinct,
+        most_common_vals => s.most_common_vals::text,
+        most_common_freqs => s.most_common_freqs,
+        histogram_bounds => s.histogram_bounds::text,
+        correlation => s.correlation,
+        most_common_elems => s.most_common_elems::text,
+        most_common_elem_freqs => s.most_common_elem_freqs,
+        elem_count_histogram => s.elem_count_histogram,
+        range_bounds_histogram => s.range_bounds_histogram::text,
+        range_empty_frac => s.range_empty_frac,
+        range_length_histogram => s.range_length_histogram::text) AS r
+WHERE s.schemaname = 'stats_import'
+AND s.tablename IN ('test', 'is_odd')
+ORDER BY s.tablename, s.attname, s.inherited;
+  schemaname  | tablename | attname | inherited 
+--------------+-----------+---------+-----------
+ stats_import | is_odd    | expr    | f
+ stats_import | test      | arange  | f
+ stats_import | test      | comp    | f
+ stats_import | test      | id      | f
+ stats_import | test      | name    | f
+ stats_import | test      | tags    | f
+(6 rows)
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+   relname    | num_stats 
+--------------+-----------
+ is_odd       |         1
+ is_odd_clone |         1
+ test         |         5
+ test_clone   |         5
+(4 rows)
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
 DROP SCHEMA stats_import CASCADE;
-NOTICE:  drop cascades to 2 other objects
+NOTICE:  drop cascades to 3 other objects
 DETAIL:  drop cascades to type stats_import.complex_type
 drop cascades to table stats_import.test
+drop cascades to table stats_import.test_clone
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index 282c031335..96795edf42 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -76,4 +76,548 @@ SELECT relpages, reltuples, relallvisible
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => '0'::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => NULL::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => NULL::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => NULL::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => NULL::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => NULL::integer,
+    n_distinct => 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => NULL::real);
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+SELECT stanullfrac, stawidth, stadistinct
+FROM pg_statistic
+WHERE starelid = 'stats_import.test'::regclass;
+
+-- warn: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_freqs => '{0.1,0.2,0.3}'::real[]
+    );
+
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{1,2,3}'::text
+    );
+
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2023-09-30,2024-10-31,3}'::text,
+    most_common_freqs => '{0.2,0.1}'::real[]
+    );
+
+-- warning: mcv cast failure
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,four,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: histogram elements null value
+-- this generates no warnings, but perhaps it should
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,NULL,3,4}'::text
+    );
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,2,3,4}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 0.5::real);
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: scalars can't have mcelem
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{1,3}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,two}'::text
+    );
+
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
+    );
+
+-- ok: mcelem
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- warn: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real
+    );
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[],
+    histogram_bounds => '{1,2,3,4}'::text,
+    correlation => 1.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    range_empty_frac => -0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+
+CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
+
+-- Generate statistics on table with data
+ANALYZE stats_import.test;
+
+CREATE TABLE stats_import.test_clone ( LIKE stats_import.test );
+
+CREATE INDEX is_odd_clone ON stats_import.test_clone(((comp).a % 2 = 1));
+
+--
+-- Copy stats from test to test_clone, and is_odd to is_odd_clone
+--
+SELECT s.schemaname, s.tablename, s.attname, s.inherited
+FROM pg_catalog.pg_stats AS s
+CROSS JOIN LATERAL
+    pg_catalog.pg_set_attribute_stats(
+        relation => ('stats_import.' || s.tablename || '_clone')::regclass::oid,
+        attname => s.attname,
+        inherited => s.inherited,
+        null_frac => s.null_frac,
+        avg_width => s.avg_width,
+        n_distinct => s.n_distinct,
+        most_common_vals => s.most_common_vals::text,
+        most_common_freqs => s.most_common_freqs,
+        histogram_bounds => s.histogram_bounds::text,
+        correlation => s.correlation,
+        most_common_elems => s.most_common_elems::text,
+        most_common_elem_freqs => s.most_common_elem_freqs,
+        elem_count_histogram => s.elem_count_histogram,
+        range_bounds_histogram => s.range_bounds_histogram::text,
+        range_empty_frac => s.range_empty_frac,
+        range_length_histogram => s.range_length_histogram::text) AS r
+WHERE s.schemaname = 'stats_import'
+AND s.tablename IN ('test', 'is_odd')
+ORDER BY s.tablename, s.attname, s.inherited;
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test_clone'::regclass;
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test'::regclass;
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd_clone'::regclass;
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd'::regclass;
+
 DROP SCHEMA stats_import CASCADE;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index eda8a039e5..e05073167b 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29837,6 +29837,69 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          </para>
        </entry>
       </row>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_attribute_stats</primary>
+        </indexterm>
+        <function>pg_set_attribute_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>
+         , <parameter>attname</parameter> <type>name</type>
+         , <parameter>inherited</parameter> <type>boolean</type>
+         , <parameter>null_frac</parameter> <type>real</type>
+         , <parameter>avg_width</parameter> <type>integer</type>
+         , <parameter>n_distinct</parameter> <type>real</type>
+         [, <parameter>most_common_vals</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_freqs</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>histogram_bounds</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>correlation</parameter> <type>real</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_elems</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_elem_freqs</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>elem_count_histogram</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_length_histogram</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_empty_frac</parameter> <type>real</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_bounds_histogram</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ] )
+        <returnvalue>void</returnvalue>
+         </para>
+         <para>
+          Replaces the <structname>pg_statistic</structname> row for the
+          <structname>pg_attribute</structname> row specified by
+          <parameter>relation</parameter>, <parameter>attname</parameter>
+          and <parameter>inherited</parameter>.
+         </para>
+         <para>
+          The purpose of this function is to allow the user to experiment with applying
+          hypothetical statistics to an attribute to see if it has an effect on the query
+          planner.
+         </para>
+         <para>
+          The remaining parameters all correspond to attributes of the same name
+          found in <link linkend="view-pg-stats"><structname>pg_stats</structname></link>,
+          and the values supplied in the parameter must meet the requirements of
+          the corresponding attribute. Any parameters not supplied are assumed to be NULL.
+         </para>
+         <para>
+          This function mimics the behavior of <command>ANALYZE</command> in its
+          effects on the values in <structname>pg_statistic</structname>, except
+          that the values are supplied as parameters rather than derived from
+          table sampling.
+         </para>
+         <para>
+          The caller must either be the owner of the relation, or have superuser
+          privileges.
+         </para>
+       </entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
-- 
2.45.2

v25-0003-Create-functions-pg_restore_-_stats.patchtext/x-patch; charset=US-ASCII; name=v25-0003-Create-functions-pg_restore_-_stats.patchDownload
From 572babd4793e21311d6a4295c3f3b4fca65657ce Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 27 Jul 2024 02:49:39 -0400
Subject: [PATCH v25 3/5] Create functions pg_restore_*_stats()

These functions are variadic requivalents of the pg_set_*_stats()
functions, which have a function signature each stat getting its own
defined parameter (or parameter pair, as the case may be).

Such a rigid function signature would make future compability difficult,
and future compatibility is just what pg_dump needs.

Instead, these functions have all input arguments put into a variable
argument list organized in name-value pairs. The leading or "name"
parameters must all be of type text and the string must exactly
correspond to the name of a statistics parameter in the corresponding
pg_set_X_stats function. The trailing or "value" parameter must be of
the type expected by the same-named parameter in the pg_set_X_stats
function. Names that do not match a parameter name and types that do not
match the expected type will emit a warning and be ignored.

The functions return a tuple indicating whether the row was written or
not, the names of statistics columns that were applied, the names of
statistics columns that were supplied but deficient in some way and
therefore discarded, and parameters that didn't match any know parameter
and thus were also discarded.

The intention of these functions is to be used in pg_dump/pg_restore and
pg_upgrade to allow the user to avoid having to run vacuumdb
--analyze-in-stages after an upgrade or restore.
---
 src/include/catalog/pg_proc.dat            |  20 +
 src/include/statistics/statistics.h        |   2 +
 src/backend/statistics/statistics.c        | 664 ++++++++++++++-
 src/test/regress/expected/stats_import.out | 936 ++++++++++++++++++++-
 src/test/regress/sql/stats_import.sql      | 695 ++++++++++++++-
 doc/src/sgml/func.sgml                     | 477 +++++++++++
 6 files changed, 2710 insertions(+), 84 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 11cbd9eded..9fefa0410c 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12250,5 +12250,25 @@
   proargtypes => 'oid name bool float4 int4 float4 text _float4 text float4 text _float4 _float4 text float4 text',
   proargnames => '{relation,attname,inherited,null_frac,avg_width,n_distinct,most_common_vals,most_common_freqs,histogram_bounds,correlation,most_common_elems,most_common_elem_freqs,elem_count_histogram,range_length_histogram,range_empty_frac,range_bounds_histogram}',
   prosrc => 'pg_set_attribute_stats' },
+{ oid => '8050',
+  descr => 'set statistics on relation',
+  proname => 'pg_restore_relation_stats', provariadic => 'any',
+  proisstrict => 'f', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record',
+  proargtypes => 'any',
+  proallargtypes => '{any,bool,_text,_text,_text}',
+  proargnames => '{kwargs,row_written,stats_applied,stats_rejected,params_rejected}',
+  proargmodes => '{v, o, o, o, o}',
+  prosrc => 'pg_restore_relation_stats' },
+{ oid => '8051',
+  descr => 'set statistics on attribute',
+  proname => 'pg_restore_attribute_stats', provariadic => 'any',
+  proisstrict => 'f', provolatile => 'v',
+  proparallel => 'u', prorettype => 'record',
+  proargtypes => 'any',
+  proallargtypes => '{any,bool,_text,_text,_text}',
+  proargnames => '{kwargs,row_written,stats_applied,stats_rejected,params_rejected}',
+  proargmodes => '{v, o, o, o, o}',
+  prosrc => 'pg_restore_attribute_stats' },
 
 ]
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 73d3b541dd..06425173e6 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -129,5 +129,7 @@ extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
 extern Datum pg_set_relation_stats(PG_FUNCTION_ARGS);
 extern Datum pg_set_attribute_stats(PG_FUNCTION_ARGS);
+extern Datum pg_restore_relation_stats(PG_FUNCTION_ARGS);
+extern Datum pg_restore_attribute_stats(PG_FUNCTION_ARGS);
 
 #endif							/* STATISTICS_H */
diff --git a/src/backend/statistics/statistics.c b/src/backend/statistics/statistics.c
index bfabe0b563..9767c8f4f3 100644
--- a/src/backend/statistics/statistics.c
+++ b/src/backend/statistics/statistics.c
@@ -26,6 +26,7 @@
 #include "catalog/pg_database.h"
 #include "catalog/pg_operator.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_type_d.h"
 #include "fmgr.h"
 #include "funcapi.h"
 #include "miscadmin.h"
@@ -39,6 +40,7 @@
 #include "utils/float.h"
 #include "utils/fmgroids.h"
 #include "utils/lsyscache.h"
+#include "utils/palloc.h"
 #include "utils/rangetypes.h"
 #include "utils/rel.h"
 #include "utils/syscache.h"
@@ -46,7 +48,8 @@
 
 /*
  * Names of parameters found in the functions pg_set_relation_stats and
- * pg_set_attribute_stats
+ * pg_set_attribute_stats, as well as keyword names used in
+ * pg_restore_relation_stats and pg_restore_attribute_stats.
  */
 const char *relation_name = "relation";
 const char *relpages_name = "relpages";
@@ -67,6 +70,17 @@ const char *elem_count_hist_name = "elem_count_histogram";
 const char *range_length_hist_name = "range_length_histogram";
 const char *range_empty_frac_name = "range_empty_frac";
 const char *range_bounds_hist_name = "range_bounds_histogram";
+const char *version_name = "version";
+
+typedef struct kwarg_data {
+	const char *argname;
+	const char *typname;
+	Oid			typoid;
+	bool		required;
+	Datum		datum;
+	bool		isnull;
+	bool		set;
+} kwarg_data;
 
 /*
  * A role has privileges to set statistics on the relation if any of the
@@ -95,7 +109,10 @@ relation_statistics_update(Datum relation_datum, bool relation_isnull,
 						   Datum relpages_datum, bool relpages_isnull,
 						   Datum reltuples_datum, bool reltuples_isnull,
 						   Datum relallvisible_datum, bool relallvisible_isnull,
-						   bool raise_errors, bool in_place)
+						   bool raise_errors, bool in_place,
+						   bool *relpages_applied,
+						   bool *reltuples_applied,
+						   bool *relallvisible_applied)
 {
 	int			elevel = (raise_errors) ? ERROR : WARNING;
 	Oid			relation;
@@ -108,6 +125,10 @@ relation_statistics_update(Datum relation_datum, bool relation_isnull,
 	HeapTuple	ctup;
 	Form_pg_class pgcform;
 
+	*relpages_applied = false;
+	*reltuples_applied = false;
+	*relallvisible_applied = false;
+
 	/* If we don't know what relation we're modifying, give up */
 	if (relation_isnull)
 	{
@@ -264,7 +285,11 @@ relation_statistics_update(Datum relation_datum, bool relation_isnull,
 
 	table_close(rel, NoLock);
 
-	PG_RETURN_BOOL(true);
+	*relpages_applied = true;
+	*reltuples_applied = true;
+	*relallvisible_applied = true;
+
+	return true;
 }
 
 /*
@@ -283,12 +308,19 @@ pg_set_relation_stats(PG_FUNCTION_ARGS)
 	bool	raise_errors = true;
 	bool	in_place = false;
 
+	bool	relpages_applied = false;
+	bool	reltuples_applied = false;
+	bool	relallvisible_applied = false;
+
 	relation_statistics_update(PG_GETARG_DATUM(0), PG_ARGISNULL(0),
 							   version_datum, version_isnull,
 							   PG_GETARG_DATUM(1), PG_ARGISNULL(1),
 							   PG_GETARG_DATUM(2), PG_ARGISNULL(2),
 							   PG_GETARG_DATUM(3), PG_ARGISNULL(3),
-							   raise_errors, in_place);
+							   raise_errors, in_place,
+							   &relpages_applied,
+							   &reltuples_applied,
+							   &relallvisible_applied);
 
 	PG_RETURN_VOID();
 }
@@ -471,7 +503,7 @@ cast_stavalues(FmgrInfo *flinfo, Datum d, Oid typid, int32 typmod,
  * Report any failures at the level of elevel.
  */
 static bool
-array_check(Datum datum, int one_dim, const char *statname, int elevel)
+array_check(Datum datum, bool one_dim, const char *statname, int elevel)
 {
 	ArrayType  *arr = DatumGetArrayTypeP(datum);
 	int16		elmlen;
@@ -568,7 +600,20 @@ attribute_statistics_update(Datum relation_datum, bool relation_isnull,
 							Datum range_length_hist_datum, bool range_length_hist_isnull,
 							Datum range_empty_frac_datum, bool range_empty_frac_isnull,
 							Datum range_bounds_hist_datum, bool range_bounds_hist_isnull,
-							bool raise_errors)
+							bool raise_errors,
+							bool *null_frac_applied,
+							bool *avg_width_applied,
+							bool *n_distinct_applied,
+							bool *mc_vals_applied,
+							bool *mc_freqs_applied,
+							bool *histogram_bounds_applied,
+							bool *correlation_applied,
+							bool *mc_elems_applied,
+							bool *mc_elem_freqs_applied,
+							bool *elem_count_hist_applied,
+							bool *range_length_hist_applied,
+							bool *range_empty_frac_applied,
+							bool *range_bounds_hist_applied)
 {
 	int			elevel = (raise_errors) ? ERROR : WARNING;
 
@@ -610,6 +655,20 @@ attribute_statistics_update(Datum relation_datum, bool relation_isnull,
 		nulls[i] = false;
 	}
 
+	*null_frac_applied = false;
+	*avg_width_applied = false;
+	*n_distinct_applied = false;
+	*mc_vals_applied = false;
+	*mc_freqs_applied = false;
+	*histogram_bounds_applied = false;
+	*correlation_applied = false;
+	*mc_elems_applied = false;
+	*mc_elem_freqs_applied = false;
+	*elem_count_hist_applied = false;
+	*range_length_hist_applied = false;
+	*range_empty_frac_applied = false;
+	*range_bounds_hist_applied = false;
+
 	/*
 	 * Some parameters are "required" in that nothing can happen if any of
 	 * them are NULL.
@@ -951,6 +1010,9 @@ attribute_statistics_update(Datum relation_datum, bool relation_isnull,
 	 * STATISTIC_KIND_MCV
 	 *
 	 * most_common_vals: ANYARRAY::text most_common_freqs: real[]
+	 *
+	 * As detailed in statistics.h, the stavalues and stanumbers arrays cannot
+	 * contain NULL values.
 	 */
 	if (!mc_vals_isnull)
 	{
@@ -987,6 +1049,9 @@ attribute_statistics_update(Datum relation_datum, bool relation_isnull,
 	 * STATISTIC_KIND_HISTOGRAM
 	 *
 	 * histogram_bounds: ANYARRAY::text
+	 *
+	 * As detailed in statistics.h, the stavalues array cannot
+	 * contain NULL values.
 	 */
 	if (!histogram_bounds_isnull)
 	{
@@ -1044,6 +1109,9 @@ attribute_statistics_update(Datum relation_datum, bool relation_isnull,
 	 * most_common_elem_freqs: real[]
 	 *
 	 * most_common_elems     : ANYARRAY::text
+	 *
+	 * As detailed in statistics.h, the stavalues and stanumbers arrays cannot
+	 * contain NULL values.
 	 */
 	if (!mc_elems_isnull)
 	{
@@ -1082,6 +1150,9 @@ attribute_statistics_update(Datum relation_datum, bool relation_isnull,
 	 * STATISTIC_KIND_DECHIST
 	 *
 	 * elem_count_histogram:	real[]
+	 *
+	 * As detailed in statistics.h, the stanumbers array cannot contain NULL
+	 * values.
 	 */
 	if (!elem_count_hist_isnull)
 	{
@@ -1090,13 +1161,18 @@ attribute_statistics_update(Datum relation_datum, bool relation_isnull,
 		Datum		stacoll = ObjectIdGetDatum(typcoll);
 		Datum		stanumbers = elem_count_hist_datum;
 
-		values[Anum_pg_statistic_stakind1 - 1 + stakindidx] = stakind;
-		values[Anum_pg_statistic_staop1 - 1 + stakindidx] = staop;
-		values[Anum_pg_statistic_stacoll1 - 1 + stakindidx] = stacoll;
-		values[Anum_pg_statistic_stanumbers1 - 1 + stakindidx] = stanumbers;
-		nulls[Anum_pg_statistic_stavalues1 - 1 + stakindidx] = true;
+		if (array_check(stanumbers, true, elem_count_hist_name, elevel))
+		{
+			values[Anum_pg_statistic_stakind1 - 1 + stakindidx] = stakind;
+			values[Anum_pg_statistic_staop1 - 1 + stakindidx] = staop;
+			values[Anum_pg_statistic_stacoll1 - 1 + stakindidx] = stacoll;
+			values[Anum_pg_statistic_stanumbers1 - 1 + stakindidx] = stanumbers;
+			nulls[Anum_pg_statistic_stavalues1 - 1 + stakindidx] = true;
 
-		stakindidx++;
+			stakindidx++;
+		}
+		else
+			elem_count_hist_isnull = true;
 	}
 
 	/*
@@ -1108,6 +1184,9 @@ attribute_statistics_update(Datum relation_datum, bool relation_isnull,
 	 * it is numerically greater, and all other stakinds appear in numerical
 	 * order. We duplicate this quirk to make before/after tests of
 	 * pg_statistic records easier.
+	 *
+	 * As detailed in statistics.h, the stavalues array cannot contain NULL
+	 * values.
 	 */
 	if (!range_bounds_hist_isnull)
 	{
@@ -1143,6 +1222,9 @@ attribute_statistics_update(Datum relation_datum, bool relation_isnull,
 	 * range_empty_frac: real
 	 *
 	 * range_length_histogram:  double precision[]::text
+	 *
+	 * As detailed in statistics.h, the stavalues array cannot contain NULL
+	 * values.
 	 */
 	if (!range_length_hist_isnull)
 	{
@@ -1169,6 +1251,7 @@ attribute_statistics_update(Datum relation_datum, bool relation_isnull,
 			values[Anum_pg_statistic_stacoll1 - 1 + stakindidx] = stacoll;
 			values[Anum_pg_statistic_stanumbers1 - 1 + stakindidx] = stanumbers;
 			values[Anum_pg_statistic_stavalues1 - 1 + stakindidx] = stavalues;
+
 			stakindidx++;
 		}
 		else
@@ -1190,6 +1273,21 @@ attribute_statistics_update(Datum relation_datum, bool relation_isnull,
 		stakindidx++;
 	}
 
+	/* Any stats whose null flags remained false got written */
+	*null_frac_applied = !null_frac_isnull;
+	*avg_width_applied = !avg_width_isnull;
+	*n_distinct_applied = !n_distinct_isnull;
+	*mc_vals_applied = !mc_vals_isnull;
+	*mc_freqs_applied = !mc_freqs_isnull;
+	*histogram_bounds_applied = !histogram_bounds_isnull;
+	*correlation_applied = !correlation_isnull;
+	*mc_elems_applied = !mc_elems_isnull;
+	*mc_elem_freqs_applied = !mc_elem_freqs_isnull;
+	*elem_count_hist_applied = !elem_count_hist_isnull;
+	*range_length_hist_applied = !range_length_hist_isnull;
+	*range_empty_frac_applied = !range_empty_frac_isnull;
+	*range_bounds_hist_applied = !range_bounds_hist_isnull;
+
 	update_pg_statistic(values, nulls);
 
 	relation_close(rel, NoLock);
@@ -1237,6 +1335,20 @@ pg_set_attribute_stats(PG_FUNCTION_ARGS)
 	bool	version_isnull = true;
 	bool	raise_errors = true;
 
+	bool null_frac_applied = false;
+	bool avg_width_applied = false;
+	bool n_distinct_applied = false;
+	bool mc_vals_applied = false;
+	bool mc_freqs_applied = false;
+	bool histogram_bounds_applied = false;
+	bool correlation_applied = false;
+	bool mc_elems_applied = false;
+	bool mc_elem_freqs_applied = false;
+	bool elem_count_hist_applied = false;
+	bool range_length_hist_applied = false;
+	bool range_empty_frac_applied = false;
+	bool range_bounds_hist_applied = false;
+
 	attribute_statistics_update(
 		PG_GETARG_DATUM(0), PG_ARGISNULL(0),
 		PG_GETARG_DATUM(1), PG_ARGISNULL(1),
@@ -1255,7 +1367,533 @@ pg_set_attribute_stats(PG_FUNCTION_ARGS)
 		PG_GETARG_DATUM(13), PG_ARGISNULL(13),
 		PG_GETARG_DATUM(14), PG_ARGISNULL(14),
 		PG_GETARG_DATUM(15), PG_ARGISNULL(15),
-		raise_errors);
+		raise_errors,
+		&null_frac_applied,
+		&avg_width_applied,
+		&n_distinct_applied,
+		&mc_vals_applied,
+		&mc_freqs_applied,
+		&histogram_bounds_applied,
+		&correlation_applied,
+		&mc_elems_applied,
+		&mc_elem_freqs_applied,
+		&elem_count_hist_applied,
+		&range_length_hist_applied,
+		&range_empty_frac_applied,
+		&range_bounds_hist_applied);
 
 	PG_RETURN_VOID();
 }
+
+/*
+ * Append to a potentially NULL Text Array
+ */
+static ArrayBuildState*
+text_array_append(ArrayBuildState *arr, const char *s)
+{
+	Datum	d = CStringGetTextDatum(s);
+
+	if (arr == NULL)
+		arr = initArrayResult(TEXTOID, CurrentMemoryContext, false);
+
+	return accumArrayResult(arr, d, false, TEXTOID, CurrentMemoryContext);
+}
+
+
+/*
+ * Convert a potentially NULL ArrayBuildState to Datum
+ */
+static Datum
+text_array_to_datum(ArrayBuildState *arr)
+{
+	if (arr == NULL)
+		return (Datum) 0;
+	return makeArrayResult(arr, CurrentMemoryContext);
+}
+
+/*
+ * Given a table of kwarg_data, and the output of extract_variadic_args(),
+ * walk the list of variadics, matching those to kwarg_data and filling out
+ * the datums and nulls accordingly. Parameters that are rejected may be
+ * appended to the params_rejected_arr.
+ *
+ * Returns true if the the whole table was processed and the kwargs can
+ * be used for the stats-setting function call.
+ */
+static bool
+process_kwargs(FunctionCallInfo fcinfo, int kwarg_start,
+			   kwarg_data kwargs[], int nkwargs,
+			   ArrayBuildState **rejected)
+{
+	Datum	   *args;
+	bool	   *argnulls;
+	Oid		   *types;
+	int			nargs;
+
+	nargs = extract_variadic_args(fcinfo, kwarg_start, true,
+							      &args, &types, &argnulls);
+
+	/* Arguments must be in key-value pairs */
+	if (nargs % 2 == 1)
+		return false;
+
+	/* loop through args, matching params to their arg indexes */
+	for (int i = 0; i < nargs; i += 2)
+	{
+		char   *statname;
+		int		argidx = i + 1;
+		bool	found = false;
+
+		if (types[i] != TEXTOID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("stat names must be of type text")));
+			return false;
+		}
+		statname = TextDatumGetCString(args[i]);
+
+		for (int j = 0; !found && j < nkwargs; j++)
+		{
+			if (strcmp(statname, kwargs[j].argname) != 0)
+				continue;
+
+			found = true;
+
+			/*
+			 * Go with the first matching set of each arg,
+			 * ignoring duplicates, do not add to rejected list because
+			 * seeing the same parameter in two places is confusing.
+			 */
+			if (kwargs[j].set)
+			{
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("stat name %s already used, subsequent values ignored",
+								statname)));
+				*rejected = text_array_append(*rejected, statname);
+			}
+			else if (types[argidx] != kwargs[j].typoid)
+			{
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be of type oid %d (%s) but is type oid %d",
+								statname, kwargs[j].typoid, kwargs[j].typname,
+								types[argidx])));
+
+				*rejected = text_array_append(*rejected, statname);
+			}
+			else
+			{
+				kwargs[j].set = true;
+				kwargs[j].datum = args[argidx];
+				kwargs[j].isnull = argnulls[argidx];
+			}
+		}
+
+		if (!found)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("stat name %s is not recognized, values ignored",
+							statname)));
+			*rejected = text_array_append(*rejected, statname);
+		}
+		pfree(statname);
+	}
+
+	/*
+	 * Iterate over the list to see if any required parameters were omitted.
+	 */
+	for (int j = 0; j < nkwargs; j++)
+	{
+		if (kwargs[j].required)
+	  	{
+	  		if (!kwargs[j].set)
+			{
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("parameter %s is required but not set",
+								kwargs[j].argname)));
+				return false;
+			}
+		}
+	}
+
+	return true;
+}
+
+
+ /*
+ * Restore statistics for a given pg_class entry.
+ *
+ * This does an in-place (i.e. non-transactional) update of pg_class, just as
+ * is done in ANALYZE. It is also done with elevel set to WARNING, such
+ * that data errors will generate warnings and may prevent the other stats
+ * from being written, but it will not actually fail the function. This is a
+ * requirement for use in binary upgrade.
+ */
+Datum
+pg_restore_relation_stats(PG_FUNCTION_ARGS)
+{
+	#define NUM_RELARGS 5
+
+	kwarg_data	relargs[NUM_RELARGS] = {
+			{relation_name, "oid", OIDOID, true, (Datum) 0, true, false },
+			{version_name, "integer", INT4OID, true, (Datum) 0, true, false},
+			{relpages_name, "integer", INT4OID, true, (Datum) 0, true, false},
+			{reltuples_name, "real", FLOAT4OID, true, (Datum) 0, true, false},
+			{relallvisible_name, "integer", INT4OID, true, (Datum) 0, true, false}
+		};
+
+	bool		kwarg_ok;
+
+	TupleDesc	tupdesc;
+	HeapTuple	htup;
+	Datum		retvalues[4];
+	bool		retnulls[4];
+
+	bool	ok;
+	bool	relpages_applied = false;
+	bool	reltuples_applied = false;
+	bool	relallvisible_applied = false;
+
+	ArrayBuildState	   *params_rejected_arr = NULL;
+
+	/* Initial set of retvals in case we error out */
+	retvalues[0] = BoolGetDatum(false);
+	retnulls[0] = false;
+	retvalues[1] = (Datum) 0; /* _text */
+	retnulls[1] = true;
+	retvalues[2] = (Datum) 0; /* _text */
+	retnulls[2] = true;
+	retvalues[3] = (Datum) 0; /* _text */
+	retnulls[3] = true;
+
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	kwarg_ok = process_kwargs(fcinfo, 0, relargs, NUM_RELARGS,
+							  &params_rejected_arr);
+
+	if (!kwarg_ok)
+	{
+		retvalues[3] = text_array_to_datum(params_rejected_arr);
+		retnulls[3] = (params_rejected_arr == NULL); 
+		htup = heap_form_tuple(tupdesc, retvalues, retnulls);
+		PG_RETURN_DATUM(HeapTupleGetDatum(htup));
+	}
+
+	ok = relation_statistics_update(relargs[0].datum, relargs[0].isnull,
+									relargs[1].datum, relargs[1].isnull,
+									relargs[2].datum, relargs[2].isnull,
+									relargs[3].datum, relargs[3].isnull,
+									relargs[4].datum, relargs[4].isnull,
+									false, /* raise_errors */
+									true, /* in_place */
+									&relpages_applied,
+									&reltuples_applied,
+									&relallvisible_applied);
+
+	if (ok)
+	{
+		/*
+		 * The record was written, report what made it in and what didn't.
+		 */
+		ArrayBuildState	   *applied_arr = NULL;
+		ArrayBuildState	   *rejected_arr = NULL;
+
+		if (!relargs[2].isnull)
+		{
+			if (relpages_applied)
+				applied_arr = text_array_append(applied_arr, relpages_name);
+			else
+				rejected_arr = text_array_append(rejected_arr, relpages_name);
+		}
+
+		if (!relargs[3].isnull)
+		{
+			if (reltuples_applied)
+				applied_arr = text_array_append(applied_arr, reltuples_name);
+			else
+				rejected_arr = text_array_append(rejected_arr, reltuples_name);
+		}
+
+		if (!relargs[4].isnull)
+		{
+			if (relallvisible_applied)
+				applied_arr = text_array_append(applied_arr, relallvisible_name);
+			else
+				rejected_arr = text_array_append(rejected_arr, relallvisible_name);
+		}
+
+		retvalues[0] = true;
+		retvalues[1] = text_array_to_datum(applied_arr);
+		retnulls[1] = (applied_arr == NULL);
+		retvalues[2] = text_array_to_datum(rejected_arr);
+		retnulls[2] = (rejected_arr == NULL);
+		retvalues[3] = text_array_to_datum(params_rejected_arr);
+		retnulls[3] = (params_rejected_arr == NULL);
+	}
+
+	htup = heap_form_tuple(tupdesc, retvalues, retnulls);
+	PG_RETURN_DATUM(HeapTupleGetDatum(htup));
+}
+
+/*
+ * Import statistics for a given relation attribute.
+ *
+ * This will insert/replace a row in pg_statistic for the given combinarion
+ * of relation, attribute, and inherited flag.
+ *
+ * The version parameter is for future use in the events as future versions
+ * may change them meaning of certain parameters.
+ *
+ * The variadic parameters all represent name-value pairs, with the names
+ * corresponding to attributes in pg_stats. Unkown names will generate a
+ * warning.
+ *
+ * Parameters null_frac, avg_width, and n_distinct are required because
+ * those attributes have no default value in pg_statistic.
+ *
+ * The remaining parameters all belong to a specific stakind, and all are
+ * optional. Some stakinds have multiple parameters, and in those cases
+ * both parameters must be specified if one of them is, otherwise a
+ * warning is generated but the rest of the stats may still be imported.
+ *
+ * If there is no attribute with a matching attname in the relation, the
+ * function will raise a warning and return false.
+ *
+ * Parameters corresponding to ANYARRAY columns are instead passed in as text
+ * values, which is a valid input string for an array of the type or element
+ * type of the attribute.
+ */
+Datum
+pg_restore_attribute_stats(PG_FUNCTION_ARGS)
+{
+	#define NUM_ATTARGS 17
+
+	kwarg_data attargs[NUM_ATTARGS] = {
+			{relation_name, "oid", OIDOID, true, (Datum) 0, true, false },
+			{attname_name, "name", NAMEOID, true, (Datum) 0, true, false },
+			{inherited_name, "inherited", BOOLOID, true, (Datum) 0, true, false},
+			{version_name, "integer", INT4OID, false, (Datum) 0, true, false},
+			{null_frac_name, "real", FLOAT4OID, true, (Datum) 0, true, false},
+			{avg_width_name, "integer", INT4OID, true, (Datum) 0, true, false},
+			{n_distinct_name, "real", FLOAT4OID, true, (Datum) 0, true, false},
+			{mc_vals_name, "text", TEXTOID, false, (Datum) 0, true, false},
+			{mc_freqs_name, "real[]", FLOAT4ARRAYOID, false, (Datum) 0, true, false},
+			{histogram_bounds_name, "text", TEXTOID, false, (Datum) 0, true, false},
+			{correlation_name, "real", FLOAT4OID, false, (Datum) 0, true, false},
+			{mc_elems_name, "text", TEXTOID, false, (Datum) 0, true, false},
+			{mc_elem_freqs_name, "real[]", FLOAT4ARRAYOID, false, (Datum) 0, true, false},
+			{elem_count_hist_name, "real[]", FLOAT4ARRAYOID, false, (Datum) 0, true, false},
+			{range_length_hist_name, "text", TEXTOID, false, (Datum) 0, true, false},
+			{range_empty_frac_name, "real", FLOAT4OID, false, (Datum) 0, true, false},
+			{range_bounds_hist_name, "text", TEXTOID, false, (Datum) 0, true, false},
+		};
+
+	bool null_frac_applied = false;
+	bool avg_width_applied = false;
+	bool n_distinct_applied = false;
+	bool mc_vals_applied = false;
+	bool mc_freqs_applied = false;
+	bool histogram_bounds_applied = false;
+	bool correlation_applied = false;
+	bool mc_elems_applied = false;
+	bool mc_elem_freqs_applied = false;
+	bool elem_count_hist_applied = false;
+	bool range_length_hist_applied = false;
+	bool range_empty_frac_applied = false;
+	bool range_bounds_hist_applied = false;
+
+	bool		raise_errors = false;
+	HeapTuple	htup;
+	TupleDesc	tupdesc;
+	Datum		retvalues[4];
+	bool		retnulls[4];
+	bool		kwarg_ok = false;
+	bool		ok = false;
+
+	ArrayBuildState	   *params_rejected_arr = NULL;
+
+	/* Initial set of retvals in case we error out */
+	retvalues[0] = BoolGetDatum(false);
+	retnulls[0] = false;
+	retvalues[1] = (Datum) 0; /* _text */
+	retnulls[1] = true;
+	retvalues[2] = (Datum) 0; /* _text */
+	retnulls[2] = true;
+	retvalues[3] = (Datum) 0; /* _text */
+	retnulls[3] = true;
+
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	kwarg_ok = process_kwargs(fcinfo, 0, attargs, NUM_ATTARGS,
+							  &params_rejected_arr);
+
+	if (!kwarg_ok)
+	{
+		retvalues[3] = text_array_to_datum(params_rejected_arr);
+		retnulls[3] = (params_rejected_arr == NULL); 
+		htup = heap_form_tuple(tupdesc, retvalues, retnulls);
+		PG_RETURN_DATUM(HeapTupleGetDatum(htup));
+	}
+
+	ok = attribute_statistics_update(attargs[0].datum, attargs[0].isnull,
+									 attargs[1].datum, attargs[1].isnull,
+									 attargs[2].datum, attargs[2].isnull,
+									 attargs[3].datum, attargs[3].isnull,
+									 attargs[4].datum, attargs[4].isnull,
+									 attargs[5].datum, attargs[5].isnull,
+									 attargs[6].datum, attargs[6].isnull,
+									 attargs[7].datum, attargs[7].isnull,
+									 attargs[8].datum, attargs[8].isnull,
+									 attargs[9].datum, attargs[9].isnull,
+									 attargs[10].datum, attargs[10].isnull,
+									 attargs[11].datum, attargs[11].isnull,
+									 attargs[12].datum, attargs[12].isnull,
+									 attargs[13].datum, attargs[13].isnull,
+									 attargs[14].datum, attargs[14].isnull,
+									 attargs[15].datum, attargs[15].isnull,
+									 attargs[16].datum, attargs[16].isnull,
+									 raise_errors,
+									 &null_frac_applied,
+									 &avg_width_applied,
+									 &n_distinct_applied,
+									 &mc_vals_applied,
+									 &mc_freqs_applied,
+									 &histogram_bounds_applied,
+									 &correlation_applied,
+									 &mc_elems_applied,
+									 &mc_elem_freqs_applied,
+									 &elem_count_hist_applied,
+									 &range_length_hist_applied,
+									 &range_empty_frac_applied,
+									 &range_bounds_hist_applied);
+	
+	if (ok)
+	{
+		/*
+		 * The record was written, report what made it in and what didn't.
+		 */
+		ArrayBuildState	   *applied_arr = NULL;
+		ArrayBuildState	   *rejected_arr = NULL;
+
+		if (!attargs[4].isnull)
+		{
+			if (null_frac_applied)
+				applied_arr = text_array_append(applied_arr, null_frac_name);
+			else
+				rejected_arr = text_array_append(rejected_arr, null_frac_name);
+		}
+
+		if (!attargs[5].isnull)
+		{
+			if (avg_width_applied)
+				applied_arr = text_array_append(applied_arr, avg_width_name);
+			else
+				rejected_arr = text_array_append(rejected_arr, avg_width_name);
+		}
+
+		if (!attargs[6].isnull)
+		{
+			if (n_distinct_applied)
+				applied_arr = text_array_append(applied_arr, n_distinct_name);
+			else
+				rejected_arr = text_array_append(rejected_arr, n_distinct_name);
+		}
+
+		if (!attargs[7].isnull)
+		{
+			if (mc_vals_applied)
+				applied_arr = text_array_append(applied_arr, mc_vals_name);
+			else
+				rejected_arr = text_array_append(rejected_arr, mc_vals_name);
+		}
+
+		if (!attargs[8].isnull)
+		{
+			if (mc_freqs_applied)
+				applied_arr = text_array_append(applied_arr, mc_freqs_name);
+			else
+				rejected_arr = text_array_append(rejected_arr, mc_freqs_name);
+		}
+
+		if (!attargs[9].isnull)
+		{
+			if (histogram_bounds_applied)
+				applied_arr = text_array_append(applied_arr, histogram_bounds_name);
+			else
+				rejected_arr = text_array_append(rejected_arr, histogram_bounds_name);
+		}
+
+		if (!attargs[10].isnull)
+		{
+			if (correlation_applied)
+				applied_arr = text_array_append(applied_arr, correlation_name);
+			else
+				rejected_arr = text_array_append(rejected_arr, correlation_name);
+		}
+
+		if (!attargs[11].isnull)
+		{
+			if (mc_elems_applied)
+				applied_arr = text_array_append(applied_arr, mc_elems_name);
+			else
+				rejected_arr = text_array_append(rejected_arr, mc_elems_name);
+		}
+
+		if (!attargs[12].isnull)
+		{
+			if (mc_elem_freqs_applied)
+				applied_arr = text_array_append(applied_arr, mc_elem_freqs_name);
+			else
+				rejected_arr = text_array_append(rejected_arr, mc_elem_freqs_name);
+		}
+
+		if (!attargs[13].isnull)
+		{
+			if (elem_count_hist_applied)
+				applied_arr = text_array_append(applied_arr, elem_count_hist_name);
+			else
+				rejected_arr = text_array_append(rejected_arr, elem_count_hist_name);
+		}
+
+		if (!attargs[14].isnull)
+		{
+			if (range_length_hist_applied)
+				applied_arr = text_array_append(applied_arr, range_length_hist_name);
+			else
+				rejected_arr = text_array_append(rejected_arr, range_length_hist_name);
+		}
+
+		if (!attargs[15].isnull)
+		{
+			if (range_empty_frac_applied)
+				applied_arr = text_array_append(applied_arr, range_empty_frac_name);
+			else
+				rejected_arr = text_array_append(rejected_arr, range_empty_frac_name);
+		}
+
+		if (!attargs[16].isnull)
+		{
+			if (range_bounds_hist_applied)
+				applied_arr = text_array_append(applied_arr, range_bounds_hist_name);
+			else
+				rejected_arr = text_array_append(rejected_arr, range_bounds_hist_name);
+		}
+
+		retvalues[0] = true;
+		retvalues[1] = text_array_to_datum(applied_arr);
+		retnulls[1] = (applied_arr == NULL);
+		retvalues[2] = text_array_to_datum(rejected_arr);
+		retnulls[2] = (rejected_arr == NULL);
+		retvalues[3] = text_array_to_datum(params_rejected_arr);
+		retnulls[3] = (params_rejected_arr == NULL);
+	}
+
+	htup = heap_form_tuple(tupdesc, retvalues, retnulls);
+	PG_RETURN_DATUM(HeapTupleGetDatum(htup));
+}
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index 834950da72..7026aebb87 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -32,7 +32,7 @@ ERROR:  pg_class entry for relid 0 not found
 -- ERROR: relpages NULL
 SELECT
     pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.test'::regclass,
+        relation => 'stats_import.test'::regclass::oid,
         relpages => NULL::integer,
         reltuples => 400.0::real,
         relallvisible => 4::integer);
@@ -40,7 +40,7 @@ ERROR:  relpages cannot be NULL
 -- ERROR: reltuples NULL
 SELECT
     pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.test'::regclass,
+        relation => 'stats_import.test'::regclass::oid,
         relpages => 17::integer,
         reltuples => NULL::real,
         relallvisible => 4::integer);
@@ -48,7 +48,7 @@ ERROR:  reltuples cannot be NULL
 -- ERROR: relallvisible NULL
 SELECT
     pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.test'::regclass,
+        relation => 'stats_import.test'::regclass::oid,
         relpages => 17::integer,
         reltuples => 400.0::real,
         relallvisible => NULL::integer);
@@ -56,7 +56,7 @@ ERROR:  relallvisible cannot be NULL
 -- true: all named
 SELECT
     pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.test'::regclass,
+        relation => 'stats_import.test'::regclass::oid,
         relpages => 17::integer,
         reltuples => 400.0::real,
         relallvisible => 4::integer);
@@ -76,7 +76,7 @@ WHERE oid = 'stats_import.test'::regclass;
 -- true: all positional
 SELECT
     pg_catalog.pg_set_relation_stats(
-        'stats_import.test'::regclass,
+        'stats_import.test'::regclass::oid,
         18::integer,
         401.0::real,
         5::integer);
@@ -113,7 +113,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
 ERROR:  relation cannot be NULL
 -- error: attname null
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => NULL::name,
     inherited => false::boolean,
     null_frac => 0.1::real,
@@ -122,7 +122,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
 ERROR:  attname cannot be NULL
 -- error: inherited null
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => NULL::boolean,
     null_frac => 0.1::real,
@@ -131,7 +131,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
 ERROR:  inherited cannot be NULL
 -- error: null_frac null
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => false::boolean,
     null_frac => NULL::real,
@@ -140,16 +140,16 @@ SELECT pg_catalog.pg_set_attribute_stats(
 ERROR:  null_frac cannot be NULL
 -- error: avg_width null
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => false::boolean,
     null_frac => 0.1::real,
     avg_width => NULL::integer,
     n_distinct => 0.3::real);
 ERROR:  avg_width cannot be NULL
--- error: avg_width null
+-- error: n_distinct null
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => false::boolean,
     null_frac => 0.1::real,
@@ -158,7 +158,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
 ERROR:  n_distinct cannot be NULL
 -- ok: no stakinds
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => false::boolean,
     null_frac => 0.1::real,
@@ -179,7 +179,7 @@ WHERE starelid = 'stats_import.test'::regclass;
 
 -- warn: mcv / mcf null mismatch
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -190,7 +190,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
 ERROR:  most_common_vals cannot be NULL when most_common_freqs is NOT NULL
 -- warn: mcv / mcf null mismatch part 2
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -201,7 +201,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
 ERROR:  most_common_freqs cannot be NULL when most_common_vals is NOT NULL
 -- warn: mcv / mcf type mismatch
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -213,7 +213,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
 ERROR:  invalid input syntax for type integer: "2023-09-30"
 -- warning: mcv cast failure
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -225,7 +225,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
 ERROR:  invalid input syntax for type integer: "four"
 -- ok: mcv+mcf
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -253,7 +253,7 @@ AND attname = 'id';
 -- warn: histogram elements null value
 -- this generates no warnings, but perhaps it should
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -264,7 +264,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
 ERROR:  histogram_bounds array cannot contain NULL values
 -- ok: histogram_bounds
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -290,7 +290,7 @@ AND attname = 'id';
 
 -- ok: correlation
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -315,7 +315,7 @@ AND attname = 'id';
 
 -- warn: scalars can't have mcelem
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -327,7 +327,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
 ERROR:  Relation test attname id is a scalar type, cannot have stats of type most_common_elems, ignored
 -- warn: mcelem / mcelem mismatch
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'tags'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -338,7 +338,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
 ERROR:  most_common_elem_freqs cannot be NULL when most_common_elems is NOT NULL
 -- warn: mcelem / mcelem null mismatch part 2
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'tags'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -349,7 +349,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
 ERROR:  most_common_elems cannot be NULL when most_common_elem_freqs is NOT NULL
 -- ok: mcelem
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'tags'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -376,7 +376,7 @@ AND attname = 'tags';
 
 -- warn: scalars can't have elem_count_histogram
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -387,7 +387,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
 ERROR:  Relation test attname id is a scalar type, cannot have stats of type elem_count_histogram, ignored
 -- warn: elem_count_histogram null element
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'tags'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -395,14 +395,21 @@ SELECT pg_catalog.pg_set_attribute_stats(
     n_distinct => -0.1::real,
     elem_count_histogram => '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
     );
- pg_set_attribute_stats 
-------------------------
- 
+ERROR:  elem_count_histogram array cannot contain NULL values
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             | {one,three}       | {0.3,0.2,0.2,0.3,0}    |                      |                        |                  | 
 (1 row)
 
 -- ok: elem_count_histogram
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'tags'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -428,7 +435,7 @@ AND attname = 'tags';
 
 -- warn: scalars can't have range stats
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -440,7 +447,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
 ERROR:  Relation test attname id is not a range type, cannot have stats of type range_length_histogram
 -- warn: range_empty_frac range_length_hist null mismatch
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'arange'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -451,7 +458,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
 ERROR:  range_empty_frac cannot be NULL when range_length_histogram is NOT NULL
 -- warn: range_empty_frac range_length_hist null mismatch part 2
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'arange'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -462,7 +469,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
 ERROR:  range_length_histogram cannot be NULL when range_empty_frac is NOT NULL
 -- ok: range_empty_frac + range_length_hist
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'arange'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -489,7 +496,7 @@ AND attname = 'arange';
 
 -- warn: scalars can't have range stats
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -500,7 +507,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
 ERROR:  Relation test attname id is not a range type, cannot have stats of type range_bounds_histogram
 -- ok: range_bounds_histogram
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'arange'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -526,7 +533,7 @@ AND attname = 'arange';
 
 -- warn: exceed STATISTIC_NUM_SLOTS
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'arange'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -735,6 +742,861 @@ WHERE s.starelid = 'stats_import.is_odd'::regclass;
 ---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
 (0 rows)
 
+--
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+        1 |         4 |             0
+(1 row)
+
+-- reject: object doesn't exist
+SELECT *
+FROM pg_restore_relation_stats(
+        'relation', '0'::oid,
+        'version', 150000::integer,
+        'relpages', '17'::integer,
+        'reltuples', 400::real,
+        'relallvisible', 4::integer);
+WARNING:  pg_class entry for relid 0 not found
+ row_written | stats_applied | stats_rejected | params_rejected 
+-------------+---------------+----------------+-----------------
+ f           |               |                | 
+(1 row)
+
+-- reject: reltuples, relallvisible missing
+SELECT *
+FROM pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass::oid,
+        'version', 150000::integer,
+        'relpages', '17'::integer);
+WARNING:  parameter reltuples is required but not set
+ row_written | stats_applied | stats_rejected | params_rejected 
+-------------+---------------+----------------+-----------------
+ f           |               |                | 
+(1 row)
+
+-- reject: null value
+SELECT *
+FROM pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass::oid,
+        'version', 150000::integer,
+        'relpages', '17'::integer,
+        'reltuples', NULL::real,
+        'relallvisible', 4::integer);
+WARNING:  reltuples cannot be NULL
+ row_written | stats_applied | stats_rejected | params_rejected 
+-------------+---------------+----------------+-----------------
+ f           |               |                | 
+(1 row)
+
+-- reject: bad relpages type
+SELECT *
+FROM pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass::oid,
+        'version', 150000::integer,
+        'relpages', 'nope'::text,
+        'reltuples', 400.0::real,
+        'relallvisible', 4::integer);
+WARNING:  relpages must be of type oid 23 (integer) but is type oid 25
+WARNING:  parameter relpages is required but not set
+ row_written | stats_applied | stats_rejected | params_rejected 
+-------------+---------------+----------------+-----------------
+ f           |               |                | {relpages}
+(1 row)
+
+-- ok 
+SELECT *
+FROM pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass::oid,
+        'version', 150000::integer,
+        'relpages', 17::integer,
+        'reltuples', 400.0::real,
+        'relallvisible', 4::integer);
+ row_written |           stats_applied            | stats_rejected | params_rejected 
+-------------+------------------------------------+----------------+-----------------
+ t           | {relpages,reltuples,relallvisible} |                | 
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass::oid;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       17 |       400 |             4
+(1 row)
+
+-- reject: object does not exist
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', '0'::oid,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+WARNING:  Parameter relation OID 0 is invalid
+ row_written | stats_applied | stats_rejected | params_rejected 
+-------------+---------------+----------------+-----------------
+ f           |               |                | 
+(1 row)
+
+-- reject: relation null
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', NULL::oid,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+WARNING:  relation cannot be NULL
+ row_written | stats_applied | stats_rejected | params_rejected 
+-------------+---------------+----------------+-----------------
+ f           |               |                | 
+(1 row)
+
+-- reject: attname null
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', NULL::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+WARNING:  attname cannot be NULL
+ row_written | stats_applied | stats_rejected | params_rejected 
+-------------+---------------+----------------+-----------------
+ f           |               |                | 
+(1 row)
+
+-- false: inherited null
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'id'::name,
+    'inherited', NULL::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+WARNING:  inherited cannot be NULL
+ row_written | stats_applied | stats_rejected | params_rejected 
+-------------+---------------+----------------+-----------------
+ f           |               |                | 
+(1 row)
+
+-- false: null_frac null
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', NULL::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+WARNING:  null_frac cannot be NULL
+ row_written | stats_applied | stats_rejected | params_rejected 
+-------------+---------------+----------------+-----------------
+ f           |               |                | 
+(1 row)
+
+-- false: avg_width null
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', NULL::integer,
+    'n_distinct', 0.3::real);
+WARNING:  avg_width cannot be NULL
+ row_written | stats_applied | stats_rejected | params_rejected 
+-------------+---------------+----------------+-----------------
+ f           |               |                | 
+(1 row)
+
+-- false: avg_width null
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', NULL::real);
+WARNING:  n_distinct cannot be NULL
+ row_written | stats_applied | stats_rejected | params_rejected 
+-------------+---------------+----------------+-----------------
+ f           |               |                | 
+(1 row)
+
+-- ok: no stakinds
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ row_written |          stats_applied           | stats_rejected | params_rejected 
+-------------+----------------------------------+----------------+-----------------
+ t           | {null_frac,avg_width,n_distinct} |                | 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.1 |         2 |        0.3 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: mcv / mcf null mismatch part 1
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_freqs', '{0.1,0.2,0.3}'::real[]
+    );
+WARNING:  most_common_vals cannot be NULL when most_common_freqs is NOT NULL
+ row_written |          stats_applied           |   stats_rejected    | params_rejected 
+-------------+----------------------------------+---------------------+-----------------
+ t           | {null_frac,avg_width,n_distinct} | {most_common_freqs} | 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: mcv / mcf null mismatch part 2
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.2::real,
+    'most_common_vals', '{1,2,3}'::text
+    );
+WARNING:  most_common_freqs cannot be NULL when most_common_vals is NOT NULL
+ row_written |          stats_applied           |   stats_rejected   | params_rejected 
+-------------+----------------------------------+--------------------+-----------------
+ t           | {null_frac,avg_width,n_distinct} | {most_common_vals} | 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.5 |         2 |       -0.2 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: mcv / mcf type mismatch
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.3::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.2,0.1}'::double precision[]
+    );
+WARNING:  most_common_freqs must be of type oid 1021 (real[]) but is type oid 1022
+WARNING:  most_common_freqs cannot be NULL when most_common_vals is NOT NULL
+ row_written |          stats_applied           |   stats_rejected   |   params_rejected   
+-------------+----------------------------------+--------------------+---------------------
+ t           | {null_frac,avg_width,n_distinct} | {most_common_vals} | {most_common_freqs}
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.5 |         2 |       -0.3 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warning: mcv cast failure
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.4::real,
+    'most_common_vals', '{2,four,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+WARNING:  invalid input syntax for type integer: "four"
+ row_written |          stats_applied           |            stats_rejected            | params_rejected 
+-------------+----------------------------------+--------------------------------------+-----------------
+ t           | {null_frac,avg_width,n_distinct} | {most_common_vals,most_common_freqs} | 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.5 |         2 |       -0.4 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- ok: mcv+mcf
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+ row_written |                            stats_applied                            | stats_rejected | params_rejected 
+-------------+---------------------------------------------------------------------+----------------+-----------------
+ t           | {null_frac,avg_width,n_distinct,most_common_vals,most_common_freqs} |                | 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: NULL in histogram array
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,NULL,3,4}'::text
+    );
+WARNING:  histogram_bounds array cannot contain NULL values
+ row_written |          stats_applied           |   stats_rejected   | params_rejected 
+-------------+----------------------------------+--------------------+-----------------
+ t           | {null_frac,avg_width,n_distinct} | {histogram_bounds} | 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- ok: histogram_bounds
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,2,3,4}'::text );
+ row_written |                   stats_applied                   | stats_rejected | params_rejected 
+-------------+---------------------------------------------------+----------------+-----------------
+ t           | {null_frac,avg_width,n_distinct,histogram_bounds} |                | 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: elem_count_histogram null element
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'tags'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+WARNING:  elem_count_histogram array cannot contain NULL values
+ row_written |          stats_applied           |     stats_rejected     | params_rejected 
+-------------+----------------------------------+------------------------+-----------------
+ t           | {null_frac,avg_width,n_distinct} | {elem_count_histogram} | 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- ok: elem_count_histogram
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'tags'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ row_written |                     stats_applied                     | stats_rejected | params_rejected 
+-------------+-------------------------------------------------------+----------------+-----------------
+ t           | {null_frac,avg_width,n_distinct,elem_count_histogram} |                | 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
+ stats_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+(1 row)
+
+-- range stats on a scalar type
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.15::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+WARNING:  Relation test attname id is not a range type, cannot have stats of type range_length_histogram
+ row_written |          stats_applied           |              stats_rejected               | params_rejected 
+-------------+----------------------------------+-------------------------------------------+-----------------
+ t           | {null_frac,avg_width,n_distinct} | {range_length_histogram,range_empty_frac} | 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.5 |         2 |      -0.15 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'arange'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+WARNING:  range_empty_frac cannot be NULL when range_length_histogram is NOT NULL
+ row_written |          stats_applied           |      stats_rejected      | params_rejected 
+-------------+----------------------------------+--------------------------+-----------------
+ t           | {null_frac,avg_width,n_distinct} | {range_length_histogram} | 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'arange'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real
+    );
+WARNING:  range_length_histogram cannot be NULL when range_empty_frac is NOT NULL
+ row_written |          stats_applied           |   stats_rejected   | params_rejected 
+-------------+----------------------------------+--------------------+-----------------
+ t           | {null_frac,avg_width,n_distinct} | {range_empty_frac} | 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- ok: range_empty_frac + range_length_hist
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'arange'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+ row_written |                              stats_applied                               | stats_rejected | params_rejected 
+-------------+--------------------------------------------------------------------------+----------------+-----------------
+ t           | {null_frac,avg_width,n_distinct,range_length_histogram,range_empty_frac} |                | 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
+(1 row)
+
+-- warn: range bounds histogram on scalar
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+WARNING:  Relation test attname id is not a range type, cannot have stats of type range_bounds_histogram
+ row_written |          stats_applied           |      stats_rejected      | params_rejected 
+-------------+----------------------------------+--------------------------+-----------------
+ t           | {null_frac,avg_width,n_distinct} | {range_bounds_histogram} | 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- ok: range_bounds_histogram
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'arange'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ row_written |                      stats_applied                      | stats_rejected | params_rejected 
+-------------+---------------------------------------------------------+----------------+-----------------
+ t           | {null_frac,avg_width,n_distinct,range_bounds_histogram} |                | 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
+ stats_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      |                        |                  | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+(1 row)
+
+-- warn: too many stat kinds
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'arange'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
+    'histogram_bounds', '{1,2,3,4}'::text,
+    'correlation', 1.1::real,
+    'most_common_elems', '{3,1}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    'range_empty_frac', -0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text);
+WARNING:  imported statistics must have a maximum of 5 slots but 6 given
+ row_written | stats_applied | stats_rejected | params_rejected 
+-------------+---------------+----------------+-----------------
+ f           |               |                | 
+(1 row)
+
+--
+-- Copy stats from test to test_clone, and is_odd to is_odd_clone
+--
+SELECT s.schemaname, s.tablename, s.attname, s.inherited, r.*
+FROM pg_catalog.pg_stats AS s
+CROSS JOIN LATERAL
+    pg_catalog.pg_restore_attribute_stats(
+        'relation', ('stats_import.' || s.tablename || '_clone')::regclass::oid,
+        'attname', s.attname,
+        'inherited', s.inherited,
+        'version', 150000,
+        'null_frac', s.null_frac,
+        'avg_width', s.avg_width,
+        'n_distinct', s.n_distinct,
+        'most_common_vals', s.most_common_vals::text,
+        'most_common_freqs', s.most_common_freqs,
+        'histogram_bounds', s.histogram_bounds::text,
+        'correlation', s.correlation,
+        'most_common_elems', s.most_common_elems::text,
+        'most_common_elem_freqs', s.most_common_elem_freqs,
+        'elem_count_histogram', s.elem_count_histogram,
+        'range_bounds_histogram', s.range_bounds_histogram::text,
+        'range_empty_frac', s.range_empty_frac,
+        'range_length_histogram', s.range_length_histogram::text) AS r
+WHERE s.schemaname = 'stats_import'
+AND s.tablename IN ('test', 'is_odd')
+ORDER BY s.tablename, s.attname, s.inherited;
+  schemaname  | tablename | attname | inherited | row_written |                                  stats_applied                                  | stats_rejected | params_rejected 
+--------------+-----------+---------+-----------+-------------+---------------------------------------------------------------------------------+----------------+-----------------
+ stats_import | is_odd    | expr    | f         | t           | {null_frac,avg_width,n_distinct,most_common_vals,most_common_freqs,correlation} |                | 
+ stats_import | test      | arange  | f         | t           | {null_frac,avg_width,n_distinct,range_bounds_histogram}                         |                | 
+ stats_import | test      | comp    | f         | t           | {null_frac,avg_width,n_distinct,histogram_bounds,correlation}                   |                | 
+ stats_import | test      | id      | f         | t           | {null_frac,avg_width,n_distinct}                                                |                | 
+ stats_import | test      | name    | f         | t           | {null_frac,avg_width,n_distinct,histogram_bounds,correlation}                   |                | 
+ stats_import | test      | tags    | f         | t           | {null_frac,avg_width,n_distinct,elem_count_histogram}                           |                | 
+(6 rows)
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+   relname    | num_stats 
+--------------+-----------
+ is_odd       |         1
+ is_odd_clone |         1
+ test         |         5
+ test_clone   |         5
+(4 rows)
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
 DROP SCHEMA stats_import CASCADE;
 NOTICE:  drop cascades to 3 other objects
 DETAIL:  drop cascades to type stats_import.complex_type
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index 96795edf42..79a5a4704a 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -31,7 +31,7 @@ SELECT
 -- ERROR: relpages NULL
 SELECT
     pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.test'::regclass,
+        relation => 'stats_import.test'::regclass::oid,
         relpages => NULL::integer,
         reltuples => 400.0::real,
         relallvisible => 4::integer);
@@ -39,7 +39,7 @@ SELECT
 -- ERROR: reltuples NULL
 SELECT
     pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.test'::regclass,
+        relation => 'stats_import.test'::regclass::oid,
         relpages => 17::integer,
         reltuples => NULL::real,
         relallvisible => 4::integer);
@@ -47,7 +47,7 @@ SELECT
 -- ERROR: relallvisible NULL
 SELECT
     pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.test'::regclass,
+        relation => 'stats_import.test'::regclass::oid,
         relpages => 17::integer,
         reltuples => 400.0::real,
         relallvisible => NULL::integer);
@@ -55,7 +55,7 @@ SELECT
 -- true: all named
 SELECT
     pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.test'::regclass,
+        relation => 'stats_import.test'::regclass::oid,
         relpages => 17::integer,
         reltuples => 400.0::real,
         relallvisible => 4::integer);
@@ -67,7 +67,7 @@ WHERE oid = 'stats_import.test'::regclass;
 -- true: all positional
 SELECT
     pg_catalog.pg_set_relation_stats(
-        'stats_import.test'::regclass,
+        'stats_import.test'::regclass::oid,
         18::integer,
         401.0::real,
         5::integer);
@@ -96,7 +96,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
 
 -- error: attname null
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => NULL::name,
     inherited => false::boolean,
     null_frac => 0.1::real,
@@ -105,7 +105,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
 
 -- error: inherited null
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => NULL::boolean,
     null_frac => 0.1::real,
@@ -114,7 +114,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
 
 -- error: null_frac null
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => false::boolean,
     null_frac => NULL::real,
@@ -123,16 +123,16 @@ SELECT pg_catalog.pg_set_attribute_stats(
 
 -- error: avg_width null
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => false::boolean,
     null_frac => 0.1::real,
     avg_width => NULL::integer,
     n_distinct => 0.3::real);
 
--- error: avg_width null
+-- error: n_distinct null
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => false::boolean,
     null_frac => 0.1::real,
@@ -141,7 +141,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
 
 -- ok: no stakinds
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => false::boolean,
     null_frac => 0.1::real,
@@ -154,7 +154,7 @@ WHERE starelid = 'stats_import.test'::regclass;
 
 -- warn: mcv / mcf null mismatch
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -165,7 +165,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
 
 -- warn: mcv / mcf null mismatch part 2
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -176,7 +176,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
 
 -- warn: mcv / mcf type mismatch
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -188,7 +188,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
 
 -- warning: mcv cast failure
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -200,7 +200,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
 
 -- ok: mcv+mcf
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -220,7 +220,7 @@ AND attname = 'id';
 -- warn: histogram elements null value
 -- this generates no warnings, but perhaps it should
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -231,7 +231,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
 
 -- ok: histogram_bounds
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -249,7 +249,7 @@ AND attname = 'id';
 
 -- ok: correlation
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -266,7 +266,7 @@ AND attname = 'id';
 
 -- warn: scalars can't have mcelem
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -278,7 +278,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
 
 -- warn: mcelem / mcelem mismatch
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'tags'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -289,7 +289,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
 
 -- warn: mcelem / mcelem null mismatch part 2
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'tags'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -300,7 +300,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
 
 -- ok: mcelem
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'tags'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -319,7 +319,7 @@ AND attname = 'tags';
 
 -- warn: scalars can't have elem_count_histogram
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -329,7 +329,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
     );
 -- warn: elem_count_histogram null element
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'tags'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -337,9 +337,16 @@ SELECT pg_catalog.pg_set_attribute_stats(
     n_distinct => -0.1::real,
     elem_count_histogram => '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
     );
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
 -- ok: elem_count_histogram
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'tags'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -357,7 +364,7 @@ AND attname = 'tags';
 
 -- warn: scalars can't have range stats
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -366,9 +373,10 @@ SELECT pg_catalog.pg_set_attribute_stats(
     range_empty_frac => 0.5::real,
     range_length_histogram => '{399,499,Infinity}'::text
     );
+
 -- warn: range_empty_frac range_length_hist null mismatch
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'arange'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -376,9 +384,10 @@ SELECT pg_catalog.pg_set_attribute_stats(
     n_distinct => -0.1::real,
     range_length_histogram => '{399,499,Infinity}'::text
     );
+
 -- warn: range_empty_frac range_length_hist null mismatch part 2
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'arange'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -386,9 +395,10 @@ SELECT pg_catalog.pg_set_attribute_stats(
     n_distinct => -0.1::real,
     range_empty_frac => 0.5::real
     );
+
 -- ok: range_empty_frac + range_length_hist
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'arange'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -407,7 +417,7 @@ AND attname = 'arange';
 
 -- warn: scalars can't have range stats
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'id'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -415,9 +425,10 @@ SELECT pg_catalog.pg_set_attribute_stats(
     n_distinct => -0.1::real,
     range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
     );
+
 -- ok: range_bounds_histogram
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'arange'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -435,7 +446,7 @@ AND attname = 'arange';
 
 -- warn: exceed STATISTIC_NUM_SLOTS
 SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
+    relation => 'stats_import.test'::regclass::oid,
     attname => 'arange'::name,
     inherited => false::boolean,
     null_frac => 0.5::real,
@@ -620,4 +631,620 @@ FROM pg_statistic s
 JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
 WHERE s.starelid = 'stats_import.is_odd'::regclass;
 
+--
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+-- reject: object doesn't exist
+SELECT *
+FROM pg_restore_relation_stats(
+        'relation', '0'::oid,
+        'version', 150000::integer,
+        'relpages', '17'::integer,
+        'reltuples', 400::real,
+        'relallvisible', 4::integer);
+
+-- reject: reltuples, relallvisible missing
+SELECT *
+FROM pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass::oid,
+        'version', 150000::integer,
+        'relpages', '17'::integer);
+
+-- reject: null value
+SELECT *
+FROM pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass::oid,
+        'version', 150000::integer,
+        'relpages', '17'::integer,
+        'reltuples', NULL::real,
+        'relallvisible', 4::integer);
+
+-- reject: bad relpages type
+SELECT *
+FROM pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass::oid,
+        'version', 150000::integer,
+        'relpages', 'nope'::text,
+        'reltuples', 400.0::real,
+        'relallvisible', 4::integer);
+
+-- ok 
+SELECT *
+FROM pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass::oid,
+        'version', 150000::integer,
+        'relpages', 17::integer,
+        'reltuples', 400.0::real,
+        'relallvisible', 4::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass::oid;
+
+-- reject: object does not exist
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', '0'::oid,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- reject: relation null
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', NULL::oid,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- reject: attname null
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', NULL::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- false: inherited null
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'id'::name,
+    'inherited', NULL::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- false: null_frac null
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', NULL::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- false: avg_width null
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', NULL::integer,
+    'n_distinct', 0.3::real);
+
+-- false: avg_width null
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', NULL::real);
+
+-- ok: no stakinds
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: mcv / mcf null mismatch part 1
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_freqs', '{0.1,0.2,0.3}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: mcv / mcf null mismatch part 2
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.2::real,
+    'most_common_vals', '{1,2,3}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: mcv / mcf type mismatch
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.3::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.2,0.1}'::double precision[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warning: mcv cast failure
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.4::real,
+    'most_common_vals', '{2,four,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- ok: mcv+mcf
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: NULL in histogram array
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,NULL,3,4}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- ok: histogram_bounds
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,2,3,4}'::text );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: elem_count_histogram null element
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'tags'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- ok: elem_count_histogram
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'tags'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- range stats on a scalar type
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.15::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'arange'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'arange'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- ok: range_empty_frac + range_length_hist
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'arange'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: range bounds histogram on scalar
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- ok: range_bounds_histogram
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'arange'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: too many stat kinds
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass::oid,
+    'attname', 'arange'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
+    'histogram_bounds', '{1,2,3,4}'::text,
+    'correlation', 1.1::real,
+    'most_common_elems', '{3,1}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    'range_empty_frac', -0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text);
+
+--
+-- Copy stats from test to test_clone, and is_odd to is_odd_clone
+--
+SELECT s.schemaname, s.tablename, s.attname, s.inherited, r.*
+FROM pg_catalog.pg_stats AS s
+CROSS JOIN LATERAL
+    pg_catalog.pg_restore_attribute_stats(
+        'relation', ('stats_import.' || s.tablename || '_clone')::regclass::oid,
+        'attname', s.attname,
+        'inherited', s.inherited,
+        'version', 150000,
+        'null_frac', s.null_frac,
+        'avg_width', s.avg_width,
+        'n_distinct', s.n_distinct,
+        'most_common_vals', s.most_common_vals::text,
+        'most_common_freqs', s.most_common_freqs,
+        'histogram_bounds', s.histogram_bounds::text,
+        'correlation', s.correlation,
+        'most_common_elems', s.most_common_elems::text,
+        'most_common_elem_freqs', s.most_common_elem_freqs,
+        'elem_count_histogram', s.elem_count_histogram,
+        'range_bounds_histogram', s.range_bounds_histogram::text,
+        'range_empty_frac', s.range_empty_frac,
+        'range_length_histogram', s.range_length_histogram::text) AS r
+WHERE s.schemaname = 'stats_import'
+AND s.tablename IN ('test', 'is_odd')
+ORDER BY s.tablename, s.attname, s.inherited;
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test_clone'::regclass;
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test'::regclass;
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd_clone'::regclass;
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd'::regclass;
+
+
 DROP SCHEMA stats_import CASCADE;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index e05073167b..33cf5a3e12 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29835,6 +29835,14 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
           The value of <structfield>relallvisible</structfield> must not be less
           than 0.
          </para>
+         <para>
+          This function is very close in purpose to
+          <function>pg_restore_relation_stats</function>, but is intended for 
+          interactive use and for that reason has important differences. First,
+          any error in the data provided causes the function to fail without
+          modifying the <structname>pg_class</structname> row. Second, the
+          function behaves transactionally, like any normal data update.
+         </para>
        </entry>
       </row>
       <row>
@@ -29900,6 +29908,475 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          </para>
        </entry>
       </row>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_restore_relation_stats</primary>
+        </indexterm>
+        <function>pg_restore_relation_stats</function> (
+        <literal>VARIADIC</literal> <parameter>kwargs</parameter> <type>"any"</type> )
+        <returnvalue>setof record</returnvalue>
+          ( <parameter>row_written</parameter> <type>boolean</type>,
+          <parameter>stats_applied</parameter> <type>text[]</type>,
+          <parameter>stats_rejected</parameter> <type>text[]</type>,
+          <parameter>params_reject</parameter> <type>text[]</type> )
+        </para>
+        <para>
+         Updates the <structname>pg_class</structname> row for the specified
+         <parameter>relation</parameter>, setting the values for the columns
+         <structfield>reltuples</structfield>,
+         <structfield>relpages</structfield>, and
+         <structfield>relallvisible</structfield>. It is highly similar to
+         <function>pg_set_relation_stats</function>, which also mimics the
+         behavior of <command>ANALYZE</command> in its effect on the values
+         in <structname>pg_class</structname>.
+        </para>
+        <para>
+         This function is very close in purpose to
+         <function>pg_set_relation_stats</function>, but is intended for 
+         use in <command>pg_upgrade</command> and <command>pg_restore</command>
+         and for that reason has important differences.
+        </para>
+        <para>First, any error in the data provided is re-cast as a
+         <literal>WARNING</literal> to allow the function to reach completion,
+         even if it cannot update any statistics.
+        </para>
+        <para>
+         Second, the function updates <structname>pg_class</structname>
+         in-place, which is to say non-transactionally. This is done to avoid
+         the table bloat in <structname>pg_class</structname> that would inevitably
+         result from modifying nearly every row in quick succession.
+        </para>
+        <para>
+         Lastly, and most noticiably, in order to facilitate compatibility with
+         future statistical structures, instead of a fixed argument list, it
+         takes a series of parameters organized in key-value pairs. Each keyword
+         parameter must be of type <type>text</type>, and must be followed by
+         another parameter whose type is determined by the value of the keyword
+         parameter. The list of accepted keywords and their corresponding parameter
+         types are as follows:
+        </para>
+        <variablelist>
+         <varlistentry>
+          <term><parameter>relation</parameter></term>
+          <listitem>
+           <para>
+            This identifies the value of <structfield>oid</structfield> of the 
+            <structname>pg_class</structname> to be modified. It must be followed
+            by a parameter of type <type>oid</type>, and the value must reference
+            an existing relation. This parameter is required.
+           </para>
+          </listitem>
+         </varlistentry>
+         <varlistentry>
+          <term><parameter>version</parameter></term>
+          <listitem>
+           <para>
+            This corresponds to the <varname>SERVER_VERSION_NUM</varname>
+            of the database that was the origin of these statistics. It must
+            be followed by a parameter of type <type>integer</type>. The function
+            will not process statistics that claim to be generated from a server
+            prior to version <literal>90200</literal>, which means 9.2.0. This
+            serves to inform the function as to how to adapt the statistics given
+            to the current database version. This parameter is not required. If
+            omitted, it will default to the current server version.
+           </para>
+          </listitem>
+         </varlistentry>
+         <varlistentry>
+          <term><parameter>relpages</parameter></term>
+          <listitem>
+           <para>
+            This sets the value of <structfield>relpages</structfield> in
+            <structname>pg_class</structname>. It must be followed by a
+            parameter of type <type>integer</type>, and the value must not
+            be less than <literal>0</literal>. This parameter is required.
+           </para>
+          </listitem>
+         </varlistentry>
+         <varlistentry>
+          <term><parameter>reltuples</parameter></term>
+          <listitem>
+           <para>
+            This sets the value of <structfield>reltuples</structfield> in
+            <structname>pg_class</structname>. It must be followed by a
+            parameter of type <type>real</type>, and the value must not be less
+            than <literal>-1.0</literal>. This parameter is required.
+           </para>
+          </listitem>
+         </varlistentry>
+         <varlistentry>
+          <term><parameter>relallvisible</parameter></term>
+          <listitem>
+           <para>
+            This sets the value of <structfield>relallvisible</structfield> in
+            <structname>pg_class</structname>. It must be followed by a
+            parameter of type <type>integer</type>, and the value must not be less
+            than <literal>0</literal>. This parameter is required.
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+        <para>
+         Any parameter keywords given that are not in this list will generate a
+         warning but will otherwise be ignored along with the paired value.
+        </para>
+        <para>
+         The function returns one with the following parameters:
+        </para>
+        <variablelist>
+         <varlistentry>
+          <term><parameter>row_written</parameter></term>
+          <listitem>
+           <para>
+            a <type>boolean</type> with the value <literal>true</literal> if the
+            statistics were written to the database and <literal>false</literal>
+            otherwise.
+           </para>
+          </listitem>
+         </varlistentry>
+         <varlistentry>
+          <term><parameter>stats_applied</parameter></term>
+          <listitem>
+           <para>
+            A <type>text[]</type> where each element is the keyword name of a
+            statistic that was successfully written. It will be <literal>NULL</literal>
+            if <parameter>row_written</parameter> is <literal>false</literal>.
+           </para>
+          </listitem>
+         </varlistentry>
+         <varlistentry>
+          <term><parameter>stats_rejected</parameter></term>
+          <listitem>
+           <para>
+            A <type>text[]</type> where each element is the keyword name of a
+            statistic that was given to the function but could not be written,
+            but the row was nevertheless successfully written. It will always
+            be <literal>NULL</literal> if <parameter>row_written</parameter>
+            is <literal>false</literal>.
+           </para>
+          </listitem>
+         </varlistentry>
+         <varlistentry>
+          <term><parameter>params_rejected</parameter></term>
+          <listitem>
+           <para>
+            A <type>text[]</type> where each element is the keyword name of a
+            keyword that was given to the function but could not be processed
+            for a variety of reasons (unknown keyword, duplicate of a keyword
+            already given, etc).
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+        <para>
+         The caller must either be the owner of the relation, or have superuser
+         privileges.
+        </para>
+        <para>
+         This function is very close in purpose to
+         <function>pg_set_relation_stats</function>, but is intended for 
+         use in <command>pg_upgrade</command> and other situations where the
+         invocation can be machine generated, and priority is given to forward
+         compatibility over readability.
+        </para>
+       </entry>
+      </row>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_restore_attribute_stats</primary>
+        </indexterm>
+        <function>pg_restore_attribute_stats</function> (
+         <literal>VARIADIC</literal> <parameter>kwargs</parameter> <type>"any"</type> )
+         <returnvalue>setof record</returnvalue>
+           ( <parameter>row_written</parameter> <type>boolean</type>,
+           <parameter>stats_applied</parameter> <type>text[]</type>,
+           <parameter>stats_rejected</parameter> <type>text[]</type>,
+           <parameter>params_reject</parameter> <type>text[]</type> )
+         </para>
+         <para>
+          Replaces the <structname>pg_statistic</structname> row for the
+          <structname>pg_attribute</structname> row specified by
+          <parameter>relation</parameter>, <parameter>attname</parameter>
+          and <parameter>inherited</parameter> values given in the
+          variant argument list <parameter>kwargs</parameter>. It is highly
+          similar to <function>pg_set_attribute_stats</function>, which also
+          mimics the behavior of <command>ANALYZE</command> in its effect on
+          the values in <structname>pg_statistic</structname>.
+         </para>
+         <para>
+          The purpose of this function is to apply statistics values in an
+          upgrade situation that are "good enough" for system operation until
+          they are replaced by the next <command>ANALYZE</command>, usually via
+          <command>autovacuum</command>. This function is equivalent to
+          <function>pg_set_attribute_stats</function> with a few important
+          differences:
+         </para>
+         <para>First, any error in the data provided is re-cast as a
+          <literal>WARNING</literal> to allow the function to reach completion,
+          even if it cannot update any statistics.
+         </para>
+         <para>
+          Second, in order to facilitate compatibility with future statistical
+          structures, instead of a fixed argument list, it takes a series of
+          parameters organized in key-value pairs. Each keyword parameter must
+          be of type <type>text</type>, and must be followed by another
+          parameter whose type is determined by the value of the keyword
+          parameter. The list of accepted keywords and their corresponding parameter
+          types are as follows:
+         </para>
+         <variablelist>
+          <varlistentry>
+           <term><parameter>relation</parameter></term>
+           <listitem>
+            <para>
+             This identifies the value of <structfield>oid</structfield> of the 
+             <structname>pg_statistic</structname> to be modified. It must be followed
+             by a parameter of type <type>oid</type>, and the value must reference an
+             existing relation. This parameter is required.
+            </para>
+           </listitem>
+          </varlistentry>
+          <varlistentry>
+           <term><parameter>attname</parameter></term>
+           <listitem>
+            <para>
+             This identifies the value of <structfield>attname</structfield> of the 
+             <structname>pg_statistic</structname> with the <structname>attnum</structname>
+             of the <structname>pg_statistic</structname> to be modified. It must
+             be followed by a parameter of type <type>name</type>, and the value must not
+             reference and existing relation. This parameter is required.
+            </para>
+           </listitem>
+          </varlistentry>
+          <varlistentry>
+           <term><parameter>inherited</parameter></term>
+           <listitem>
+            <para>
+             This identifies the value of <structfield>inherited</structfield> of the 
+             <structname>pg_statistic</structname> to be modified. It must be followed
+             by a parameter of type <type>boolean</type>. This parameter is required.
+            </para>
+           </listitem>
+          </varlistentry>
+          <varlistentry>
+           <term><parameter>version</parameter></term>
+           <listitem>
+            <para>
+             This corresponds to the<varname>SERVER_VERSION_NUM</varname>
+             of the database that was the origin of these statistics. It must
+             be followed by a parameter of type <type>integer</type>. The function
+             will not process statistics that claim to be generated from a server
+             prior to version <literal>90200</literal>, which means 9.2.0. This
+             serves to inform the function as to how to adapt the statistics given
+             to the current database version. This parameter is not required. If
+             omitted, it will default to the current server version.
+            </para>
+           </listitem>
+          </varlistentry>
+          <varlistentry>
+           <term><parameter>null_frac</parameter></term>
+           <listitem>
+            <para>
+             This sets the value of <structfield>null_frac</structfield> in
+             <structname>pg_statistic</structname>. It must be followed by a
+             parameter of type <type>real</type>. This parameter is required.
+            </para>
+           </listitem>
+          </varlistentry>
+          <varlistentry>
+           <term><parameter>avg_width</parameter></term>
+           <listitem>
+            <para>
+             This sets the value of <structfield>avg_width</structfield> in
+             <structname>pg_statistic</structname>. It must be followed by a
+             parameter of type <type>integer</type>. This parameter is required.
+            </para>
+           </listitem>
+          </varlistentry>
+          <varlistentry>
+           <term><parameter>n_distinct</parameter></term>
+           <listitem>
+            <para>
+             This sets the value of <structfield>n_distinct</structfield> in
+             <structname>pg_statistic</structname>. It must be followed by a
+             parameter of type <type>real</type>. This parameter is required.
+            </para>
+           </listitem>
+          </varlistentry>
+          <varlistentry>
+           <term><parameter>most_common_vals</parameter></term>
+           <listitem>
+            <para>
+             This sets the value of <structfield>most_common_vals</structfield> in
+             <structname>pg_statistic</structname>. It must be followed by a
+             parameter of type <type>text</type>, which represents the text I/O
+             representation of the array of most common values for this this column's
+             data. This parameter is optional, but if provided then
+             <parameter>most_common_freqs</parameter> must also be provided.
+            </para>
+           </listitem>
+          </varlistentry>
+          <varlistentry>
+           <term><parameter>most_common_freqs</parameter></term>
+           <listitem>
+            <para>
+             This sets the value of <structfield>most_common_freqs</structfield> in
+             <structname>pg_statistic</structname>. It must be followed by a
+             parameter of type <type>real[]</type>. The parameter is optional, but if
+             provided then <parameter>most_common_elems</parameter> must
+             also be provided.
+            </para>
+           </listitem>
+          </varlistentry>
+          <varlistentry>
+           <term><parameter>histogram_bounds</parameter></term>
+           <listitem>
+            <para>
+             This sets the value of <structfield>most_common_vals</structfield> in
+             <structname>pg_statistic</structname>. It must be followed by a
+             parameter of type <type>text</type>, which represents the text I/O
+             representation of the histogram bounds values for this this column's
+             data. This parameter is optional.
+            </para>
+           </listitem>
+          </varlistentry>
+          <varlistentry>
+           <term><parameter>correlation</parameter></term>
+           <listitem>
+            <para>
+             This sets the value of <structfield>correlation</structfield> in
+             <structname>pg_statistic</structname>. It must be followed by a
+             parameter of type <type>real</type>. The parameter is optional.
+            </para>
+           </listitem>
+          </varlistentry>
+          <varlistentry>
+           <term><parameter>most_common_elems</parameter></term>
+           <listitem>
+            <para>
+             This sets the value of <structfield>most_common_elemss</structfield> in
+             <structname>pg_statistic</structname>. It must be followed by a
+             parameter of type <type>text</type>, which represents the text I/O
+             representation of the array of most common elemens for this this column's
+             data. This parameter is optional, but if provided then the parameter
+             <parameter>most_common_elem_freqs</parameter> must also be provided.
+            </para>
+           </listitem>
+          </varlistentry>
+          <varlistentry>
+           <term><parameter>most_common_elem_freqs</parameter></term>
+           <listitem>
+            <para>
+             This sets the value of <structfield>most_common_elem_freqs</structfield> in
+             <structname>pg_statistic</structname>. It must be followed by a
+             parameter of type <type>real[]</type>. The parameter is optional, but if provided
+             then <parameter>most_common_elems</parameter> must also be provided.
+            </para>
+           </listitem>
+          </varlistentry>
+          <varlistentry>
+           <term><parameter>elem_count_histogram</parameter></term>
+           <listitem>
+            <para>
+             This sets the value of <structfield>elem_count_histogram</structfield> in
+             <structname>pg_statistic</structname>. It must be followed by a
+             parameter of type <type>real[]</type>. The parameter is optional.
+            </para>
+           </listitem>
+          </varlistentry>
+          <varlistentry>
+           <term><parameter>range_length_histogram</parameter></term>
+           <listitem>
+            <para>
+             This sets the value of <structfield>range_length_histogram</structfield> in
+             <structname>pg_statistic</structname>. It must be followed by a
+             parameter of type <type>text</type>, which represents the text I/O
+             representation of the array of range lengh histogram for this this column's
+             data. This parameter is optional, but if provided then the parameter
+             <parameter>range_empty_frac</parameter> must also be provided.
+            </para>
+           </listitem>
+          </varlistentry>
+          <varlistentry>
+           <term><parameter>range_empty_frac</parameter></term>
+           <listitem>
+            <para>
+             This sets the value of <structfield>range_empty_frac</structfield> in
+             <structname>pg_statistic</structname>. It must be followed by a
+             parameter of type <type>real</type>. The parameter is optional, but if provided
+             then <parameter>range_length_histogram</parameter> must also be provided.
+            </para>
+           </listitem>
+          </varlistentry>
+          <varlistentry>
+           <term><parameter>range_bounds_histogram</parameter></term>
+           <listitem>
+            <para>
+             This sets the value of <structfield>range_bounds_histogram</structfield> in
+             <structname>pg_statistic</structname>. It must be followed by a
+             parameter of type <type>text</type>, which represents the text I/O
+             representation of the array of range bounds histogram for this this column's
+             data. This parameter is optional.
+            </para>
+           </listitem>
+          </varlistentry>
+         </variablelist>
+         <para>
+          Any other parameter names given must also have a value pair, but will emit
+          a warning and thereafter be ignored.
+         </para>
+         <para>
+          The function returns one with the following parameters:
+         </para>
+         <variablelist>
+          <varlistentry>
+           <term><parameter>row_written</parameter></term>
+           <listitem>
+            <para>
+             a <type>boolean</type> with the value <literal>true</literal> if the
+             statistics were written to the database and <literal>false</literal>
+             otherwise.
+            </para>
+           </listitem>
+          </varlistentry>
+          <varlistentry>
+           <term><parameter>stats_applied</parameter></term>
+           <listitem>
+            <para>
+             A <type>text[]</type> where each element is the keyword name of a
+             statistic that was successfully written. It will be <literal>NULL</literal>
+             if <parameter>row_written</parameter> is <literal>false</literal>.
+            </para>
+           </listitem>
+          </varlistentry>
+          <varlistentry>
+           <term><parameter>stats_rejected</parameter></term>
+           <listitem>
+            <para>
+             A <type>text[]</type> where each element is the keyword name of a
+             statistic that was given to the function but could not be written,
+             but the row was nevertheless successfully written. It will always
+             be <literal>NULL</literal> if <parameter>row_written</parameter>
+             is <literal>false</literal>.
+            </para>
+           </listitem>
+          </varlistentry>
+          <varlistentry>
+           <term><parameter>params_rejected</parameter></term>
+           <listitem>
+            <para>
+             A <type>text[]</type> where each element is the keyword name of a
+             keyword that was given to the function but could not be processed
+             for a variety of reasons (unknown keyword, duplicate of a keyword
+             already given, etc).
+            </para>
+           </listitem>
+          </varlistentry>
+         </variablelist>
+       </entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
-- 
2.45.2

v25-0005-Enable-dumping-of-table-index-stats-in-pg_dump.patchtext/x-patch; charset=US-ASCII; name=v25-0005-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From 35ccf9ba8e15bb0ead8fdc352026d614ba0b6695 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH v25 5/5] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will generate a statement that
calls pg_set_relation_stats(), and it will generate a series of
statements that call pg_set_attribute_stats(), one per attribute. These
statements will restore the statistics of the current system onto the
destination system.

Adds the command-line options -X / --statistics-only, which are mutually
exclusive to --schema-only and --data-only.

Statistics are not dumped when --schema-only is specified, except during
a binary upgrade.

As is the pattern with pg_dump options, staistics can be disabled using
--no-statistics.

Table statistics are dumped in the data section. This is true even if
dumping stats in a binary upgrade. Index and Materialized View
statistics are dumped in the post-data section.
---
 src/bin/pg_dump/pg_backup.h          |   6 +
 src/bin/pg_dump/pg_backup_archiver.c |   5 +
 src/bin/pg_dump/pg_dump.c            | 397 ++++++++++++++++++++++++++-
 src/bin/pg_dump/pg_dump.h            |   8 +
 src/bin/pg_dump/pg_dump_sort.c       |   5 +
 src/bin/pg_dump/pg_dumpall.c         |   5 +
 src/bin/pg_dump/pg_restore.c         |   8 +
 src/bin/pg_dump/t/001_basic.pl       |  10 +-
 doc/src/sgml/ref/pg_dump.sgml        |  36 ++-
 doc/src/sgml/ref/pg_restore.sgml     |  31 ++-
 10 files changed, 493 insertions(+), 18 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index d942a6a256..25d386f5bb 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -112,11 +112,13 @@ typedef struct _restoreOptions
 	int			no_publications;	/* Skip publication entries */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
 	int			dataOnly;
 	int			schemaOnly;
+	int			statisticsOnly;
 	int			dumpSections;
 	int			verbose;
 	int			aclsSkip;
@@ -161,6 +163,7 @@ typedef struct _restoreOptions
 	/* flags derived entirely from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } RestoreOptions;
 
 typedef struct _dumpOptions
@@ -172,6 +175,7 @@ typedef struct _dumpOptions
 	/* various user-settable parameters */
 	bool		schemaOnly;
 	bool		dataOnly;
+	bool		statisticsOnly;
 	int			dumpSections;	/* bitmask of chosen sections */
 	bool		aclsSkip;
 	const char *lockWaitTimeout;
@@ -185,6 +189,7 @@ typedef struct _dumpOptions
 	int			no_security_labels;
 	int			no_publications;
 	int			no_subscriptions;
+	int			no_statistics;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
@@ -211,6 +216,7 @@ typedef struct _dumpOptions
 	/* flags derived entirely from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } DumpOptions;
 
 /*
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 68e321212d..9bfa2f1326 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2958,6 +2958,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's a stats dump, maybe ignore it */
+	if (ropt->no_statistics && strcmp(te->desc, "STATISTICS") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2987,6 +2991,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 5f7cd2b29e..a75bc3407b 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -424,6 +424,7 @@ main(int argc, char **argv)
 		{"encoding", required_argument, NULL, 'E'},
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
+		{"statistics-only", no_argument, NULL, 'X'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -452,6 +453,7 @@ main(int argc, char **argv)
 		{"no-comments", no_argument, &dopt.no_comments, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -497,7 +499,7 @@ main(int argc, char **argv)
 
 	InitDumpOptions(&dopt);
 
-	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:RsS:t:T:U:vwWxZ:",
+	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:PRsS:t:T:U:vwWxXZ:",
 							long_options, &optindex)) != -1)
 	{
 		switch (c)
@@ -571,6 +573,10 @@ main(int argc, char **argv)
 				dopt.cparams.pgport = pg_strdup(optarg);
 				break;
 
+			case 'X':			/* Dump statistics only */
+				dopt.statisticsOnly = true;
+				break;
+
 			case 'R':
 				/* no-op, still accepted for backwards compatibility */
 				break;
@@ -740,8 +746,11 @@ main(int argc, char **argv)
 	if (dopt.binary_upgrade)
 		dopt.sequence_data = 1;
 
-	if (dopt.dataOnly && dopt.schemaOnly)
-		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (((int)dopt.dataOnly + (int) dopt.schemaOnly + (int) dopt.statisticsOnly) > 1)
+		pg_fatal("options -s/--schema-only, -a/--data-only, and -X/--statistics-only cannot be used together");
+
+	if (dopt.statisticsOnly && dopt.no_statistics)
+		pg_fatal("options -X/--statistics-only and --no-statistics cannot be used together");
 
 	if (dopt.schemaOnly && foreign_servers_include_patterns.head != NULL)
 		pg_fatal("options -s/--schema-only and --include-foreign-data cannot be used together");
@@ -756,8 +765,23 @@ main(int argc, char **argv)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
 	/* set derivative flags */
-	dopt.dumpSchema = (!dopt.dataOnly);
-	dopt.dumpData = (!dopt.schemaOnly);
+	dopt.dumpSchema = dopt.schemaOnly ||
+		!(dopt.dataOnly || dopt.statisticsOnly);
+
+	dopt.dumpData = dopt.dataOnly ||
+		!(dopt.schemaOnly || dopt.statisticsOnly);
+
+	if (dopt.statisticsOnly)
+		/* stats are only thing wanted */
+		dopt.dumpStatistics = true;
+	else if (dopt.no_statistics)
+		/* stats specifically excluded */
+		dopt.dumpStatistics = false;
+	else if (dopt.binary_upgrade)
+		/* binary upgrade and not specifically excluded */
+		dopt.dumpStatistics = true;
+	else
+		dopt.dumpStatistics = !(dopt.schemaOnly || dopt.dataOnly);
 
 	/*
 	 * --inserts are already implied above if --column-inserts or
@@ -1054,6 +1078,7 @@ main(int argc, char **argv)
 	ropt->dropSchema = dopt.outputClean;
 	ropt->dataOnly = dopt.dataOnly;
 	ropt->schemaOnly = dopt.schemaOnly;
+	ropt->statisticsOnly = dopt.statisticsOnly;
 	ropt->if_exists = dopt.if_exists;
 	ropt->column_inserts = dopt.column_inserts;
 	ropt->dumpSections = dopt.dumpSections;
@@ -1132,7 +1157,7 @@ help(const char *progname)
 	printf(_("  -?, --help                   show this help, then exit\n"));
 
 	printf(_("\nOptions controlling the output content:\n"));
-	printf(_("  -a, --data-only              dump only the data, not the schema\n"));
+	printf(_("  -a, --data-only              dump only the data, not the schema or statistics\n"));
 	printf(_("  -b, --large-objects          include large objects in dump\n"));
 	printf(_("  --blobs                      (same as --large-objects, deprecated)\n"));
 	printf(_("  -B, --no-large-objects       exclude large objects in dump\n"));
@@ -1145,11 +1170,12 @@ help(const char *progname)
 	printf(_("  -N, --exclude-schema=PATTERN do NOT dump the specified schema(s)\n"));
 	printf(_("  -O, --no-owner               skip restoration of object ownership in\n"
 			 "                               plain-text format\n"));
-	printf(_("  -s, --schema-only            dump only the schema, no data\n"));
+	printf(_("  -s, --schema-only            dump only the schema, no data or statistics\n"));
 	printf(_("  -S, --superuser=NAME         superuser user name to use in plain-text format\n"));
 	printf(_("  -t, --table=PATTERN          dump only the specified table(s)\n"));
 	printf(_("  -T, --exclude-table=PATTERN  do NOT dump the specified table(s)\n"));
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
+	printf(_("  -X, --statistics-only        dump only the statistics, not schema or data\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
@@ -1176,6 +1202,7 @@ help(const char *progname)
 	printf(_("  --no-comments                do not dump comments\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -6673,6 +6700,42 @@ getFuncs(Archive *fout)
 	destroyPQExpBuffer(query);
 }
 
+/*
+ * getRelationStatistics
+ *    register the statistics object as a dependent of the relation.
+ *
+ */
+static RelStatsInfo *
+getRelationStatistics(Archive *fout, DumpableObject *rel, char relkind)
+{
+	if ((relkind == RELKIND_RELATION) ||
+		(relkind == RELKIND_PARTITIONED_TABLE) ||
+		(relkind == RELKIND_INDEX) ||
+		(relkind == RELKIND_PARTITIONED_INDEX) ||
+		(relkind == RELKIND_MATVIEW))
+	{
+		RelStatsInfo	   *info = pg_malloc0(sizeof(RelStatsInfo));
+		DumpableObject	   *dobj = &info->dobj;
+
+		dobj->objType = DO_REL_STATS;
+		dobj->catId.tableoid = 0;
+		dobj->catId.oid = 0;
+		AssignDumpId(dobj);
+		dobj->dependencies = (DumpId *) pg_malloc(sizeof(DumpId));
+		dobj->dependencies[0] = rel->dumpId;
+		dobj->nDeps = 1;
+		dobj->allocDeps = 1;
+		dobj->components |= DUMP_COMPONENT_STATISTICS;
+		dobj->dump = rel->dump;
+		dobj->name = pg_strdup(rel->name);
+		dobj->namespace = rel->namespace;
+		info->relkind = relkind;
+
+		return info;
+	}
+	return NULL;
+}
+
 /*
  * getTables
  *	  read all the tables (no indexes) in the system catalogs,
@@ -7050,6 +7113,7 @@ getTables(Archive *fout, int *numTables)
 
 		/* Tables have data */
 		tblinfo[i].dobj.components |= DUMP_COMPONENT_DATA;
+		tblinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
 
 		/* Mark whether table has an ACL */
 		if (!PQgetisnull(res, i, i_relacl))
@@ -7098,6 +7162,8 @@ getTables(Archive *fout, int *numTables)
 				}
 			}
 		}
+		if (tblinfo[i].interesting)
+			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relkind);
 	}
 
 	if (query->len != 0)
@@ -7533,11 +7599,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 		for (int c = 0; c < numinds; c++, j++)
 		{
 			char		contype;
+			char		indexkind;
+			RelStatsInfo   *relstats;
 
 			indxinfo[j].dobj.objType = DO_INDEX;
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -7560,7 +7629,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			{
 				NULL, NULL
 			};
+
+			if (indxinfo[j].parentidx == 0)
+				indexkind = RELKIND_INDEX;
+			else
+				indexkind = RELKIND_PARTITIONED_INDEX;
+
 			contype = *(PQgetvalue(res, j, i_contype));
+			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, indexkind);
 
 			if (contype == 'p' || contype == 'u' || contype == 'x')
 			{
@@ -7593,6 +7669,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				constrinfo->separate = true;
 
 				indxinfo[j].indexconstraint = constrinfo->dobj.dumpId;
+				if (relstats != NULL)
+					addObjectDependency(&relstats->dobj, constrinfo->dobj.dumpId);
 			}
 			else
 			{
@@ -10030,6 +10108,296 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * Tabular description of the parameters to pg_restore_relation_stats()
+ * required, param_name, param_type
+ */
+static const char *rel_stats_arginfo[][3] = {
+	{"f", "relation", "regclass::oid"},
+	{"f", "version", "integer"},
+	{"f", "relpages", "integer"},
+	{"f", "reltuples", "real"},
+	{"f", "relallvisible", "integer"},
+};
+
+/*
+ * Tabular description of the parameters to pg_restore_attribute_stats()
+ * required, param_name, param_type
+ */
+static const char *att_stats_arginfo[][3] = {
+	{"f", "relation", "regclass::oid"},
+	{"f", "attname", "name"},
+	{"f", "inherited", "boolean"},
+	{"f", "version", "integer"},
+	{"f", "null_frac", "float4"},
+	{"f", "avg_width", "integer"},
+	{"f", "n_distinct", "float4"},
+	{"f", "most_common_vals", "text"},
+	{"f", "most_common_freqs", "float4[]"},
+	{"f", "histogram_bounds", "text"},
+	{"f", "correlation", "float4"},
+	{"f", "most_common_elems", "text"},
+	{"f", "most_common_elem_freqs", "float4[]"},
+	{"f", "elem_count_histogram", "float4[]"},
+	{"f", "range_length_histogram", "text"},
+	{"f", "range_empty_frac", "float4"},
+	{"f", "range_bounds_histogram", "text"},
+};
+
+/*
+ * getRelStatsExportQuery --
+ *
+ * Generate a query that will fetch all relation (e.g. pg_class)
+ * stats for a given relation.
+ */
+static void
+getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "current_setting('server_version_num') AS version, "
+						 "c.relpages, c.reltuples, c.relallvisible "
+						 "FROM pg_class c "
+						 "JOIN pg_namespace n "
+						 "ON n.oid = c.relnamespace "
+						 "WHERE n.nspname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND c.relname = ");
+	appendStringLiteralAH(query, relname, fout);
+}
+
+/*
+ * getAttStatsExportQuery --
+ *
+ * Generate a query that will fetch all attribute (e.g. pg_statistic)
+ * stats for a given relation.
+ */
+static void
+getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "s.attname,"
+						 "s.inherited,"
+						 "current_setting('server_version_num') AS version, "
+						 "s.null_frac,"
+						 "s.avg_width,"
+						 "s.n_distinct,"
+						 "s.most_common_vals,"
+						 "s.most_common_freqs,"
+						 "s.histogram_bounds,"
+						 "s.correlation,"
+						 "s.most_common_elems,"
+						 "s.most_common_elem_freqs,"
+						 "s.elem_count_histogram,");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 "s.range_length_histogram,"
+							 "s.range_empty_frac,"
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(query,
+							 "NULL AS range_length_histogram,"
+							 "NULL AS range_empty_frac,"
+							 "NULL AS range_bounds_histogram ");
+
+	appendPQExpBufferStr(query,
+						 "FROM pg_stats s "
+						 "JOIN pg_namespace n "
+						 "ON n.nspname = s.schemaname "
+						 "JOIN pg_class c "
+						 "ON c.relname = s.tablename "
+						 "AND c.relnamespace = n.oid "
+						 "WHERE s.schemaname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND s.tablename = ");
+	appendStringLiteralAH(query, relname, fout);
+	appendPQExpBufferStr(query, " ORDER BY s.attname, s.inherited");
+}
+
+
+/*
+ * appendNamedArgument --
+ *
+ * Convenience routine for constructing parameters of the form:
+ * 'paraname', 'value'::type
+ */
+static void
+appendNamedArgument(PQExpBuffer out, Archive *fout,
+					const char *argname, bool positional,
+					const char *argval, const char *argtype)
+{
+	appendPQExpBufferStr(out, "\t");
+
+	if (!positional)
+	{
+		appendStringLiteralAH(out, argname, fout);
+		appendPQExpBufferStr(out, ", ");
+	}
+
+	appendStringLiteralAH(out, argval, fout);
+	appendPQExpBuffer(out, "::%s", argtype);
+}
+
+/*
+ * appendRelStatsImport --
+ *
+ * Append a formatted pg_restore_relation_stats statement.
+ */
+static void
+appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	const char *sep = "";
+
+	if (PQntuples(res) == 0)
+		return;
+
+	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
+
+	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
+	{
+		bool		positional = (rel_stats_arginfo[argno][0][0] == 't');
+		const char *argname = rel_stats_arginfo[argno][1];
+		const char *argtype = rel_stats_arginfo[argno][2];
+		int			fieldno = PQfnumber(res, argname);
+
+		if (fieldno < 0)
+			pg_fatal("relation stats export query missing field '%s'",
+					 argname);
+
+		if (PQgetisnull(res, 0, fieldno))
+		{
+			if (!positional)
+				pg_fatal("relation stats export query unexpected NULL in '%s'",
+						 argname);
+			else
+				continue;
+		}
+
+		appendPQExpBufferStr(out, sep);
+		appendNamedArgument(out, fout, argname, positional,
+							PQgetvalue(res, 0, fieldno), argtype);
+
+		sep = ",\n";
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+/*
+ * appendAttStatsImport --
+ *
+ * Append a series of formatted pg_restore_attribute_stats statements.
+ */
+static void
+appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	{
+		const char *sep = "";
+
+		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+		for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
+		{
+			bool		positional = (att_stats_arginfo[argno][0][0] == 't');
+			const char *argname = att_stats_arginfo[argno][1];
+			const char *argtype = att_stats_arginfo[argno][2];
+			int			fieldno = PQfnumber(res, argname);
+
+			if (fieldno < 0)
+				pg_fatal("attribute stats export query missing field '%s'",
+						 argname);
+
+			if (PQgetisnull(res, rownum, fieldno))
+			{
+				if (positional)
+					pg_fatal("attribute stats export query unexpected NULL in '%s'",
+							 argname);
+				else
+					continue;
+			}
+
+			appendPQExpBufferStr(out, sep);
+			appendNamedArgument(out, fout, argname, positional,
+								PQgetvalue(res, rownum, fieldno), argtype);
+			sep = ",\n";
+		}
+		appendPQExpBufferStr(out, "\n);\n");
+	}
+}
+
+/*
+ * get statistics dump section, which depends on the parent object type
+ *
+ * objects created in SECTION_PRE_DATA have stats in SECTION_DATA
+ * objects created in SECTION_POST_DATA have stats in SECTION_POST_DATA
+ */
+static teSection
+statisticsDumpSection(const RelStatsInfo *rsinfo)
+{
+
+	if ((rsinfo->relkind == RELKIND_MATVIEW) ||
+		(rsinfo->relkind == RELKIND_INDEX) ||
+		(rsinfo->relkind == RELKIND_PARTITIONED_INDEX))
+			return SECTION_POST_DATA;
+
+	return SECTION_DATA;
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	PGresult	   *res;
+	PQExpBuffer		query;
+	PQExpBuffer		out;
+	PQExpBuffer		tag;
+	DumpableObject *dobj = (DumpableObject *) &rsinfo->dobj;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", "STATISTICS DATA", fmtId(dobj->name));
+
+	query = createPQExpBuffer();
+	out = createPQExpBuffer();
+
+	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendRelStatsImport(out, fout, res);
+	PQclear(res);
+
+	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendAttStatsImport(out, fout, res);
+	PQclear(res);
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATISTICS DATA",
+							  .section = statisticsDumpSection(rsinfo),
+							  .createStmt = out->data,
+							  .deps = dobj->dependencies,
+							  .nDeps = dobj->nDeps));
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(out);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -10478,6 +10846,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_SUBSCRIPTION_REL:
 			dumpSubscriptionTable(fout, (const SubRelInfo *) dobj);
 			break;
+		case DO_REL_STATS:
+			dumpRelationStats(fout, (const RelStatsInfo *) dobj);
+			break;
 		case DO_PRE_DATA_BOUNDARY:
 		case DO_POST_DATA_BOUNDARY:
 			/* never dumped, nothing to do */
@@ -16748,6 +17119,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 		free(indstatvalsarray);
 	}
 
+	/* Comments and stats share same .dep */
+
 	/* Dump Index Comments */
 	if (indxinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
 		dumpComment(fout, "INDEX", qindxname,
@@ -18430,6 +18803,16 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 				/* must come after the pre-data boundary */
 				addObjectDependency(dobj, preDataBound->dumpId);
 				break;
+			case DO_REL_STATS:
+				/* stats section varies by parent object type, DATA or POST */
+				if (statisticsDumpSection((RelStatsInfo *) dobj) == SECTION_DATA)
+				{
+					addObjectDependency(dobj, preDataBound->dumpId);
+					addObjectDependency(postDataBound, dobj->dumpId);
+				}
+				else
+					addObjectDependency(dobj, postDataBound->dumpId);
+				break;
 		}
 	}
 }
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 4b2e5870a9..602f3e4417 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -84,6 +84,7 @@ typedef enum
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION,
 	DO_SUBSCRIPTION_REL,		/* see note for SubRelInfo */
+	DO_REL_STATS,
 } DumpableObjectType;
 
 /*
@@ -101,6 +102,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
@@ -416,6 +418,12 @@ typedef struct _indexAttachInfo
 	IndxInfo   *partitionIdx;	/* link to index on partition */
 } IndexAttachInfo;
 
+typedef struct _relStatsInfo
+{
+	DumpableObject	dobj;
+	char			relkind;		/* 'r', 'v', 'c', etc */
+} RelStatsInfo;
+
 typedef struct _statsExtInfo
 {
 	DumpableObject dobj;
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 4cb754caa5..cfe277f5ce 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -1490,6 +1490,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "POST-DATA BOUNDARY  (ID %d)",
 					 obj->dumpId);
 			return;
+		case DO_REL_STATS:
+			snprintf(buf, bufsize,
+					 "RELATION STATISTICS FOR %s  (ID %d OID %u)",
+					 obj->name, obj->dumpId, obj->catId.oid);
+			return;
 	}
 	/* shouldn't get here */
 	snprintf(buf, bufsize,
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 882dbf8e86..aea4aeb189 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -103,6 +103,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -172,6 +173,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -451,6 +453,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -666,6 +670,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 3475168a64..e285b2828f 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -75,6 +75,7 @@ main(int argc, char **argv)
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
 	static int	no_subscriptions = 0;
+	static int  no_statistics = 0;
 	static int	strict_names = 0;
 
 	struct option cmdopts[] = {
@@ -107,6 +108,7 @@ main(int argc, char **argv)
 		{"username", 1, NULL, 'U'},
 		{"verbose", 0, NULL, 'v'},
 		{"single-transaction", 0, NULL, '1'},
+		{"statistics-only", no_argument, NULL, 'P'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -127,6 +129,7 @@ main(int argc, char **argv)
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
+		{"no-statistics", no_argument, &no_statistics, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -270,6 +273,10 @@ main(int argc, char **argv)
 				opts->aclsSkip = 1;
 				break;
 
+			case 'X':			/* Restore statistics only */
+				opts->statisticsOnly = 1;
+				break;
+
 			case '1':			/* Restore data in a single transaction */
 				opts->single_txn = true;
 				opts->exit_on_error = true;
@@ -374,6 +381,7 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
diff --git a/src/bin/pg_dump/t/001_basic.pl b/src/bin/pg_dump/t/001_basic.pl
index b9d13a0e1d..b1c7a10919 100644
--- a/src/bin/pg_dump/t/001_basic.pl
+++ b/src/bin/pg_dump/t/001_basic.pl
@@ -46,8 +46,8 @@ command_fails_like(
 
 command_fails_like(
 	[ 'pg_dump', '-s', '-a' ],
-	qr/\Qpg_dump: error: options -s\/--schema-only and -a\/--data-only cannot be used together\E/,
-	'pg_dump: options -s/--schema-only and -a/--data-only cannot be used together'
+	qr/\Qpg_dump: error: options -s\/--schema-only, -a\/--data-only, and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: options -s/--schema-only, -a/--data-only, and -X/--statistics-only cannot be used together'
 );
 
 command_fails_like(
@@ -56,6 +56,12 @@ command_fails_like(
 	'pg_dump: options -s/--schema-only and --include-foreign-data cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '--statistics-only', '--no-statistics' ],
+	qr/\Qpg_dump: error: options -X\/--statistics-only and --no-statistics cannot be used together\E/,
+	'pg_dump: options -X\/--statistics-only and --no-statistics cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-j2', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: option --include-foreign-data is not supported with parallel backup\E/,
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index b95ed87517..aee50b37a1 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -119,7 +119,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Dump only the data, not the schema (data definitions).
+        Dump only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are dumped.
        </para>
 
@@ -137,8 +137,9 @@ PostgreSQL documentation
       <listitem>
        <para>
         Include large objects in the dump.  This is the default behavior
-        except when <option>--schema</option>, <option>--table</option>, or
-        <option>--schema-only</option> is specified.  The <option>-b</option>
+        except when <option>--schema</option>, <option>--table</option>,
+        <option>--schema-only</option>, or
+        <option>--statistics-only</option> is specified.  The <option>-b</option>
         switch is therefore only useful to add large objects to dumps
         where a specific schema or table has been requested.  Note that
         large objects are considered data and therefore will be included when
@@ -512,10 +513,11 @@ PostgreSQL documentation
       <term><option>--schema-only</option></term>
       <listitem>
        <para>
-        Dump only the object definitions (schema), not data.
+        Dump only the object definitions (schema), not data or statistics.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive to <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
         <option>--section=pre-data --section=post-data</option>.
@@ -648,6 +650,18 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Dump only the statistics, not the schema (data definitions) or data.
+        Statistics for tables, materialized views, and indexes are dumped.
+       </para>
+
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-Z <replaceable class="parameter">level</replaceable></option></term>
       <term><option>-Z <replaceable class="parameter">method</replaceable></option>[:<replaceable>detail</replaceable>]</term>
@@ -829,7 +843,8 @@ PostgreSQL documentation
         though you do not need the data in it.
        </para>
        <para>
-        To exclude data for all tables in the database, see <option>--schema-only</option>.
+        To exclude data for all tables in the database, see <option>--schema-only</option>
+        or <option>--statistics-only</option>.
        </para>
       </listitem>
      </varlistentry>
@@ -1086,6 +1101,15 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index 2e3ba80258..8b1658e648 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -94,7 +94,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Restore only the data, not the schema (data definitions).
+        Restore only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are restored,
         if present in the archive.
        </para>
@@ -483,10 +483,11 @@ PostgreSQL documentation
         to the extent that schema entries are present in the archive.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive of <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
-        <option>--section=pre-data --section=post-data</option>.
+        <option>--section=pre-data --section=post-data --no-statistics</option>.
        </para>
        <para>
         (Do not confuse this with the <option>--schema</option> option, which
@@ -599,6 +600,20 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Restore only the statistics, not schema or data.
+       </para>
+       <para>
+        (Do not confuse this with the <option>--schema</option> option, which
+        uses the word <quote>schema</quote> in a different meaning.)
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-1</option></term>
       <term><option>--single-transaction</option></term>
@@ -723,6 +738,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore statistics, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
-- 
2.45.2

#180Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#179)
Re: Statistics Import and Export

On Sat, 2024-07-27 at 21:08 -0400, Corey Huinker wrote:

I don't like the idea of mixing statistics and control parameters
in
the same list.

There's no way around it, at least now we need never worry about a
confusing order for the parameters in the _restore_ functions because
they can now be in any order you like.

Perhaps I was not precise enough when I said "control" parameters.
Mainly what I was worried about is trying to take parameters that
control things like transaction behavior (in-place vs mvcc), and
pg_dump should not be specifying that kind of thing. A parameter like
"version" is specified by pg_dump anyway, so it's probably fine the way
you've done it.

SELECT pg_catalog.pg_set_attribute_stats(
    relation => 'stats_import.test'::regclass::oid,
    attname => 'arange'::name,
    inherited => false::boolean,
    null_frac => 0.5::real,
    avg_width => 2::integer,
    n_distinct => -0.1::real,
    range_empty_frac => 0.5::real,
    range_length_histogram => '{399,499,Infinity}'::text
    );
 pg_set_attribute_stats
------------------------
 
(1 row)

I like it.

and here is a restore function

-- warning: mcv cast failure
SELECT *
FROM pg_catalog.pg_restore_attribute_stats(
    'relation', 'stats_import.test'::regclass::oid,
    'attname', 'id'::name,
    'inherited', false::boolean,
    'version', 150000::integer,
    'null_frac', 0.5::real,
    'avg_width', 2::integer,
    'n_distinct', -0.4::real,
    'most_common_vals', '{2,four,3}'::text,
    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
    );
WARNING:  invalid input syntax for type integer: "four"
 row_written |          stats_applied           |          
 stats_rejected            | params_rejected
-------------+----------------------------------+--------------------
------------------+-----------------
 t           | {null_frac,avg_width,n_distinct} |
{most_common_vals,most_common_freqs} |
(1 row)

I think I like this, as well, except for the return value, which seems
like too much information and a bit over-engineered. Can we simplify it
to what's actually going to be used by pg_upgrade and other tools?

Attached is v25.

I believe 0001 and 0002 are in good shape API-wise, and I can start
getting those committed. I will try to clean up the code in the
process.

Regards,
Jeff Davis

#181Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#180)
Re: Statistics Import and Export

WARNING: invalid input syntax for type integer: "four"
row_written | stats_applied |
stats_rejected | params_rejected
-------------+----------------------------------+--------------------
------------------+-----------------
t | {null_frac,avg_width,n_distinct} |
{most_common_vals,most_common_freqs} |
(1 row)

I think I like this, as well, except for the return value, which seems
like too much information and a bit over-engineered. Can we simplify it
to what's actually going to be used by pg_upgrade and other tools?

pg_upgrade currently won't need any of it, it currently does nothing when a
statistics import fails. But it could do *something* based on this
information. For example, we might have an option
--analyze-tables-that-have-a-statistics-import-failure that analyzes tables
that have at least one statistics that didn't import. For instance,
postgres_fdw may try to do stats import first, and if that fails fall back
to a remote table sample.

We could do other things. It seems a shame to just throw away this
information when it could potentially be used in the future.

Attached is v25.

I believe 0001 and 0002 are in good shape API-wise, and I can start
getting those committed. I will try to clean up the code in the
process.

:)

#182Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#179)
2 attachment(s)
Re: Statistics Import and Export

On Sat, 2024-07-27 at 21:08 -0400, Corey Huinker wrote:

Attached is v25.

I attached new versions of 0001 and 0002. Still working on them, so
these aren't final.

v25j-0001:

* There seems to be confusion between the relation for which we are
updating the stats, and pg_class. Permissions and ShareUpdateExclusive
should be taken on the former, not the latter. For consistency with
vac_update_relstats(), RowExclusiveLock should be fine on pg_class.
* Lots of unnecessary #includes were removed.
* I refactored substantially to do basic checks in the SQL function
pg_set_relation_stats() and make calling the internal function easier.
Similar refactoring might not work for pg_set_attribute_stats(), but
that's OK.
* You don't need to declare the SQL function signatures. They're
autogenerated from pg_proc.dat into fmgrprotos.h.
* I removed the inplace stuff for this patch because there's no
coverage for it and it can be easily added back in 0003.
* I renamed the file to import_stats.c. Annoying to rebase, I know,
but better now than later.

v25j-0002:

* I just did some minor cleanup on the #includes and rebased it. I
still need to look in more detail.

Regards,
Jeff Davis

Attachments:

v25j-0001-Create-function-pg_set_relation_stats.patchtext/x-patch; charset=UTF-8; name=v25j-0001-Create-function-pg_set_relation_stats.patchDownload
From a634651fa1f4f203db889739b6943e6b03332762 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 24 Jul 2024 23:45:26 -0400
Subject: [PATCH v25j 1/2] Create function pg_set_relation_stats.

This function is used to tweak statistics on any relation that the user
owns.

The first parameter, relation, is used to identify the the relation to be
modified.

The remaining parameters correspond to the statistics attributes in
pg_class: relpages, reltuples, and relallisvible.

This function allows the user to tweak pg_class statistics values,
allowing the user to inflate rowcounts, table size, and visibility to see
what effect those changes will have on the the query planner.

The function has no return value.

Author: Corey Huinker
Discussion: https://postgr.es/m/CADkLM=e0jM7m0GFV44nNtvL22CtnFUu+pekppCVE=DOBe58RTQ@mail.gmail.com
---
 doc/src/sgml/func.sgml                     |  63 ++++++
 src/backend/statistics/Makefile            |   1 +
 src/backend/statistics/import_stats.c      | 212 +++++++++++++++++++++
 src/backend/statistics/meson.build         |   1 +
 src/include/catalog/pg_proc.dat            |   9 +
 src/test/regress/expected/stats_import.out |  99 ++++++++++
 src/test/regress/parallel_schedule         |   2 +-
 src/test/regress/sql/stats_import.sql      |  79 ++++++++
 8 files changed, 465 insertions(+), 1 deletion(-)
 create mode 100644 src/backend/statistics/import_stats.c
 create mode 100644 src/test/regress/expected/stats_import.out
 create mode 100644 src/test/regress/sql/stats_import.sql

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 0f7154b76ab..9ad63e11d45 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29895,6 +29895,69 @@ DETAIL:  Make sure pg_wal_replay_wait() isn't called within a transaction with a
     in identifying the specific disk files associated with database objects.
    </para>
 
+   <table id="functions-admin-statsimport">
+    <title>Database Object Statistics Import Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry">
+         <para role="func_signature">
+           <indexterm>
+            <primary>pg_set_relation_stats</primary>
+           </indexterm>
+           <function>pg_set_relation_stats</function> (
+            <parameter>relation</parameter> <type>regclass</type>
+            , <parameter>relpages</parameter> <type>integer</type>,
+            , <parameter>reltuples</parameter> <type>real</type>,
+            , <parameter>relallvisible</parameter> <type>integer</type> )
+           <returnvalue>void</returnvalue>
+         </para>
+         <para>
+          Updates the <structname>pg_class</structname> row for the specified
+          <parameter>relation</parameter>, setting the values for the columns
+          <structfield>reltuples</structfield>,
+          <structfield>relpages</structfield>, and
+          <structfield>relallvisible</structfield>.
+         </para>
+         <para>
+          This function mimics the behavior of <command>ANALYZE</command> in its
+          effects on the values in <structname>pg_class</structname>, except that
+          the values are supplied as parameters rather than derived from table
+          sampling.
+         </para>
+         <para>
+          The caller must either be the owner of the relation, or have superuser
+          privileges.
+         </para>
+         <para>
+          The value of <structfield>relpages</structfield> must not be less than
+          0.
+         </para>
+         <para>
+          The value of <structfield>reltuples</structfield> must not be less than
+          -1.0.
+         </para>
+         <para>
+          The value of <structfield>relallvisible</structfield> must not be less
+          than 0.
+         </para>
+       </entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
    <table id="functions-admin-dblocation">
     <title>Database Object Location Functions</title>
     <tgroup cols="1">
diff --git a/src/backend/statistics/Makefile b/src/backend/statistics/Makefile
index 89cf8c27973..5e776c02218 100644
--- a/src/backend/statistics/Makefile
+++ b/src/backend/statistics/Makefile
@@ -15,6 +15,7 @@ include $(top_builddir)/src/Makefile.global
 OBJS = \
 	dependencies.o \
 	extended_stats.o \
+	import_stats.o \
 	mcv.o \
 	mvdistinct.o
 
diff --git a/src/backend/statistics/import_stats.c b/src/backend/statistics/import_stats.c
new file mode 100644
index 00000000000..8c0a2bab305
--- /dev/null
+++ b/src/backend/statistics/import_stats.c
@@ -0,0 +1,212 @@
+/*-------------------------------------------------------------------------
+ * import_stats.c
+ *
+ *	  PostgreSQL statistics import
+ *
+ * Code supporting the direct importation of relation statistics, similar to
+ * what is done by the ANALYZE command.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *       src/backend/statistics/import_stats.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_database.h"
+#include "miscadmin.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/syscache.h"
+
+/*
+ * A role has privileges to set statistics on the relation if any of the
+ * following are true:
+ *   - the role owns the current database and the relation is not shared
+ *   - the role has the MAINTAIN privilege on the relation
+ *
+ */
+static bool
+check_can_update_stats(Relation rel)
+{
+	Oid			relid = RelationGetRelid(rel);
+	Form_pg_class reltuple = rel->rd_rel;
+
+	return ((object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId())
+			 && !reltuple->relisshared) ||
+			pg_class_aclcheck(relid, GetUserId(), ACL_MAINTAIN) == ACLCHECK_OK);
+}
+
+/*
+ * Internal function for modifying statistics for a relation.
+ */
+static bool
+relation_statistics_update(Oid reloid, int version, int32 relpages,
+						   float4 reltuples, int32 relallvisible, int elevel)
+{
+	Relation		relation;
+	Relation		crel;
+	HeapTuple		ctup;
+	Form_pg_class	pgcform;
+
+	if (relpages < -1)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relpages cannot be < -1")));
+		return false;
+	}
+
+	if (reltuples < -1.0)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("reltuples cannot be < -1.0")));
+		return false;
+	}
+
+	if (relallvisible < -1)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relallvisible cannot be < -1")));
+		return false;
+	}
+
+	/*
+	 * Open relation with ShareUpdateExclusiveLock, consistent with
+	 * ANALYZE. Only needed for permission check and then we close it (but
+	 * retain the lock).
+	 */
+	relation = try_table_open(reloid, ShareUpdateExclusiveLock);
+
+	if (!relation)
+	{
+		elog(elevel, "could not open relation with OID %u", reloid);
+		return false;
+	}
+
+	if (!check_can_update_stats(relation))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						RelationGetRelationName(relation))));
+		table_close(relation, NoLock);
+		return false;
+	}
+
+	table_close(relation, NoLock);
+
+	/*
+	 * Take RowExclusiveLock on pg_class, consistent with
+	 * vac_update_relstats().
+	 */
+	crel = table_open(RelationRelationId, RowExclusiveLock);
+
+	ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(reloid));
+	if (!HeapTupleIsValid(ctup))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_OBJECT_IN_USE),
+				 errmsg("pg_class entry for relid %u not found", reloid)));
+		return false;
+	}
+
+	pgcform = (Form_pg_class) GETSTRUCT(ctup);
+
+	/* only update pg_class if there is a meaningful change */
+	if (pgcform->relpages != relpages ||
+		pgcform->reltuples != reltuples ||
+		pgcform->relallvisible != relallvisible)
+	{
+		int			cols[3] = {
+			Anum_pg_class_relpages,
+			Anum_pg_class_reltuples,
+			Anum_pg_class_relallvisible,
+		};
+		Datum		values[3] = {
+			Int32GetDatum(relpages),
+			Float4GetDatum(reltuples),
+			Int32GetDatum(relallvisible),
+		};
+		bool		nulls[3] = {false, false, false};
+
+		TupleDesc	tupdesc = RelationGetDescr(crel);
+		HeapTuple	newtup;
+
+		CatalogIndexState indstate = CatalogOpenIndexes(crel);
+
+		newtup = heap_modify_tuple_by_cols(ctup, tupdesc, 3, cols, values,
+										   nulls);
+
+		CatalogTupleUpdateWithInfo(crel, &newtup->t_self, newtup, indstate);
+		heap_freetuple(newtup);
+		CatalogCloseIndexes(indstate);
+	}
+
+	/* release the lock, consistent with vac_update_relstats() */
+	table_close(crel, RowExclusiveLock);
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * Set statistics for a given pg_class entry.
+ *
+ * Use a transactional update, and assume statistics come from the current
+ * server version.
+ *
+ * Not intended for bulk import of statistics from older versions.
+ */
+Datum
+pg_set_relation_stats(PG_FUNCTION_ARGS)
+{
+	Oid			reloid;
+	int			version = PG_VERSION_NUM;
+	int			elevel = ERROR;
+	int32		relpages;
+	float		reltuples;
+	int32		relallvisible;
+
+	if (PG_ARGISNULL(0))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relation cannot be NULL")));
+	else
+		reloid = PG_GETARG_OID(0);
+
+	if (PG_ARGISNULL(1))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relpages cannot be NULL")));
+	else
+		relpages = PG_GETARG_INT32(1);
+
+	if (PG_ARGISNULL(2))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("reltuples cannot be NULL")));
+	else
+		reltuples = PG_GETARG_FLOAT4(2);
+
+	if (PG_ARGISNULL(3))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relallvisible cannot be NULL")));
+	else
+		relallvisible = PG_GETARG_INT32(3);
+
+
+	relation_statistics_update(reloid, version, relpages, reltuples,
+							   relallvisible, elevel);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/statistics/meson.build b/src/backend/statistics/meson.build
index 73b29a3d50a..849df3bf323 100644
--- a/src/backend/statistics/meson.build
+++ b/src/backend/statistics/meson.build
@@ -3,6 +3,7 @@
 backend_sources += files(
   'dependencies.c',
   'extended_stats.c',
+  'import_stats.c',
   'mcv.c',
   'mvdistinct.c',
 )
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index d36f6001bb1..53b02b8665c 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12247,4 +12247,13 @@
   proargnames => '{summarized_tli,summarized_lsn,pending_lsn,summarizer_pid}',
   prosrc => 'pg_get_wal_summarizer_state' },
 
+# Statistics Import
+{ oid => '8048',
+  descr => 'set statistics on relation',
+  proname => 'pg_set_relation_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'void',
+  proargtypes => 'oid int4 float4 int4',
+  proargnames => '{relation,relpages,reltuples,relallvisible}',
+  prosrc => 'pg_set_relation_stats' },
+
 ]
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
new file mode 100644
index 00000000000..f727cce9765
--- /dev/null
+++ b/src/test/regress/expected/stats_import.out
@@ -0,0 +1,99 @@
+CREATE SCHEMA stats_import;
+CREATE TYPE stats_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+CREATE TABLE stats_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+-- starting stats
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+        0 |        -1 |             0
+(1 row)
+
+-- error: regclass not found
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 0::Oid,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+ERROR:  could not open relation with OID 0
+-- error: relpages NULL
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => NULL::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+ERROR:  relpages cannot be NULL
+-- error: reltuples NULL
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => NULL::real,
+        relallvisible => 4::integer);
+ERROR:  reltuples cannot be NULL
+-- error: relallvisible NULL
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => NULL::integer);
+ERROR:  relallvisible cannot be NULL
+-- named arguments
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+ pg_set_relation_stats 
+-----------------------
+ 
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       17 |       400 |             4
+(1 row)
+
+-- positional arguments
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        'stats_import.test'::regclass,
+        18::integer,
+        401.0::real,
+        5::integer);
+ pg_set_relation_stats 
+-----------------------
+ 
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       18 |       401 |             5
+(1 row)
+
+DROP SCHEMA stats_import CASCADE;
+NOTICE:  drop cascades to 2 other objects
+DETAIL:  drop cascades to type stats_import.complex_type
+drop cascades to table stats_import.test
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 2429ec2bbaa..85fc85bfa03 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,7 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import
 
 # ----------
 # Load huge amounts of data
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
new file mode 100644
index 00000000000..effd5b892bf
--- /dev/null
+++ b/src/test/regress/sql/stats_import.sql
@@ -0,0 +1,79 @@
+CREATE SCHEMA stats_import;
+
+CREATE TYPE stats_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+
+CREATE TABLE stats_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+
+-- starting stats
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+-- error: regclass not found
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 0::Oid,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+
+-- error: relpages NULL
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => NULL::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+
+-- error: reltuples NULL
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => NULL::real,
+        relallvisible => 4::integer);
+
+-- error: relallvisible NULL
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => NULL::integer);
+
+-- named arguments
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+-- positional arguments
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        'stats_import.test'::regclass,
+        18::integer,
+        401.0::real,
+        5::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+DROP SCHEMA stats_import CASCADE;
-- 
2.34.1

v25j-0002-Create-function-pg_set_attribute_stats.patchtext/x-patch; charset=UTF-8; name=v25j-0002-Create-function-pg_set_attribute_stats.patchDownload
From 7be0bf667f32c9c7ac2429415e92facdecb796b3 Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Thu, 8 Aug 2024 17:56:22 -0700
Subject: [PATCH v25j 2/2] Create function pg_set_attribute_stats.

---
 doc/src/sgml/func.sgml                     |  63 ++
 src/backend/catalog/system_functions.sql   |  22 +
 src/backend/statistics/import_stats.c      | 999 ++++++++++++++++++++-
 src/include/catalog/pg_proc.dat            |   7 +
 src/include/statistics/statistics.h        |   3 +
 src/test/regress/expected/stats_import.out | 645 ++++++++++++-
 src/test/regress/sql/stats_import.sql      | 544 +++++++++++
 7 files changed, 2281 insertions(+), 2 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 9ad63e11d45..f35c79a3b77 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29954,6 +29954,69 @@ DETAIL:  Make sure pg_wal_replay_wait() isn't called within a transaction with a
          </para>
        </entry>
       </row>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_attribute_stats</primary>
+        </indexterm>
+        <function>pg_set_attribute_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>
+         , <parameter>attname</parameter> <type>name</type>
+         , <parameter>inherited</parameter> <type>boolean</type>
+         , <parameter>null_frac</parameter> <type>real</type>
+         , <parameter>avg_width</parameter> <type>integer</type>
+         , <parameter>n_distinct</parameter> <type>real</type>
+         [, <parameter>most_common_vals</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_freqs</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>histogram_bounds</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>correlation</parameter> <type>real</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_elems</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_elem_freqs</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>elem_count_histogram</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_length_histogram</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_empty_frac</parameter> <type>real</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_bounds_histogram</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ] )
+        <returnvalue>void</returnvalue>
+         </para>
+         <para>
+          Replaces the <structname>pg_statistic</structname> row for the
+          <structname>pg_attribute</structname> row specified by
+          <parameter>relation</parameter>, <parameter>attname</parameter>
+          and <parameter>inherited</parameter>.
+         </para>
+         <para>
+          The purpose of this function is to allow the user to experiment with applying
+          hypothetical statistics to an attribute to see if it has an effect on the query
+          planner.
+         </para>
+         <para>
+          The remaining parameters all correspond to attributes of the same name
+          found in <link linkend="view-pg-stats"><structname>pg_stats</structname></link>,
+          and the values supplied in the parameter must meet the requirements of
+          the corresponding attribute. Any parameters not supplied are assumed to be NULL.
+         </para>
+         <para>
+          This function mimics the behavior of <command>ANALYZE</command> in its
+          effects on the values in <structname>pg_statistic</structname>, except
+          that the values are supplied as parameters rather than derived from
+          table sampling.
+         </para>
+         <para>
+          The caller must either be the owner of the relation, or have superuser
+          privileges.
+         </para>
+       </entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 623b9539b15..49f0d0dab2d 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -639,6 +639,28 @@ LANGUAGE INTERNAL
 CALLED ON NULL INPUT VOLATILE PARALLEL SAFE
 AS 'pg_stat_reset_slru';
 
+CREATE OR REPLACE FUNCTION
+  pg_set_attribute_stats(relation oid,
+                         attname name,
+                         inherited bool,
+                         null_frac real,
+                         avg_width integer,
+                         n_distinct real,
+                         most_common_vals text DEFAULT NULL,
+                         most_common_freqs real[] DEFAULT NULL,
+                         histogram_bounds text DEFAULT NULL,
+                         correlation real DEFAULT NULL,
+                         most_common_elems text DEFAULT NULL,
+                         most_common_elem_freqs real[] DEFAULT NULL,
+                         elem_count_histogram real[] DEFAULT NULL,
+                         range_length_histogram text DEFAULT NULL,
+                         range_empty_frac real DEFAULT NULL,
+                         range_bounds_histogram text DEFAULT NULL)
+RETURNS void
+LANGUAGE INTERNAL
+CALLED ON NULL INPUT VOLATILE
+AS 'pg_set_attribute_stats';
+
 --
 -- The default permissions for functions mean that anyone can execute them.
 -- A number of functions shouldn't be executable by just anyone, but rather
diff --git a/src/backend/statistics/import_stats.c b/src/backend/statistics/import_stats.c
index 8c0a2bab305..5c17085018a 100644
--- a/src/backend/statistics/import_stats.c
+++ b/src/backend/statistics/import_stats.c
@@ -20,18 +20,48 @@
 
 #include "access/heapam.h"
 #include "catalog/indexing.h"
+#include "catalog/pg_collation.h"
 #include "catalog/pg_database.h"
+#include "catalog/pg_operator.h"
 #include "miscadmin.h"
+#include "nodes/nodeFuncs.h"
+#include "statistics/statistics.h"
 #include "utils/acl.h"
+#include "utils/array.h"
 #include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
 #include "utils/syscache.h"
 
+/*
+ * Names of parameters found in the functions pg_set_relation_stats and
+ * pg_set_attribute_stats
+ */
+const char *relation_name = "relation";
+const char *relpages_name = "relpages";
+const char *reltuples_name = "reltuples";
+const char *relallvisible_name = "relallvisible";
+const char *attname_name = "attname";
+const char *inherited_name = "inherited";
+const char *null_frac_name = "null_frac";
+const char *avg_width_name = "avg_width";
+const char *n_distinct_name = "n_distinct";
+const char *mc_vals_name = "most_common_vals";
+const char *mc_freqs_name = "most_common_freqs";
+const char *histogram_bounds_name = "histogram_bounds";
+const char *correlation_name = "correlation";
+const char *mc_elems_name = "most_common_elems";
+const char *mc_elem_freqs_name = "most_common_elem_freqs";
+const char *elem_count_hist_name = "elem_count_histogram";
+const char *range_length_hist_name = "range_length_histogram";
+const char *range_empty_frac_name = "range_empty_frac";
+const char *range_bounds_hist_name = "range_bounds_histogram";
+
 /*
  * A role has privileges to set statistics on the relation if any of the
  * following are true:
  *   - the role owns the current database and the relation is not shared
  *   - the role has the MAINTAIN privilege on the relation
- *
  */
 static bool
 check_can_update_stats(Relation rel)
@@ -210,3 +240,970 @@ pg_set_relation_stats(PG_FUNCTION_ARGS)
 
 	PG_RETURN_VOID();
 }
+
+/*
+ * Test if the type is a scalar for MCELEM purposes
+ */
+static bool
+type_is_scalar(Oid typid)
+{
+	HeapTuple	tp;
+	bool		result = false;
+
+	tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+	if (HeapTupleIsValid(tp))
+	{
+		Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+
+		result = (!OidIsValid(typtup->typanalyze));
+		ReleaseSysCache(tp);
+	}
+	return result;
+}
+
+/*
+ * If this relation is an index and that index has expressions in it, and
+ * the attnum specified is known to be an expression, then we must walk
+ * the list attributes up to the specified attnum to get the right
+ * expression.
+ */
+static Node *
+get_attr_expr(Relation rel, int attnum)
+{
+	if ((rel->rd_rel->relkind == RELKIND_INDEX
+		 || (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+		&& (rel->rd_indexprs != NIL)
+		&& (rel->rd_index->indkey.values[attnum - 1] == 0))
+	{
+		ListCell   *indexpr_item = list_head(rel->rd_indexprs);
+
+		for (int i = 0; i < attnum - 1; i++)
+			if (rel->rd_index->indkey.values[i] == 0)
+				indexpr_item = lnext(rel->rd_indexprs, indexpr_item);
+
+		if (indexpr_item == NULL)	/* shouldn't happen */
+			elog(ERROR, "too few entries in indexprs list");
+
+		return (Node *) lfirst(indexpr_item);
+	}
+	return NULL;
+}
+
+/*
+ * Fetch datatype information, this is needed to derive the proper staopN
+ * and stacollN values.
+ *
+ */
+static TypeCacheEntry *
+get_attr_stat_type(Relation rel, Name attname, int elevel,
+				   int16 *attnum, int32 *typmod, Oid *typcoll)
+{
+	Oid			relid = RelationGetRelid(rel);
+	Form_pg_attribute attr;
+	HeapTuple	atup;
+	Oid			typid;
+	Node	   *expr;
+
+	atup = SearchSysCache2(ATTNAME, ObjectIdGetDatum(relid),
+						   NameGetDatum(attname));
+
+	/* Attribute not found */
+	if (!HeapTupleIsValid(atup))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s has no attname %s",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+		return NULL;
+	}
+
+	attr = (Form_pg_attribute) GETSTRUCT(atup);
+	if (attr->attisdropped)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s attname %s is dropped",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+		return NULL;
+	}
+	*attnum = attr->attnum;
+
+	expr = get_attr_expr(rel, attr->attnum);
+
+	if (expr == NULL)
+	{
+		/* regular attribute */
+		typid = attr->atttypid;
+		*typmod = attr->atttypmod;
+		*typcoll = attr->attcollation;
+	}
+	else
+	{
+		typid = exprType(expr);
+		*typmod = exprTypmod(expr);
+
+		/*
+		 * If a collation has been specified for the index column, use that in
+		 * preference to anything else; but if not, fall back to whatever we
+		 * can get from the expression.
+		 */
+		if (OidIsValid(attr->attcollation))
+			*typcoll = attr->attcollation;
+		else
+			*typcoll = exprCollation(expr);
+	}
+	ReleaseSysCache(atup);
+
+	/* if it's a multirange, step down to the range type */
+	if (type_is_multirange(typid))
+		typid = get_multirange_range(typid);
+
+	return lookup_type_cache(typid,
+							 TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR);
+}
+
+/*
+ * Perform the cast of a known TextDatum into the type specified.
+ *
+ * If no errors are found, ok is set to true.
+ *
+ * Otherwise, set ok to false, capture the error found, and re-throw at the
+ * level specified by elevel.
+ */
+static Datum
+cast_stavalues(FmgrInfo *flinfo, Datum d, Oid typid, int32 typmod, 
+			   int elevel, bool *ok)
+{
+	LOCAL_FCINFO(fcinfo, 8);
+	char	   *s;
+	Datum		result;
+	ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+	escontext.details_wanted = true;
+
+	s = TextDatumGetCString(d);
+
+	InitFunctionCallInfoData(*fcinfo, flinfo, 3, InvalidOid,
+							 (Node *) &escontext, NULL);
+
+	fcinfo->args[0].value = CStringGetDatum(s);
+	fcinfo->args[0].isnull = false;
+	fcinfo->args[1].value = ObjectIdGetDatum(typid);
+	fcinfo->args[1].isnull = false;
+	fcinfo->args[2].value = Int32GetDatum(typmod);
+	fcinfo->args[2].isnull = false;
+
+	result = FunctionCallInvoke(fcinfo);
+
+	if (SOFT_ERROR_OCCURRED(&escontext))
+	{
+		if (elevel != ERROR)
+			escontext.error_data->elevel = elevel;
+		ThrowErrorData(escontext.error_data);
+		*ok = false;
+	}
+	else
+		*ok = true;
+
+	pfree(s);
+
+	return result;
+}
+
+
+/*
+ * Check array for any NULLs, and optionally for one-dimensionality.
+ *
+ * Report any failures at the level of elevel.
+ */
+static bool
+array_check(Datum datum, int one_dim, const char *statname, int elevel)
+{
+	ArrayType  *arr = DatumGetArrayTypeP(datum);
+	int16		elmlen;
+	char		elmalign;
+	bool		elembyval;
+	Datum	   *values;
+	bool	   *nulls;
+	int			nelems;
+
+	if (one_dim && (arr->ndim != 1))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be a multidimensional array", statname)));
+		return false;
+	}
+
+	get_typlenbyvalalign(ARR_ELEMTYPE(arr), &elmlen, &elembyval, &elmalign);
+
+	deconstruct_array(arr, ARR_ELEMTYPE(arr), elmlen, elembyval, elmalign,
+					  &values, &nulls, &nelems);
+
+	for (int i = 0; i < nelems; i++)
+		if (nulls[i])
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s array cannot contain NULL values", statname)));
+			return false;
+		}
+	return true;
+}
+
+/*
+ * Update the pg_statistic record.
+ */
+static void
+update_pg_statistic(Datum values[], bool nulls[])
+{
+	Relation	sd = table_open(StatisticRelationId, RowExclusiveLock);
+	CatalogIndexState indstate = CatalogOpenIndexes(sd);
+	HeapTuple	oldtup;
+	HeapTuple	stup;
+
+	/* Is there already a pg_statistic tuple for this attribute? */
+	oldtup = SearchSysCache3(STATRELATTINH,
+							 values[Anum_pg_statistic_starelid - 1],
+							 values[Anum_pg_statistic_staattnum - 1],
+							 values[Anum_pg_statistic_stainherit - 1]);
+
+	if (HeapTupleIsValid(oldtup))
+	{
+		/* Yes, replace it */
+		bool		replaces[Natts_pg_statistic];
+
+		for (int i = 0; i < Natts_pg_statistic; i++)
+			replaces[i] = true;
+
+		stup = heap_modify_tuple(oldtup, RelationGetDescr(sd),
+								 values, nulls, replaces);
+		ReleaseSysCache(oldtup);
+		CatalogTupleUpdateWithInfo(sd, &stup->t_self, stup, indstate);
+	}
+	else
+	{
+		/* No, insert new tuple */
+		stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
+		CatalogTupleInsertWithInfo(sd, stup, indstate);
+	}
+
+	heap_freetuple(stup);
+	CatalogCloseIndexes(indstate);
+	table_close(sd, NoLock);
+}
+
+/*
+ * Insert or Update Attribute Statistics
+ */
+static bool
+attribute_statistics_update(Datum relation_datum, bool relation_isnull,
+							Datum attname_datum, bool attname_isnull,
+							Datum inherited_datum, bool inherited_isnull,
+							Datum version_datum, bool version_isnull,
+							Datum null_frac_datum, bool null_frac_isnull,
+							Datum avg_width_datum, bool avg_width_isnull,
+							Datum n_distinct_datum, bool n_distinct_isnull,
+							Datum mc_vals_datum, bool mc_vals_isnull,
+							Datum mc_freqs_datum, bool mc_freqs_isnull, 
+							Datum histogram_bounds_datum, bool histogram_bounds_isnull,
+							Datum correlation_datum, bool correlation_isnull,
+							Datum mc_elems_datum, bool mc_elems_isnull,
+							Datum mc_elem_freqs_datum, bool mc_elem_freqs_isnull,
+							Datum elem_count_hist_datum, bool elem_count_hist_isnull,
+							Datum range_length_hist_datum, bool range_length_hist_isnull,
+							Datum range_empty_frac_datum, bool range_empty_frac_isnull,
+							Datum range_bounds_hist_datum, bool range_bounds_hist_isnull,
+							bool raise_errors)
+{
+	int			elevel = (raise_errors) ? ERROR : WARNING;
+
+	Oid			relation;
+	Name		attname;
+	int			version;
+
+	Relation	rel;
+
+	TypeCacheEntry *typcache;
+	TypeCacheEntry *elemtypcache = NULL;
+
+	int16		attnum;
+	int32		typmod;
+	Oid			typcoll;
+
+	FmgrInfo	finfo;
+
+	int			stakind_count;
+
+	Datum		values[Natts_pg_statistic];
+	bool		nulls[Natts_pg_statistic];
+
+	/*
+	 * The statkind index, we have only STATISTIC_NUM_SLOTS to hold these stats
+	 */
+	int			stakindidx = 0;
+
+	/*
+	 * Initialize output tuple.
+	 *
+	 * All non-repeating attributes should be NOT NULL. Only values for unused
+	 * statistics slots, and certain stakind-specific values for stanumbersN
+	 * and stavaluesN will ever be set NULL.
+	 */
+	for (int i = 0; i < Natts_pg_statistic; i++)
+	{
+		values[i] = (Datum) 0;
+		nulls[i] = false;
+	}
+
+	/*
+	 * Some parameters are "required" in that nothing can happen if any of
+	 * them are NULL.
+	 */
+	if (relation_isnull)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", relation_name)));
+		return false;
+	}
+	relation = DatumGetObjectId(relation_datum);
+
+	if (attname_isnull)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", attname_name)));
+		return false;
+	}
+	attname = DatumGetName(attname_datum);
+
+	if (inherited_isnull)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", inherited_name)));
+		return false;
+	}
+
+	/*
+	 * NULL version means assume current server version
+	 */
+	version = (version_isnull) ? PG_VERSION_NUM : DatumGetInt32(version_datum);
+	if (version < 90200)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Cannot export statistics prior to version 9.2")));
+		return false;
+	}
+
+	if (null_frac_isnull)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", null_frac_name)));
+		return false;
+	}
+
+	if (avg_width_isnull)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", avg_width_name)));
+		return false;
+	}
+
+	if (n_distinct_isnull)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", n_distinct_name)));
+		return false;
+	}
+
+	/*
+	 * Some parameters are linked, should both be NULL or NOT NULL.
+	 * Disagreement means that the statistic pair will fail so the 
+	 * NOT NULL one must be abandoned (set NULL) after an
+	 * ERROR/WARNING. By ensuring that the values are aligned it is 
+	 * possible to use one as a proxy for the other later.
+	 */
+
+	/*
+	 * STATISTIC_KIND_MCV = mc_vals + mc_freqs
+	 */
+	if (mc_vals_isnull != mc_freqs_isnull)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL when %s is NOT NULL",
+						mc_vals_isnull ? mc_vals_name : mc_freqs_name,
+						!mc_vals_isnull ? mc_vals_name : mc_freqs_name)));
+
+		mc_vals_isnull = true;
+		mc_freqs_isnull = true;
+	}
+
+	/*
+	 * STATISTIC_KIND_MCELEM = mc_elems + mc_elem_freqs
+	 */
+	if (mc_elems_isnull != mc_elem_freqs_isnull)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL when %s is NOT NULL",
+						mc_elems_isnull ? mc_elems_name : mc_elem_freqs_name,
+						!mc_elems_isnull ? mc_elems_name : mc_elem_freqs_name)));
+		mc_elems_isnull = true;
+		mc_elem_freqs_isnull = true;
+	}
+
+	/*
+	 * STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM =
+	 * range_length_histogram + range_empty_frac
+	 */
+	else if (range_length_hist_isnull != range_empty_frac_isnull)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL when %s is NOT NULL",
+						range_length_hist_isnull ?
+						range_length_hist_name : range_empty_frac_name,
+						!range_length_hist_isnull ?
+						range_length_hist_name : range_empty_frac_name)));
+		range_length_hist_isnull = true;
+		range_empty_frac_isnull = true;
+	}
+
+	/*
+	 * If a caller specifies more stakind-stats than we have slots to store
+	 * them, reject them all.
+	 */
+	stakind_count = (int) !mc_vals_isnull +
+		(int) !mc_elems_isnull +
+		(int) (!range_length_hist_isnull) +
+		(int) !histogram_bounds_isnull +
+		(int) !correlation_isnull +
+		(int) !elem_count_hist_isnull +
+		(int) !range_bounds_hist_isnull;
+
+	if (stakind_count > STATISTIC_NUM_SLOTS)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("imported statistics must have a maximum of %d slots "
+						"but %d given",
+						STATISTIC_NUM_SLOTS, stakind_count)));
+		return false;
+	}
+
+	rel = try_relation_open(relation, ShareUpdateExclusiveLock);
+
+	if (rel == NULL)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Parameter relation OID %u is invalid", relation)));
+		return false;
+	}
+
+	/*
+	 * Many of the values that are set for a particular stakind are entirely
+	 * derived from the attribute itself, or it's expression.
+	 */
+	typcache = get_attr_stat_type(rel, attname, elevel, &attnum, &typmod, &typcoll);
+	if (typcache == NULL)
+	{
+		relation_close(rel, NoLock);
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("unexpected typecache error")));
+		return false;
+	}
+
+	/*
+	 * Derive element type if we have stat kinds that need it.
+	 *
+	 * This duplicates some type-specific logic found in various typanalyze
+	 * functions which are called from vacuum's examine_attribute(), but using
+	 * that directly has proven awkward.
+	 */
+	if (!mc_elems_isnull || !elem_count_hist_isnull)
+	{
+		Oid			elemtypid;
+
+		if (typcache->type_id == TSVECTOROID)
+		{
+			/*
+			 * tsvectors always have a text oid base type and default
+			 * collation
+			 */
+			elemtypid = TEXTOID;
+			typcoll = DEFAULT_COLLATION_OID;
+		}
+		else if (typcache->typtype == TYPTYPE_RANGE)
+			elemtypid = get_range_subtype(typcache->type_id);
+		else
+			elemtypid = get_base_element_type(typcache->type_id);
+
+		/* not finding a basetype means we already had it */
+		if (elemtypid == InvalidOid)
+			elemtypid = typcache->type_id;
+
+		/* The stats need the eq_opr, any validation would need the lt_opr */
+		elemtypcache = lookup_type_cache(elemtypid, TYPECACHE_EQ_OPR);
+		if (elemtypcache == NULL)
+		{
+			/* warn and ignore any stats that can't be fulfilled */
+			if (!mc_elems_isnull)
+			{
+				ereport(elevel,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s cannot accept %s stats, ignored",
+								NameStr(*attname),
+								mc_elems_name)));
+				mc_elems_isnull = true;
+				mc_elem_freqs_isnull = true;
+			}
+
+			if (!elem_count_hist_isnull)
+			{
+				ereport(elevel,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s cannot accept %s stats, ignored",
+								NameStr(*attname),
+								elem_count_hist_name)));
+				elem_count_hist_isnull = true;
+			}
+		}
+	}
+
+	/*
+	 * histogram_bounds and correlation must have a type < operator
+	 */
+	if (typcache->lt_opr == InvalidOid)
+	{
+		if (!histogram_bounds_isnull)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Relation %s attname %s cannot "
+							"have stats of type %s, ignored.",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							histogram_bounds_name)));
+			histogram_bounds_isnull = true;
+		}
+
+		if (!correlation_isnull)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Relation %s attname %s cannot "
+							"have stats of type %s, ignored.",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							correlation_name)));
+			correlation_isnull = true;
+		}
+	}
+
+	/*
+	 * Scalar types can't have most_common_elems, most_common_elem_freqs, or
+	 * element_count_histogram
+	 */
+	if (type_is_scalar(typcache->type_id))
+	{
+		/* warn and ignore any stats that can't be fulfilled */
+		if (!mc_elems_isnull)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Relation %s attname %s is a scalar type, "
+							"cannot have stats of type %s, ignored",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							mc_elems_name)));
+			mc_elems_isnull = true;
+			mc_elem_freqs_isnull = true;
+		}
+
+		if (!elem_count_hist_isnull)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Relation %s attname %s is a scalar type, "
+							"cannot have stats of type %s, ignored",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							elem_count_hist_name)));
+			elem_count_hist_isnull = true;
+		}
+	}
+
+	/* Only range types can have range stats */
+	if ((typcache->typtype != TYPTYPE_RANGE) &&
+		(typcache->typtype != TYPTYPE_MULTIRANGE))
+	{
+		/* warn and ignore any stats that can't be fulfilled */
+		if (!range_length_hist_isnull)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Relation %s attname %s is not a range type, "
+							"cannot have stats of type %s",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							range_length_hist_name)));
+			range_length_hist_isnull = true;
+			range_empty_frac_isnull = true;
+		}
+
+		if (!range_bounds_hist_isnull)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Relation %s attname %s is not a range type, "
+							"cannot have stats of type %s",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							range_bounds_hist_name)));
+			range_bounds_hist_isnull = true;
+		}
+	}
+
+	if (!check_can_update_stats(rel))
+	{
+		relation_close(rel, NoLock);
+		ereport(elevel,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						RelationGetRelationName(rel))));
+		return false;
+	}
+
+	/* Populate pg_statistic tuple */
+	values[Anum_pg_statistic_starelid - 1] = relation_datum;
+	values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(attnum);
+	values[Anum_pg_statistic_stainherit - 1] = inherited_datum;
+	values[Anum_pg_statistic_stanullfrac - 1] = null_frac_datum;
+	values[Anum_pg_statistic_stawidth - 1] = avg_width_datum;
+	values[Anum_pg_statistic_stadistinct - 1] = n_distinct_datum;
+
+	fmgr_info(F_ARRAY_IN, &finfo);
+
+	/*
+	 * STATISTIC_KIND_MCV
+	 *
+	 * most_common_vals: ANYARRAY::text most_common_freqs: real[]
+	 */
+	if (!mc_vals_isnull)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_MCV);
+		Datum		staop = ObjectIdGetDatum(typcache->eq_opr);
+		Datum		stacoll = ObjectIdGetDatum(typcoll);
+		bool		converted = false;
+		Datum		stanumbers = mc_freqs_datum;
+		Datum		stavalues = cast_stavalues(&finfo, mc_vals_datum,
+											   typcache->type_id, typmod,
+											   elevel, &converted);
+
+		if (converted &&
+			array_check(stavalues, false, mc_vals_name, elevel) &&
+			array_check(stanumbers, true, mc_freqs_name, elevel))
+		{
+			values[Anum_pg_statistic_stakind1 - 1 + stakindidx] = stakind;
+			values[Anum_pg_statistic_staop1 - 1 + stakindidx] = staop;
+			values[Anum_pg_statistic_stacoll1 - 1 + stakindidx] = stacoll;
+			values[Anum_pg_statistic_stanumbers1 - 1 + stakindidx] = stanumbers;
+			values[Anum_pg_statistic_stavalues1 - 1 + stakindidx] = stavalues;
+
+			stakindidx++;
+		}
+		else
+		{
+			/* Mark as skipped */
+			mc_vals_isnull = true;
+			mc_freqs_isnull = true;
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_HISTOGRAM
+	 *
+	 * histogram_bounds: ANYARRAY::text
+	 */
+	if (!histogram_bounds_isnull)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_HISTOGRAM);
+		Datum		staop = ObjectIdGetDatum(typcache->lt_opr);
+		Datum		stacoll = ObjectIdGetDatum(typcoll);
+		Datum		stavalues;
+		bool		converted = false;
+
+		stavalues = cast_stavalues(&finfo, histogram_bounds_datum,
+								   typcache->type_id, typmod, elevel,
+								   &converted);
+
+		if (converted &&
+			array_check(stavalues, false, histogram_bounds_name, elevel))
+		{
+			values[Anum_pg_statistic_stakind1 - 1 + stakindidx] = stakind;
+			values[Anum_pg_statistic_staop1 - 1 + stakindidx] = staop;
+			values[Anum_pg_statistic_stacoll1 - 1 + stakindidx] = stacoll;
+			nulls[Anum_pg_statistic_stanumbers1 - 1 + stakindidx] = true;
+			values[Anum_pg_statistic_stavalues1 - 1 + stakindidx] = stavalues;
+
+			stakindidx++;
+		}
+		else
+			histogram_bounds_isnull = true;
+	}
+
+	/*
+	 * STATISTIC_KIND_CORRELATION
+	 *
+	 * correlation: real
+	 */
+	if (!correlation_isnull)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_CORRELATION);
+		Datum		staop = ObjectIdGetDatum(typcache->lt_opr);
+		Datum		stacoll = ObjectIdGetDatum(typcoll);
+		Datum		elems[] = {correlation_datum};
+		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+		Datum		stanumbers = PointerGetDatum(arry);
+
+		values[Anum_pg_statistic_stakind1 - 1 + stakindidx] = stakind;
+		values[Anum_pg_statistic_staop1 - 1 + stakindidx] = staop;
+		values[Anum_pg_statistic_stacoll1 - 1 + stakindidx] = stacoll;
+		values[Anum_pg_statistic_stanumbers1 - 1 + stakindidx] = stanumbers;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + stakindidx] = true;
+
+		stakindidx++;
+	}
+
+	/*
+	 * STATISTIC_KIND_MCELEM
+	 *
+	 * most_common_elem_freqs: real[]
+	 *
+	 * most_common_elems     : ANYARRAY::text
+	 */
+	if (!mc_elems_isnull)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_MCELEM);
+		Datum		staop = ObjectIdGetDatum(elemtypcache->eq_opr);
+		Datum		stacoll = ObjectIdGetDatum(typcoll);
+		Datum		stanumbers = mc_elem_freqs_datum;
+		bool		converted = false;
+		Datum		stavalues;
+
+		stavalues = cast_stavalues(&finfo, mc_elems_datum,
+								   elemtypcache->type_id, typmod,
+								   elevel, &converted);
+
+		if (converted &&
+			array_check(stavalues, false, mc_elems_name, elevel) &&
+			array_check(stanumbers, true, mc_elem_freqs_name, elevel))
+		{
+			values[Anum_pg_statistic_stakind1 - 1 + stakindidx] = stakind;
+			values[Anum_pg_statistic_staop1 - 1 + stakindidx] = staop;
+			values[Anum_pg_statistic_stacoll1 - 1 + stakindidx] = stacoll;
+			values[Anum_pg_statistic_stanumbers1 - 1 + stakindidx] = stanumbers;
+			values[Anum_pg_statistic_stavalues1 - 1 + stakindidx] = stavalues;
+
+			stakindidx++;
+		}
+		else
+		{
+			/* the mc_elem stat did not write */
+			mc_elems_isnull = true;
+			mc_elem_freqs_isnull = true;
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_DECHIST
+	 *
+	 * elem_count_histogram:	real[]
+	 */
+	if (!elem_count_hist_isnull)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_DECHIST);
+		Datum		staop = ObjectIdGetDatum(elemtypcache->eq_opr);
+		Datum		stacoll = ObjectIdGetDatum(typcoll);
+		Datum		stanumbers = elem_count_hist_datum;
+
+		values[Anum_pg_statistic_stakind1 - 1 + stakindidx] = stakind;
+		values[Anum_pg_statistic_staop1 - 1 + stakindidx] = staop;
+		values[Anum_pg_statistic_stacoll1 - 1 + stakindidx] = stacoll;
+		values[Anum_pg_statistic_stanumbers1 - 1 + stakindidx] = stanumbers;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + stakindidx] = true;
+
+		stakindidx++;
+	}
+
+	/*
+	 * STATISTIC_KIND_BOUNDS_HISTOGRAM
+	 *
+	 * range_bounds_histogram: ANYARRAY::text
+	 *
+	 * This stakind appears before STATISTIC_KIND_BOUNDS_HISTOGRAM even though
+	 * it is numerically greater, and all other stakinds appear in numerical
+	 * order. We duplicate this quirk to make before/after tests of
+	 * pg_statistic records easier.
+	 */
+	if (!range_bounds_hist_isnull)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_BOUNDS_HISTOGRAM);
+		Datum		staop = ObjectIdGetDatum(InvalidOid);
+		Datum		stacoll = ObjectIdGetDatum(InvalidOid);
+
+		bool		converted = false;
+		Datum		stavalues;
+
+		stavalues = cast_stavalues(&finfo, range_bounds_hist_datum,
+								   typcache->type_id, typmod,
+								   elevel, &converted);
+
+		if (converted &&
+			array_check(stavalues, false, "range_bounds_histogram", elevel))
+		{
+			values[Anum_pg_statistic_stakind1 - 1 + stakindidx] = stakind;
+			values[Anum_pg_statistic_staop1 - 1 + stakindidx] = staop;
+			values[Anum_pg_statistic_stacoll1 - 1 + stakindidx] = stacoll;
+			nulls[Anum_pg_statistic_stanumbers1 - 1 + stakindidx] = true;
+			values[Anum_pg_statistic_stavalues1 - 1 + stakindidx] = stavalues;
+
+			stakindidx++;
+		}
+		else
+			range_bounds_hist_isnull = true;
+	}
+
+	/*
+	 * STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM
+	 *
+	 * range_empty_frac: real
+	 *
+	 * range_length_histogram:  double precision[]::text
+	 */
+	if (!range_length_hist_isnull)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM);
+		Datum		staop = ObjectIdGetDatum(Float8LessOperator);
+		Datum		stacoll = ObjectIdGetDatum(InvalidOid);
+
+		/* The anyarray is always a float8[] for this stakind */
+		Datum		elems[] = {range_empty_frac_datum};
+		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+		Datum		stanumbers = PointerGetDatum(arry);
+
+		bool		converted = false;
+		Datum		stavalues;
+
+		stavalues = cast_stavalues(&finfo, range_length_hist_datum, FLOAT8OID,
+								   0, elevel, &converted);
+
+		if (converted &&
+			array_check(stavalues, false, range_length_hist_name, elevel))
+		{
+			values[Anum_pg_statistic_staop1 - 1 + stakindidx] = staop;
+			values[Anum_pg_statistic_stakind1 - 1 + stakindidx] = stakind;
+			values[Anum_pg_statistic_stacoll1 - 1 + stakindidx] = stacoll;
+			values[Anum_pg_statistic_stanumbers1 - 1 + stakindidx] = stanumbers;
+			values[Anum_pg_statistic_stavalues1 - 1 + stakindidx] = stavalues;
+			stakindidx++;
+		}
+		else
+		{
+			range_empty_frac_isnull = true;
+			range_length_hist_isnull = true;
+		}
+	}
+
+	/* fill in all remaining slots */
+	while (stakindidx < STATISTIC_NUM_SLOTS)
+	{
+		values[Anum_pg_statistic_stakind1 - 1 + stakindidx] = Int16GetDatum(0);
+		values[Anum_pg_statistic_staop1 - 1 + stakindidx] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_stacoll1 - 1 + stakindidx] = ObjectIdGetDatum(InvalidOid);
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + stakindidx] = true;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + stakindidx] = true;
+
+		stakindidx++;
+	}
+
+	update_pg_statistic(values, nulls);
+
+	relation_close(rel, NoLock);
+
+	return true;
+}
+
+/*
+ * Import statistics for a given relation attribute.
+ *
+ * This will insert/replace a row in pg_statistic for the given relation and
+ * attribute name.
+ *
+ * The function takes input parameters that correspond to columns in the view
+ * pg_stats.
+ *
+ * Of those, the columns attname, inherited, null_frac, avg_width, and
+ * n_distinct all correspond to NOT NULL columns in pg_statistic. These
+ * parameters have no default value and passing NULL to them will result
+ * in an error.
+ *
+ * If there is no attribute with a matching attname in the relation, the
+ * function will raise an error. Likewise for setting inherited statistics
+ * on a table that is not partitioned.
+ *
+ * The remaining parameters all belong to a specific stakind. Some stakinds
+ * have multiple parameters, and in those cases both parameters must be
+ * NOT NULL or both NULL, otherwise an error will be raised.
+ *
+ * Omitting a parameter or explicitly passing NULL means that that particular
+ * stakind is not associated with the attribute.
+ *
+ * Parameters that are NOT NULL will be inspected for consistency checks,
+ * any of which can raise an error.
+ *
+ * Parameters corresponding to ANYARRAY columns are instead passed in as text
+ * values, which is a valid input string for an array of the type or element
+ * type of the attribute. Any error generated by the array_in() function will
+ * in turn fail the function.
+ */
+Datum
+pg_set_attribute_stats(PG_FUNCTION_ARGS)
+{
+	Datum	version_datum = (Datum) 0;
+	bool	version_isnull = true;
+	bool	raise_errors = true;
+
+	attribute_statistics_update(
+		PG_GETARG_DATUM(0), PG_ARGISNULL(0),
+		PG_GETARG_DATUM(1), PG_ARGISNULL(1),
+		PG_GETARG_DATUM(2), PG_ARGISNULL(2),
+		version_datum, version_isnull,
+		PG_GETARG_DATUM(3), PG_ARGISNULL(3),
+		PG_GETARG_DATUM(4), PG_ARGISNULL(4),
+		PG_GETARG_DATUM(5), PG_ARGISNULL(5),
+		PG_GETARG_DATUM(6), PG_ARGISNULL(6),
+		PG_GETARG_DATUM(7), PG_ARGISNULL(7),
+		PG_GETARG_DATUM(8), PG_ARGISNULL(8),
+		PG_GETARG_DATUM(9), PG_ARGISNULL(9),
+		PG_GETARG_DATUM(10), PG_ARGISNULL(10),
+		PG_GETARG_DATUM(11), PG_ARGISNULL(11),
+		PG_GETARG_DATUM(12), PG_ARGISNULL(12),
+		PG_GETARG_DATUM(13), PG_ARGISNULL(13),
+		PG_GETARG_DATUM(14), PG_ARGISNULL(14),
+		PG_GETARG_DATUM(15), PG_ARGISNULL(15),
+		raise_errors);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 53b02b8665c..c89b03246d0 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12255,5 +12255,12 @@
   proargtypes => 'oid int4 float4 int4',
   proargnames => '{relation,relpages,reltuples,relallvisible}',
   prosrc => 'pg_set_relation_stats' },
+{ oid => '8049',
+  descr => 'set statistics on attribute',
+  proname => 'pg_set_attribute_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'void',
+  proargtypes => 'oid name bool float4 int4 float4 text _float4 text float4 text _float4 _float4 text float4 text',
+  proargnames => '{relation,attname,inherited,null_frac,avg_width,n_distinct,most_common_vals,most_common_freqs,histogram_bounds,correlation,most_common_elems,most_common_elem_freqs,elem_count_histogram,range_length_histogram,range_empty_frac,range_bounds_histogram}',
+  prosrc => 'pg_set_attribute_stats' },
 
 ]
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 7f2bf18716d..73d3b541dda 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -127,4 +127,7 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												int nclauses);
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
+extern Datum pg_set_relation_stats(PG_FUNCTION_ARGS);
+extern Datum pg_set_attribute_stats(PG_FUNCTION_ARGS);
+
 #endif							/* STATISTICS_H */
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index f727cce9765..0d7ea037473 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -93,7 +93,650 @@ WHERE oid = 'stats_import.test'::regclass;
        18 |       401 |             5
 (1 row)
 
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => '0'::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  Parameter relation OID 0 is invalid
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => NULL::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  relation cannot be NULL
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => NULL::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  attname cannot be NULL
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => NULL::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  inherited cannot be NULL
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => NULL::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  null_frac cannot be NULL
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => NULL::integer,
+    n_distinct => 0.3::real);
+ERROR:  avg_width cannot be NULL
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => NULL::real);
+ERROR:  n_distinct cannot be NULL
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT stanullfrac, stawidth, stadistinct
+FROM pg_statistic
+WHERE starelid = 'stats_import.test'::regclass;
+ stanullfrac | stawidth | stadistinct 
+-------------+----------+-------------
+         0.1 |        2 |         0.3
+(1 row)
+
+-- warn: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_freqs => '{0.1,0.2,0.3}'::real[]
+    );
+ERROR:  most_common_vals cannot be NULL when most_common_freqs is NOT NULL
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{1,2,3}'::text
+    );
+ERROR:  most_common_freqs cannot be NULL when most_common_vals is NOT NULL
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2023-09-30,2024-10-31,3}'::text,
+    most_common_freqs => '{0.2,0.1}'::real[]
+    );
+ERROR:  invalid input syntax for type integer: "2023-09-30"
+-- warning: mcv cast failure
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,four,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+ERROR:  invalid input syntax for type integer: "four"
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: histogram elements null value
+-- this generates no warnings, but perhaps it should
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,NULL,3,4}'::text
+    );
+ERROR:  histogram_bounds array cannot contain NULL values
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,2,3,4}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 0.5::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |         0.5 |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have mcelem
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{1,3}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+ERROR:  Relation test attname id is a scalar type, cannot have stats of type most_common_elems, ignored
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,two}'::text
+    );
+ERROR:  most_common_elem_freqs cannot be NULL when most_common_elems is NOT NULL
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
+    );
+ERROR:  most_common_elems cannot be NULL when most_common_elem_freqs is NOT NULL
+-- ok: mcelem
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             | {one,three}       | {0.3,0.2,0.2,0.3,0}    |                      |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ERROR:  Relation test attname id is a scalar type, cannot have stats of type elem_count_histogram, ignored
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
+ stats_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ERROR:  Relation test attname id is not a range type, cannot have stats of type range_length_histogram
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ERROR:  range_empty_frac cannot be NULL when range_length_histogram is NOT NULL
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real
+    );
+ERROR:  range_length_histogram cannot be NULL when range_empty_frac is NOT NULL
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
+(1 row)
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ERROR:  Relation test attname id is not a range type, cannot have stats of type range_bounds_histogram
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
+ stats_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      |                        |                  | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+(1 row)
+
+-- warn: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[],
+    histogram_bounds => '{1,2,3,4}'::text,
+    correlation => 1.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    range_empty_frac => -0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ERROR:  imported statistics must have a maximum of 5 slots but 6 given
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
+-- Generate statistics on table with data
+ANALYZE stats_import.test;
+CREATE TABLE stats_import.test_clone ( LIKE stats_import.test );
+CREATE INDEX is_odd_clone ON stats_import.test_clone(((comp).a % 2 = 1));
+--
+-- Copy stats from test to test_clone, and is_odd to is_odd_clone
+--
+SELECT s.schemaname, s.tablename, s.attname, s.inherited
+FROM pg_catalog.pg_stats AS s
+CROSS JOIN LATERAL
+    pg_catalog.pg_set_attribute_stats(
+        relation => ('stats_import.' || s.tablename || '_clone')::regclass::oid,
+        attname => s.attname,
+        inherited => s.inherited,
+        null_frac => s.null_frac,
+        avg_width => s.avg_width,
+        n_distinct => s.n_distinct,
+        most_common_vals => s.most_common_vals::text,
+        most_common_freqs => s.most_common_freqs,
+        histogram_bounds => s.histogram_bounds::text,
+        correlation => s.correlation,
+        most_common_elems => s.most_common_elems::text,
+        most_common_elem_freqs => s.most_common_elem_freqs,
+        elem_count_histogram => s.elem_count_histogram,
+        range_bounds_histogram => s.range_bounds_histogram::text,
+        range_empty_frac => s.range_empty_frac,
+        range_length_histogram => s.range_length_histogram::text) AS r
+WHERE s.schemaname = 'stats_import'
+AND s.tablename IN ('test', 'is_odd')
+ORDER BY s.tablename, s.attname, s.inherited;
+  schemaname  | tablename | attname | inherited 
+--------------+-----------+---------+-----------
+ stats_import | is_odd    | expr    | f
+ stats_import | test      | arange  | f
+ stats_import | test      | comp    | f
+ stats_import | test      | id      | f
+ stats_import | test      | name    | f
+ stats_import | test      | tags    | f
+(6 rows)
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+   relname    | num_stats 
+--------------+-----------
+ is_odd       |         1
+ is_odd_clone |         1
+ test         |         5
+ test_clone   |         5
+(4 rows)
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
 DROP SCHEMA stats_import CASCADE;
-NOTICE:  drop cascades to 2 other objects
+NOTICE:  drop cascades to 3 other objects
 DETAIL:  drop cascades to type stats_import.complex_type
 drop cascades to table stats_import.test
+drop cascades to table stats_import.test_clone
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index effd5b892bf..9693c6cfeed 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -76,4 +76,548 @@ SELECT relpages, reltuples, relallvisible
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => '0'::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => NULL::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => NULL::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => NULL::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => NULL::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => NULL::integer,
+    n_distinct => 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => NULL::real);
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+SELECT stanullfrac, stawidth, stadistinct
+FROM pg_statistic
+WHERE starelid = 'stats_import.test'::regclass;
+
+-- warn: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_freqs => '{0.1,0.2,0.3}'::real[]
+    );
+
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{1,2,3}'::text
+    );
+
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2023-09-30,2024-10-31,3}'::text,
+    most_common_freqs => '{0.2,0.1}'::real[]
+    );
+
+-- warning: mcv cast failure
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,four,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: histogram elements null value
+-- this generates no warnings, but perhaps it should
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,NULL,3,4}'::text
+    );
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,2,3,4}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 0.5::real);
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: scalars can't have mcelem
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{1,3}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,two}'::text
+    );
+
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
+    );
+
+-- ok: mcelem
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- warn: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real
+    );
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[],
+    histogram_bounds => '{1,2,3,4}'::text,
+    correlation => 1.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    range_empty_frac => -0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+
+CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
+
+-- Generate statistics on table with data
+ANALYZE stats_import.test;
+
+CREATE TABLE stats_import.test_clone ( LIKE stats_import.test );
+
+CREATE INDEX is_odd_clone ON stats_import.test_clone(((comp).a % 2 = 1));
+
+--
+-- Copy stats from test to test_clone, and is_odd to is_odd_clone
+--
+SELECT s.schemaname, s.tablename, s.attname, s.inherited
+FROM pg_catalog.pg_stats AS s
+CROSS JOIN LATERAL
+    pg_catalog.pg_set_attribute_stats(
+        relation => ('stats_import.' || s.tablename || '_clone')::regclass::oid,
+        attname => s.attname,
+        inherited => s.inherited,
+        null_frac => s.null_frac,
+        avg_width => s.avg_width,
+        n_distinct => s.n_distinct,
+        most_common_vals => s.most_common_vals::text,
+        most_common_freqs => s.most_common_freqs,
+        histogram_bounds => s.histogram_bounds::text,
+        correlation => s.correlation,
+        most_common_elems => s.most_common_elems::text,
+        most_common_elem_freqs => s.most_common_elem_freqs,
+        elem_count_histogram => s.elem_count_histogram,
+        range_bounds_histogram => s.range_bounds_histogram::text,
+        range_empty_frac => s.range_empty_frac,
+        range_length_histogram => s.range_length_histogram::text) AS r
+WHERE s.schemaname = 'stats_import'
+AND s.tablename IN ('test', 'is_odd')
+ORDER BY s.tablename, s.attname, s.inherited;
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test_clone'::regclass;
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test'::regclass;
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd_clone'::regclass;
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd'::regclass;
+
 DROP SCHEMA stats_import CASCADE;
-- 
2.34.1

#183Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#181)
2 attachment(s)
Re: Statistics Import and Export

On Sun, 2024-08-04 at 01:09 -0400, Corey Huinker wrote:

I believe 0001 and 0002 are in good shape API-wise, and I can start
getting those committed. I will try to clean up the code in the
process.

Attached v26j.

I'm slowly refactoring it and rediscovering some of the interesting
corners in deriving the right information to store the stats. There's
still a ways to go, though. The error paths could also use some work.

Regards,
Jeff Davis

Attachments:

v26j-0001-Create-function-pg_set_relation_stats.patchtext/x-patch; charset=UTF-8; name=v26j-0001-Create-function-pg_set_relation_stats.patchDownload
From d38008b0e60e0241c86f583c951763cc01d1e270 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 24 Jul 2024 23:45:26 -0400
Subject: [PATCH v26j 1/2] Create function pg_set_relation_stats.

This function is used to tweak statistics on any relation that the user
owns.

The first parameter, relation, is used to identify the the relation to be
modified.

The remaining parameters correspond to the statistics attributes in
pg_class: relpages, reltuples, and relallisvible.

This function allows the user to tweak pg_class statistics values,
allowing the user to inflate rowcounts, table size, and visibility to see
what effect those changes will have on the the query planner.

The function has no return value.

Author: Corey Huinker
Discussion: https://postgr.es/m/CADkLM=e0jM7m0GFV44nNtvL22CtnFUu+pekppCVE=DOBe58RTQ@mail.gmail.com
---
 doc/src/sgml/func.sgml                     |  65 +++++++
 src/backend/statistics/Makefile            |   1 +
 src/backend/statistics/import_stats.c      | 212 +++++++++++++++++++++
 src/backend/statistics/meson.build         |   1 +
 src/include/catalog/pg_proc.dat            |   9 +
 src/test/regress/expected/stats_import.out |  99 ++++++++++
 src/test/regress/parallel_schedule         |   2 +-
 src/test/regress/sql/stats_import.sql      |  79 ++++++++
 8 files changed, 467 insertions(+), 1 deletion(-)
 create mode 100644 src/backend/statistics/import_stats.c
 create mode 100644 src/test/regress/expected/stats_import.out
 create mode 100644 src/test/regress/sql/stats_import.sql

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 5dd95d73a1a..02d93f6c925 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29938,6 +29938,71 @@ DETAIL:  Make sure pg_wal_replay_wait() isn't called within a transaction with a
     in identifying the specific disk files associated with database objects.
    </para>
 
+   <table id="functions-admin-statsimport">
+    <title>Database Object Statistics Import Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry">
+        <para role="func_signature">
+         <indexterm>
+          <primary>pg_set_relation_stats</primary>
+         </indexterm>
+         <function>pg_set_relation_stats</function> ( <parameter>relation</parameter> <type>regclass</type>, <parameter>relpages</parameter> <type>integer</type>, <parameter>reltuples</parameter> <type>real</type>, <parameter>relallvisible</parameter> <type>integer</type> )
+         <returnvalue>void</returnvalue>
+        </para>
+        <para>
+         Updates table-level statistics for the given relation to the
+         specified values. The parameters correspond to columns in <link
+         linkend="catalog-pg-class"><structname>pg_class</structname></link>.
+        </para>
+        <para>
+         Ordinarily, these statistics are collected automatically or updated
+         as a part of <xref linkend="sql-vacuum"/> or <xref
+         linkend="sql-analyze"/>, so it's not necessary to call this
+         function. However, it may be useful when testing the effects of
+         statistics on the planner to understand or anticipate plan changes.
+        </para>
+        <para>
+         The caller must have the <literal>MAINTAIN</literal> privilege on
+         the table or be the owner of the database.
+        </para>
+        <para>
+         The value of <structfield>relpages</structfield> must be greater than
+         or equal to <literal>0</literal>,
+         <structfield>reltuples</structfield> must be greater than or equal to
+         <literal>-1.0</literal>, and <structfield>relallvisible</structfield>
+         must be greater than or equal to <literal>0</literal>.
+        </para>
+        <warning>
+         <para>
+          Changes made by this function are likely to be overwritten by <link
+          linkend="autovacuum">autovacuum</link> (or manual
+          <command>VACUUM</command> or <command>ANALYZE</command>) and should
+          be considered temporary.
+         </para>
+         <para>
+          The signature of this function may change in new major releases if
+          there are changes to the statistics being tracked.
+         </para>
+        </warning>
+       </entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
    <table id="functions-admin-dblocation">
     <title>Database Object Location Functions</title>
     <tgroup cols="1">
diff --git a/src/backend/statistics/Makefile b/src/backend/statistics/Makefile
index 89cf8c27973..5e776c02218 100644
--- a/src/backend/statistics/Makefile
+++ b/src/backend/statistics/Makefile
@@ -15,6 +15,7 @@ include $(top_builddir)/src/Makefile.global
 OBJS = \
 	dependencies.o \
 	extended_stats.o \
+	import_stats.o \
 	mcv.o \
 	mvdistinct.o
 
diff --git a/src/backend/statistics/import_stats.c b/src/backend/statistics/import_stats.c
new file mode 100644
index 00000000000..8c0a2bab305
--- /dev/null
+++ b/src/backend/statistics/import_stats.c
@@ -0,0 +1,212 @@
+/*-------------------------------------------------------------------------
+ * import_stats.c
+ *
+ *	  PostgreSQL statistics import
+ *
+ * Code supporting the direct importation of relation statistics, similar to
+ * what is done by the ANALYZE command.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *       src/backend/statistics/import_stats.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_database.h"
+#include "miscadmin.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/syscache.h"
+
+/*
+ * A role has privileges to set statistics on the relation if any of the
+ * following are true:
+ *   - the role owns the current database and the relation is not shared
+ *   - the role has the MAINTAIN privilege on the relation
+ *
+ */
+static bool
+check_can_update_stats(Relation rel)
+{
+	Oid			relid = RelationGetRelid(rel);
+	Form_pg_class reltuple = rel->rd_rel;
+
+	return ((object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId())
+			 && !reltuple->relisshared) ||
+			pg_class_aclcheck(relid, GetUserId(), ACL_MAINTAIN) == ACLCHECK_OK);
+}
+
+/*
+ * Internal function for modifying statistics for a relation.
+ */
+static bool
+relation_statistics_update(Oid reloid, int version, int32 relpages,
+						   float4 reltuples, int32 relallvisible, int elevel)
+{
+	Relation		relation;
+	Relation		crel;
+	HeapTuple		ctup;
+	Form_pg_class	pgcform;
+
+	if (relpages < -1)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relpages cannot be < -1")));
+		return false;
+	}
+
+	if (reltuples < -1.0)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("reltuples cannot be < -1.0")));
+		return false;
+	}
+
+	if (relallvisible < -1)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relallvisible cannot be < -1")));
+		return false;
+	}
+
+	/*
+	 * Open relation with ShareUpdateExclusiveLock, consistent with
+	 * ANALYZE. Only needed for permission check and then we close it (but
+	 * retain the lock).
+	 */
+	relation = try_table_open(reloid, ShareUpdateExclusiveLock);
+
+	if (!relation)
+	{
+		elog(elevel, "could not open relation with OID %u", reloid);
+		return false;
+	}
+
+	if (!check_can_update_stats(relation))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						RelationGetRelationName(relation))));
+		table_close(relation, NoLock);
+		return false;
+	}
+
+	table_close(relation, NoLock);
+
+	/*
+	 * Take RowExclusiveLock on pg_class, consistent with
+	 * vac_update_relstats().
+	 */
+	crel = table_open(RelationRelationId, RowExclusiveLock);
+
+	ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(reloid));
+	if (!HeapTupleIsValid(ctup))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_OBJECT_IN_USE),
+				 errmsg("pg_class entry for relid %u not found", reloid)));
+		return false;
+	}
+
+	pgcform = (Form_pg_class) GETSTRUCT(ctup);
+
+	/* only update pg_class if there is a meaningful change */
+	if (pgcform->relpages != relpages ||
+		pgcform->reltuples != reltuples ||
+		pgcform->relallvisible != relallvisible)
+	{
+		int			cols[3] = {
+			Anum_pg_class_relpages,
+			Anum_pg_class_reltuples,
+			Anum_pg_class_relallvisible,
+		};
+		Datum		values[3] = {
+			Int32GetDatum(relpages),
+			Float4GetDatum(reltuples),
+			Int32GetDatum(relallvisible),
+		};
+		bool		nulls[3] = {false, false, false};
+
+		TupleDesc	tupdesc = RelationGetDescr(crel);
+		HeapTuple	newtup;
+
+		CatalogIndexState indstate = CatalogOpenIndexes(crel);
+
+		newtup = heap_modify_tuple_by_cols(ctup, tupdesc, 3, cols, values,
+										   nulls);
+
+		CatalogTupleUpdateWithInfo(crel, &newtup->t_self, newtup, indstate);
+		heap_freetuple(newtup);
+		CatalogCloseIndexes(indstate);
+	}
+
+	/* release the lock, consistent with vac_update_relstats() */
+	table_close(crel, RowExclusiveLock);
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * Set statistics for a given pg_class entry.
+ *
+ * Use a transactional update, and assume statistics come from the current
+ * server version.
+ *
+ * Not intended for bulk import of statistics from older versions.
+ */
+Datum
+pg_set_relation_stats(PG_FUNCTION_ARGS)
+{
+	Oid			reloid;
+	int			version = PG_VERSION_NUM;
+	int			elevel = ERROR;
+	int32		relpages;
+	float		reltuples;
+	int32		relallvisible;
+
+	if (PG_ARGISNULL(0))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relation cannot be NULL")));
+	else
+		reloid = PG_GETARG_OID(0);
+
+	if (PG_ARGISNULL(1))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relpages cannot be NULL")));
+	else
+		relpages = PG_GETARG_INT32(1);
+
+	if (PG_ARGISNULL(2))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("reltuples cannot be NULL")));
+	else
+		reltuples = PG_GETARG_FLOAT4(2);
+
+	if (PG_ARGISNULL(3))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relallvisible cannot be NULL")));
+	else
+		relallvisible = PG_GETARG_INT32(3);
+
+
+	relation_statistics_update(reloid, version, relpages, reltuples,
+							   relallvisible, elevel);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/statistics/meson.build b/src/backend/statistics/meson.build
index 73b29a3d50a..849df3bf323 100644
--- a/src/backend/statistics/meson.build
+++ b/src/backend/statistics/meson.build
@@ -3,6 +3,7 @@
 backend_sources += files(
   'dependencies.c',
   'extended_stats.c',
+  'import_stats.c',
   'mcv.c',
   'mvdistinct.c',
 )
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 4abc6d95262..d700dd50f7b 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12255,4 +12255,13 @@
   proargnames => '{summarized_tli,summarized_lsn,pending_lsn,summarizer_pid}',
   prosrc => 'pg_get_wal_summarizer_state' },
 
+# Statistics Import
+{ oid => '8048',
+  descr => 'set statistics on relation',
+  proname => 'pg_set_relation_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'void',
+  proargtypes => 'oid int4 float4 int4',
+  proargnames => '{relation,relpages,reltuples,relallvisible}',
+  prosrc => 'pg_set_relation_stats' },
+
 ]
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
new file mode 100644
index 00000000000..f727cce9765
--- /dev/null
+++ b/src/test/regress/expected/stats_import.out
@@ -0,0 +1,99 @@
+CREATE SCHEMA stats_import;
+CREATE TYPE stats_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+CREATE TABLE stats_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+-- starting stats
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+        0 |        -1 |             0
+(1 row)
+
+-- error: regclass not found
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 0::Oid,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+ERROR:  could not open relation with OID 0
+-- error: relpages NULL
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => NULL::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+ERROR:  relpages cannot be NULL
+-- error: reltuples NULL
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => NULL::real,
+        relallvisible => 4::integer);
+ERROR:  reltuples cannot be NULL
+-- error: relallvisible NULL
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => NULL::integer);
+ERROR:  relallvisible cannot be NULL
+-- named arguments
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+ pg_set_relation_stats 
+-----------------------
+ 
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       17 |       400 |             4
+(1 row)
+
+-- positional arguments
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        'stats_import.test'::regclass,
+        18::integer,
+        401.0::real,
+        5::integer);
+ pg_set_relation_stats 
+-----------------------
+ 
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       18 |       401 |             5
+(1 row)
+
+DROP SCHEMA stats_import CASCADE;
+NOTICE:  drop cascades to 2 other objects
+DETAIL:  drop cascades to type stats_import.complex_type
+drop cascades to table stats_import.test
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 2429ec2bbaa..85fc85bfa03 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,7 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import
 
 # ----------
 # Load huge amounts of data
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
new file mode 100644
index 00000000000..effd5b892bf
--- /dev/null
+++ b/src/test/regress/sql/stats_import.sql
@@ -0,0 +1,79 @@
+CREATE SCHEMA stats_import;
+
+CREATE TYPE stats_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+
+CREATE TABLE stats_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+
+-- starting stats
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+-- error: regclass not found
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 0::Oid,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+
+-- error: relpages NULL
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => NULL::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+
+-- error: reltuples NULL
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => NULL::real,
+        relallvisible => 4::integer);
+
+-- error: relallvisible NULL
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => NULL::integer);
+
+-- named arguments
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+-- positional arguments
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        'stats_import.test'::regclass,
+        18::integer,
+        401.0::real,
+        5::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+DROP SCHEMA stats_import CASCADE;
-- 
2.34.1

v26j-0002-Create-function-pg_set_attribute_stats.patchtext/x-patch; charset=UTF-8; name=v26j-0002-Create-function-pg_set_attribute_stats.patchDownload
From 66e6e735d7e7c3aeb2f374812259864d4c4e2f1e Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Thu, 8 Aug 2024 17:56:22 -0700
Subject: [PATCH v26j 2/2] Create function pg_set_attribute_stats.

---
 doc/src/sgml/func.sgml                     |  60 ++
 src/backend/catalog/system_functions.sql   |  22 +
 src/backend/statistics/import_stats.c      | 957 ++++++++++++++++++++-
 src/include/catalog/pg_proc.dat            |   7 +
 src/include/statistics/statistics.h        |   3 +
 src/test/regress/expected/stats_import.out | 645 +++++++++++++-
 src/test/regress/sql/stats_import.sql      | 544 ++++++++++++
 7 files changed, 2236 insertions(+), 2 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 02d93f6c925..402a14fdb1f 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29999,6 +29999,66 @@ DETAIL:  Make sure pg_wal_replay_wait() isn't called within a transaction with a
         </warning>
        </entry>
       </row>
+      <row>
+       <entry role="func_table_entry">
+        <para role="func_signature">
+         <indexterm>
+          <primary>pg_set_attribute_stats</primary>
+         </indexterm>
+         <function>pg_set_attribute_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>attname</parameter> <type>name</type>,
+         <parameter>inherited</parameter> <type>boolean</type>,
+         <parameter>null_frac</parameter> <type>real</type>,
+         <parameter>avg_width</parameter> <type>integer</type>,
+         <parameter>n_distinct</parameter> <type>real</type>
+         <optional>, <parameter>most_common_vals</parameter> <type>text</type>, <parameter>most_common_freqs</parameter> <type>real[]</type> </optional>
+         <optional>, <parameter>histogram_bounds</parameter> <type>text</type> </optional>
+         <optional>, <parameter>correlation</parameter> <type>real</type> </optional>
+         <optional>, <parameter>most_common_elems</parameter> <type>text</type>, <parameter>most_common_elem_freqs</parameter> <type>real[]</type> </optional>
+         <optional>, <parameter>elem_count_histogram</parameter> <type>real[]</type> </optional>
+         <optional>, <parameter>range_length_histogram</parameter> <type>text</type> </optional>
+         <optional>, <parameter>range_empty_frac</parameter> <type>real</type> </optional>
+         <optional>, <parameter>range_bounds_histogram</parameter> <type>text</type> </optional> )
+         <returnvalue>void</returnvalue>
+        </para>
+        <para>
+         Updates attribute-level statistics for the given relation and
+         attribute name to the specified values. The parameters correspond to
+         to attributes of the same name found in the <link
+         linkend="view-pg-stats"><structname>pg_stats</structname></link>
+         view, and the values supplied in the parameter must meet the
+         requirements of the corresponding attribute.
+        </para>
+        <para>
+         Any optional parameters left unspecified will cause the corresponding
+         statistics to be set to <literal>NULL</literal>.
+        </para>
+        <para>
+         Ordinarily, these statistics are collected automatically or updated
+         as a part of <xref linkend="sql-vacuum"/> or <xref
+         linkend="sql-analyze"/>, so it's not necessary to call this
+         function. However, it may be useful when testing the effects of
+         statistics on the planner to understand or anticipate plan changes.
+        </para>
+        <para>
+         The caller must have the <literal>MAINTAIN</literal> privilege on
+         the table or be the owner of the database.
+        </para>
+        <warning>
+         <para>
+          Changes made by this function are likely to be overwritten by <link
+          linkend="autovacuum">autovacuum</link> (or manual
+          <command>VACUUM</command> or <command>ANALYZE</command>) and should
+          be considered temporary.
+         </para>
+         <para>
+          The signature of this function may change in new major releases if
+          there are changes to the statistics being tracked.
+         </para>
+        </warning>
+       </entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 623b9539b15..49f0d0dab2d 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -639,6 +639,28 @@ LANGUAGE INTERNAL
 CALLED ON NULL INPUT VOLATILE PARALLEL SAFE
 AS 'pg_stat_reset_slru';
 
+CREATE OR REPLACE FUNCTION
+  pg_set_attribute_stats(relation oid,
+                         attname name,
+                         inherited bool,
+                         null_frac real,
+                         avg_width integer,
+                         n_distinct real,
+                         most_common_vals text DEFAULT NULL,
+                         most_common_freqs real[] DEFAULT NULL,
+                         histogram_bounds text DEFAULT NULL,
+                         correlation real DEFAULT NULL,
+                         most_common_elems text DEFAULT NULL,
+                         most_common_elem_freqs real[] DEFAULT NULL,
+                         elem_count_histogram real[] DEFAULT NULL,
+                         range_length_histogram text DEFAULT NULL,
+                         range_empty_frac real DEFAULT NULL,
+                         range_bounds_histogram text DEFAULT NULL)
+RETURNS void
+LANGUAGE INTERNAL
+CALLED ON NULL INPUT VOLATILE
+AS 'pg_set_attribute_stats';
+
 --
 -- The default permissions for functions mean that anyone can execute them.
 -- A number of functions shouldn't be executable by just anyone, but rather
diff --git a/src/backend/statistics/import_stats.c b/src/backend/statistics/import_stats.c
index 8c0a2bab305..3a09b880a38 100644
--- a/src/backend/statistics/import_stats.c
+++ b/src/backend/statistics/import_stats.c
@@ -20,18 +20,61 @@
 
 #include "access/heapam.h"
 #include "catalog/indexing.h"
+#include "catalog/pg_collation.h"
 #include "catalog/pg_database.h"
+#include "catalog/pg_operator.h"
 #include "miscadmin.h"
+#include "nodes/nodeFuncs.h"
+#include "statistics/statistics.h"
 #include "utils/acl.h"
+#include "utils/array.h"
 #include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
 #include "utils/syscache.h"
 
+static void update_pg_statistic(Datum values[], bool nulls[]);
+static Datum cast_stavalues(FmgrInfo *flinfo, Datum d, Oid typid,
+							int32 typmod, int elevel, bool *ok);
+static bool array_check(Datum datum, int one_dim, const char *statname,
+						int elevel);
+static bool type_is_scalar(Oid typid);
+static TypeCacheEntry *get_attr_stat_type(Relation rel, AttrNumber attnum,
+										  int elevel, int32 *typmod, Oid *typcoll);
+static void use_stats_slot(Datum *values, bool *nulls, int slotidx,
+						   int16 stakind, Oid staop, Oid stacoll,
+						   Datum stanumbers, bool stanumbers_isnull,
+						   Datum stavalues, bool stavalues_isnull);
+
+/*
+ * Names of parameters found in the functions pg_set_relation_stats and
+ * pg_set_attribute_stats
+ */
+const char *relation_name = "relation";
+const char *relpages_name = "relpages";
+const char *reltuples_name = "reltuples";
+const char *relallvisible_name = "relallvisible";
+const char *attname_name = "attname";
+const char *inherited_name = "inherited";
+const char *null_frac_name = "null_frac";
+const char *avg_width_name = "avg_width";
+const char *n_distinct_name = "n_distinct";
+const char *mc_vals_name = "most_common_vals";
+const char *mc_freqs_name = "most_common_freqs";
+const char *histogram_bounds_name = "histogram_bounds";
+const char *correlation_name = "correlation";
+const char *mc_elems_name = "most_common_elems";
+const char *mc_elem_freqs_name = "most_common_elem_freqs";
+const char *elem_count_hist_name = "elem_count_histogram";
+const char *range_length_hist_name = "range_length_histogram";
+const char *range_empty_frac_name = "range_empty_frac";
+const char *range_bounds_hist_name = "range_bounds_histogram";
+
 /*
  * A role has privileges to set statistics on the relation if any of the
  * following are true:
  *   - the role owns the current database and the relation is not shared
  *   - the role has the MAINTAIN privilege on the relation
- *
  */
 static bool
 check_can_update_stats(Relation rel)
@@ -158,6 +201,760 @@ relation_statistics_update(Oid reloid, int version, int32 relpages,
 	PG_RETURN_BOOL(true);
 }
 
+/*
+ * Insert or Update Attribute Statistics
+ *
+ * See pg_statistic.h for an explanation of how each statistic kind is
+ * stored. Custom statistics kinds are not supported.
+ *
+ * Depending on the statistics kind, we need to derive information from the
+ * attribute for which we're storing the stats. For instance, the MCVs are
+ * stored as an anyarray, and the representation of the array needs to store
+ * the correct element type, which must be derived from the attribute.
+ */
+static bool
+attribute_statistics_update(Oid reloid, AttrNumber attnum, int version,
+							int elevel, bool inherited, float null_frac,
+							int avg_width, float n_distinct,
+							Datum mc_vals_datum, bool mc_vals_isnull,
+							Datum mc_freqs_datum, bool mc_freqs_isnull, 
+							Datum histogram_bounds_datum, bool histogram_bounds_isnull,
+							Datum correlation_datum, bool correlation_isnull,
+							Datum mc_elems_datum, bool mc_elems_isnull,
+							Datum mc_elem_freqs_datum, bool mc_elem_freqs_isnull,
+							Datum elem_count_hist_datum, bool elem_count_hist_isnull,
+							Datum range_length_hist_datum, bool range_length_hist_isnull,
+							Datum range_empty_frac_datum, bool range_empty_frac_isnull,
+							Datum range_bounds_hist_datum, bool range_bounds_hist_isnull)
+{
+	Relation	rel;
+
+	TypeCacheEntry *typcache;
+	TypeCacheEntry *elemtypcache = NULL;
+
+	int32		typmod;
+	Oid			typcoll;
+
+	FmgrInfo	finfo;
+
+	Datum		values[Natts_pg_statistic];
+	bool		nulls[Natts_pg_statistic];
+
+	/*
+	 * The statkind index, we have only STATISTIC_NUM_SLOTS to hold these stats
+	 */
+	int			stakindidx = 0;
+	char	   *attname = get_attname(reloid, attnum, false);
+
+	/*
+	 * Initialize nulls array to be false for all non-NULL attributes, and
+	 * true for all nullable attributes.
+	 */
+	for (int i = 0; i < Natts_pg_statistic; i++)
+	{
+		values[i] = (Datum) 0;
+		if (i < Anum_pg_statistic_stanumbers1 - 1)
+			nulls[i] = false;
+		else
+			nulls[i] = true;
+	}
+
+	/*
+	 * Some parameters are linked, should both be NULL or NOT NULL.
+	 * Disagreement means that the statistic pair will fail so the 
+	 * NOT NULL one must be abandoned (set NULL) after an
+	 * ERROR/WARNING. By ensuring that the values are aligned it is 
+	 * possible to use one as a proxy for the other later.
+	 */
+
+	/*
+	 * STATISTIC_KIND_MCV = mc_vals + mc_freqs
+	 */
+	if (mc_vals_isnull != mc_freqs_isnull)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL when %s is NOT NULL",
+						mc_vals_isnull ? mc_vals_name : mc_freqs_name,
+						!mc_vals_isnull ? mc_vals_name : mc_freqs_name)));
+
+		mc_vals_isnull = true;
+		mc_freqs_isnull = true;
+	}
+
+	/*
+	 * STATISTIC_KIND_MCELEM = mc_elems + mc_elem_freqs
+	 */
+	if (mc_elems_isnull != mc_elem_freqs_isnull)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL when %s is NOT NULL",
+						mc_elems_isnull ? mc_elems_name : mc_elem_freqs_name,
+						!mc_elems_isnull ? mc_elems_name : mc_elem_freqs_name)));
+		mc_elems_isnull = true;
+		mc_elem_freqs_isnull = true;
+	}
+
+	/*
+	 * STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM =
+	 * range_length_histogram + range_empty_frac
+	 */
+	else if (range_length_hist_isnull != range_empty_frac_isnull)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL when %s is NOT NULL",
+						range_length_hist_isnull ?
+						range_length_hist_name : range_empty_frac_name,
+						!range_length_hist_isnull ?
+						range_length_hist_name : range_empty_frac_name)));
+		range_length_hist_isnull = true;
+		range_empty_frac_isnull = true;
+	}
+
+	rel = try_relation_open(reloid, ShareUpdateExclusiveLock);
+
+	if (rel == NULL)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Parameter relation OID %u is invalid", reloid)));
+		return false;
+	}
+
+	/*
+	 * Many of the values that are set for a particular stakind are entirely
+	 * derived from the attribute itself, or it's expression.
+	 */
+	typcache = get_attr_stat_type(rel, attnum, elevel, &typmod, &typcoll);
+	if (typcache == NULL)
+	{
+		relation_close(rel, NoLock);
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("unexpected typecache error")));
+		return false;
+	}
+
+	/*
+	 * Derive element type if we have stat kinds that need it.
+	 *
+	 * This duplicates some type-specific logic found in various typanalyze
+	 * functions which are called from vacuum's examine_attribute(), but using
+	 * that directly has proven awkward.
+	 */
+	if (!mc_elems_isnull || !elem_count_hist_isnull)
+	{
+		Oid			elemtypid;
+
+		if (typcache->type_id == TSVECTOROID)
+		{
+			/*
+			 * tsvectors always have a text oid base type and default
+			 * collation
+			 */
+			elemtypid = TEXTOID;
+			typcoll = DEFAULT_COLLATION_OID;
+		}
+		else if (typcache->typtype == TYPTYPE_RANGE)
+			elemtypid = get_range_subtype(typcache->type_id);
+		else
+			elemtypid = get_base_element_type(typcache->type_id);
+
+		/* not finding a basetype means we already had it */
+		if (elemtypid == InvalidOid)
+			elemtypid = typcache->type_id;
+
+		/* The stats need the eq_opr, any validation would need the lt_opr */
+		elemtypcache = lookup_type_cache(elemtypid, TYPECACHE_EQ_OPR);
+		if (elemtypcache == NULL)
+		{
+			/* warn and ignore any stats that can't be fulfilled */
+			if (!mc_elems_isnull)
+			{
+				ereport(elevel,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s cannot accept %s stats, ignored",
+								attname,
+								mc_elems_name)));
+				mc_elems_isnull = true;
+				mc_elem_freqs_isnull = true;
+			}
+
+			if (!elem_count_hist_isnull)
+			{
+				ereport(elevel,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s cannot accept %s stats, ignored",
+								attname,
+								elem_count_hist_name)));
+				elem_count_hist_isnull = true;
+			}
+		}
+	}
+
+	/*
+	 * histogram_bounds and correlation must have a type < operator
+	 */
+	if (typcache->lt_opr == InvalidOid)
+	{
+		if (!histogram_bounds_isnull)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Relation %s attname %s cannot "
+							"have stats of type %s, ignored.",
+							RelationGetRelationName(rel),
+							attname,
+							histogram_bounds_name)));
+			histogram_bounds_isnull = true;
+		}
+
+		if (!correlation_isnull)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Relation %s attname %s cannot "
+							"have stats of type %s, ignored.",
+							RelationGetRelationName(rel),
+							attname,
+							correlation_name)));
+			correlation_isnull = true;
+		}
+	}
+
+	/*
+	 * Scalar types can't have most_common_elems, most_common_elem_freqs, or
+	 * element_count_histogram
+	 */
+	if (type_is_scalar(typcache->type_id))
+	{
+		/* warn and ignore any stats that can't be fulfilled */
+		if (!mc_elems_isnull)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Relation %s attname %s is a scalar type, "
+							"cannot have stats of type %s, ignored",
+							RelationGetRelationName(rel),
+							attname,
+							mc_elems_name)));
+			mc_elems_isnull = true;
+			mc_elem_freqs_isnull = true;
+		}
+
+		if (!elem_count_hist_isnull)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Relation %s attname %s is a scalar type, "
+							"cannot have stats of type %s, ignored",
+							RelationGetRelationName(rel),
+							attname,
+							elem_count_hist_name)));
+			elem_count_hist_isnull = true;
+		}
+	}
+
+	/* Only range types can have range stats */
+	if ((typcache->typtype != TYPTYPE_RANGE) &&
+		(typcache->typtype != TYPTYPE_MULTIRANGE))
+	{
+		/* warn and ignore any stats that can't be fulfilled */
+		if (!range_length_hist_isnull)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Relation %s attname %s is not a range type, "
+							"cannot have stats of type %s",
+							RelationGetRelationName(rel),
+							attname,
+							range_length_hist_name)));
+			range_length_hist_isnull = true;
+			range_empty_frac_isnull = true;
+		}
+
+		if (!range_bounds_hist_isnull)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Relation %s attname %s is not a range type, "
+							"cannot have stats of type %s",
+							RelationGetRelationName(rel),
+							attname,
+							range_bounds_hist_name)));
+			range_bounds_hist_isnull = true;
+		}
+	}
+
+	if (!check_can_update_stats(rel))
+	{
+		relation_close(rel, NoLock);
+		ereport(elevel,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						RelationGetRelationName(rel))));
+		return false;
+	}
+
+	/* Populate pg_statistic tuple */
+	values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(reloid);
+	values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(attnum);
+	values[Anum_pg_statistic_stainherit - 1] = BoolGetDatum(inherited);
+	values[Anum_pg_statistic_stanullfrac - 1] = Float4GetDatum(null_frac);
+	values[Anum_pg_statistic_stawidth - 1] = Int32GetDatum(avg_width);
+	values[Anum_pg_statistic_stadistinct - 1] = Float4GetDatum(n_distinct);
+
+	fmgr_info(F_ARRAY_IN, &finfo);
+
+	/*
+	 * STATISTIC_KIND_MCV
+	 *
+	 * most_common_vals: ANYARRAY::text most_common_freqs: real[]
+	 */
+	if (!mc_vals_isnull)
+	{
+		bool		converted = false;
+		Datum		stanumbers = mc_freqs_datum;
+		Datum		stavalues = cast_stavalues(&finfo, mc_vals_datum,
+											   typcache->type_id, typmod,
+											   elevel, &converted);
+
+		if (converted &&
+			array_check(stavalues, false, mc_vals_name, elevel) &&
+			array_check(stanumbers, true, mc_freqs_name, elevel))
+		{
+			use_stats_slot(values, nulls, stakindidx++,
+						   STATISTIC_KIND_MCV,
+						   typcache->eq_opr, typcoll,
+						   stanumbers, false, stavalues, false);
+		}
+		else
+		{
+			/* Mark as skipped */
+			mc_vals_isnull = true;
+			mc_freqs_isnull = true;
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_HISTOGRAM
+	 *
+	 * histogram_bounds: ANYARRAY::text
+	 */
+	if (!histogram_bounds_isnull)
+	{
+		Datum		stavalues;
+		bool		converted = false;
+
+		stavalues = cast_stavalues(&finfo, histogram_bounds_datum,
+								   typcache->type_id, typmod, elevel,
+								   &converted);
+
+		if (converted &&
+			array_check(stavalues, false, histogram_bounds_name, elevel))
+		{
+			use_stats_slot(values, nulls, stakindidx++,
+						   STATISTIC_KIND_HISTOGRAM,
+						   typcache->lt_opr, typcoll,
+						   0, true, stavalues, false);
+		}
+		else
+			histogram_bounds_isnull = true;
+	}
+
+	/*
+	 * STATISTIC_KIND_CORRELATION
+	 *
+	 * correlation: real
+	 */
+	if (!correlation_isnull)
+	{
+		Datum		elems[] = {correlation_datum};
+		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+		Datum		stanumbers = PointerGetDatum(arry);
+
+		use_stats_slot(values, nulls, stakindidx++,
+					   STATISTIC_KIND_CORRELATION,
+					   typcache->lt_opr, typcoll,
+					   stanumbers, false, 0, true);
+	}
+
+	/*
+	 * STATISTIC_KIND_MCELEM
+	 *
+	 * most_common_elem_freqs: real[]
+	 *
+	 * most_common_elems     : ANYARRAY::text
+	 */
+	if (!mc_elems_isnull)
+	{
+		Datum		stanumbers = mc_elem_freqs_datum;
+		bool		converted = false;
+		Datum		stavalues;
+
+		stavalues = cast_stavalues(&finfo, mc_elems_datum,
+								   elemtypcache->type_id, typmod,
+								   elevel, &converted);
+
+		if (converted &&
+			array_check(stavalues, false, mc_elems_name, elevel) &&
+			array_check(stanumbers, true, mc_elem_freqs_name, elevel))
+		{
+			use_stats_slot(values, nulls, stakindidx++,
+						   STATISTIC_KIND_MCELEM,
+						   elemtypcache->eq_opr, typcoll,
+						   stanumbers, false, stavalues, false);
+		}
+		else
+		{
+			/* the mc_elem stat did not write */
+			mc_elems_isnull = true;
+			mc_elem_freqs_isnull = true;
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_DECHIST
+	 *
+	 * elem_count_histogram:	real[]
+	 */
+	if (!elem_count_hist_isnull)
+	{
+		Datum		stanumbers = elem_count_hist_datum;
+
+		use_stats_slot(values, nulls, stakindidx++,
+					   STATISTIC_KIND_DECHIST,
+					   elemtypcache->eq_opr, typcoll,
+					   stanumbers, false, 0, true);
+	}
+
+	/*
+	 * STATISTIC_KIND_BOUNDS_HISTOGRAM
+	 *
+	 * range_bounds_histogram: ANYARRAY::text
+	 *
+	 * This stakind appears before STATISTIC_KIND_BOUNDS_HISTOGRAM even though
+	 * it is numerically greater, and all other stakinds appear in numerical
+	 * order. We duplicate this quirk to make before/after tests of
+	 * pg_statistic records easier.
+	 */
+	if (!range_bounds_hist_isnull)
+	{
+		bool		converted = false;
+		Datum		stavalues;
+
+		stavalues = cast_stavalues(&finfo, range_bounds_hist_datum,
+								   typcache->type_id, typmod,
+								   elevel, &converted);
+
+		if (converted &&
+			array_check(stavalues, false, "range_bounds_histogram", elevel))
+		{
+			use_stats_slot(values, nulls, stakindidx++,
+						   STATISTIC_KIND_BOUNDS_HISTOGRAM,
+						   InvalidOid, InvalidOid,
+						   0, true, stavalues, false);
+		}
+		else
+			range_bounds_hist_isnull = true;
+	}
+
+	/*
+	 * STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM
+	 *
+	 * range_empty_frac: real
+	 *
+	 * range_length_histogram:  double precision[]::text
+	 */
+	if (!range_length_hist_isnull)
+	{
+		/* The anyarray is always a float8[] for this stakind */
+		Datum		elems[] = {range_empty_frac_datum};
+		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+		Datum		stanumbers = PointerGetDatum(arry);
+
+		bool		converted = false;
+		Datum		stavalues;
+
+		stavalues = cast_stavalues(&finfo, range_length_hist_datum, FLOAT8OID,
+								   0, elevel, &converted);
+
+		if (converted &&
+			array_check(stavalues, false, range_length_hist_name, elevel))
+		{
+			use_stats_slot(values, nulls, stakindidx++,
+						   STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM,
+						   Float8LessOperator, InvalidOid,
+						   stanumbers, false, stavalues, false);
+		}
+		else
+		{
+			range_empty_frac_isnull = true;
+			range_length_hist_isnull = true;
+		}
+	}
+
+	update_pg_statistic(values, nulls);
+
+	relation_close(rel, NoLock);
+
+	return true;
+}
+
+/*
+ * Test if the type is a scalar for MCELEM purposes
+ */
+static bool
+type_is_scalar(Oid typid)
+{
+	HeapTuple	tp;
+	bool		result = false;
+
+	tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+	if (HeapTupleIsValid(tp))
+	{
+		Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+
+		result = (!OidIsValid(typtup->typanalyze));
+		ReleaseSysCache(tp);
+	}
+	return result;
+}
+
+/*
+ * If this relation is an index and that index has expressions in it, and
+ * the attnum specified is known to be an expression, then we must walk
+ * the list attributes up to the specified attnum to get the right
+ * expression.
+ */
+static Node *
+get_attr_expr(Relation rel, int attnum)
+{
+	if ((rel->rd_rel->relkind == RELKIND_INDEX
+		 || (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+		&& (rel->rd_indexprs != NIL)
+		&& (rel->rd_index->indkey.values[attnum - 1] == 0))
+	{
+		ListCell   *indexpr_item = list_head(rel->rd_indexprs);
+
+		for (int i = 0; i < attnum - 1; i++)
+			if (rel->rd_index->indkey.values[i] == 0)
+				indexpr_item = lnext(rel->rd_indexprs, indexpr_item);
+
+		if (indexpr_item == NULL)	/* shouldn't happen */
+			elog(ERROR, "too few entries in indexprs list");
+
+		return (Node *) lfirst(indexpr_item);
+	}
+	return NULL;
+}
+
+/*
+ * Fetch datatype information, this is needed to derive the proper staopN
+ * and stacollN values.
+ *
+ */
+static TypeCacheEntry *
+get_attr_stat_type(Relation rel, AttrNumber attnum, int elevel,
+				   int32 *typmod, Oid *typcoll)
+{
+	Oid			relid = RelationGetRelid(rel);
+	Form_pg_attribute attr;
+	HeapTuple	atup;
+	Oid			typid;
+	Node	   *expr;
+
+	atup = SearchSysCache2(ATTNUM, ObjectIdGetDatum(relid),
+						   Int16GetDatum(attnum));
+
+	/* Attribute not found */
+	if (!HeapTupleIsValid(atup))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s has no attribute %d",
+						RelationGetRelationName(rel), attnum)));
+		return NULL;
+	}
+
+	attr = (Form_pg_attribute) GETSTRUCT(atup);
+	if (attr->attisdropped)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s attribute %d is dropped",
+						RelationGetRelationName(rel),
+						attnum)));
+		return NULL;
+	}
+
+	expr = get_attr_expr(rel, attr->attnum);
+
+	if (expr == NULL)
+	{
+		/* regular attribute */
+		typid = attr->atttypid;
+		*typmod = attr->atttypmod;
+		*typcoll = attr->attcollation;
+	}
+	else
+	{
+		typid = exprType(expr);
+		*typmod = exprTypmod(expr);
+
+		/*
+		 * If a collation has been specified for the index column, use that in
+		 * preference to anything else; but if not, fall back to whatever we
+		 * can get from the expression.
+		 */
+		if (OidIsValid(attr->attcollation))
+			*typcoll = attr->attcollation;
+		else
+			*typcoll = exprCollation(expr);
+	}
+	ReleaseSysCache(atup);
+
+	/* if it's a multirange, step down to the range type */
+	if (type_is_multirange(typid))
+		typid = get_multirange_range(typid);
+
+	return lookup_type_cache(typid,
+							 TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR);
+}
+
+/*
+ * Perform the cast of a known TextDatum into the type specified.
+ *
+ * If no errors are found, ok is set to true.
+ *
+ * Otherwise, set ok to false, capture the error found, and re-throw at the
+ * level specified by elevel.
+ */
+static Datum
+cast_stavalues(FmgrInfo *flinfo, Datum d, Oid typid, int32 typmod, 
+			   int elevel, bool *ok)
+{
+	LOCAL_FCINFO(fcinfo, 8);
+	char	   *s;
+	Datum		result;
+	ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+	escontext.details_wanted = true;
+
+	s = TextDatumGetCString(d);
+
+	InitFunctionCallInfoData(*fcinfo, flinfo, 3, InvalidOid,
+							 (Node *) &escontext, NULL);
+
+	fcinfo->args[0].value = CStringGetDatum(s);
+	fcinfo->args[0].isnull = false;
+	fcinfo->args[1].value = ObjectIdGetDatum(typid);
+	fcinfo->args[1].isnull = false;
+	fcinfo->args[2].value = Int32GetDatum(typmod);
+	fcinfo->args[2].isnull = false;
+
+	result = FunctionCallInvoke(fcinfo);
+
+	if (SOFT_ERROR_OCCURRED(&escontext))
+	{
+		if (elevel != ERROR)
+			escontext.error_data->elevel = elevel;
+		ThrowErrorData(escontext.error_data);
+		*ok = false;
+	}
+	else
+		*ok = true;
+
+	pfree(s);
+
+	return result;
+}
+
+
+/*
+ * Check array for any NULLs, and optionally for one-dimensionality.
+ *
+ * Report any failures at the level of elevel.
+ */
+static bool
+array_check(Datum datum, int one_dim, const char *statname, int elevel)
+{
+	ArrayType  *arr = DatumGetArrayTypeP(datum);
+	int16		elmlen;
+	char		elmalign;
+	bool		elembyval;
+	Datum	   *values;
+	bool	   *nulls;
+	int			nelems;
+
+	if (one_dim && (arr->ndim != 1))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be a multidimensional array", statname)));
+		return false;
+	}
+
+	get_typlenbyvalalign(ARR_ELEMTYPE(arr), &elmlen, &elembyval, &elmalign);
+
+	deconstruct_array(arr, ARR_ELEMTYPE(arr), elmlen, elembyval, elmalign,
+					  &values, &nulls, &nelems);
+
+	for (int i = 0; i < nelems; i++)
+		if (nulls[i])
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s array cannot contain NULL values", statname)));
+			return false;
+		}
+	return true;
+}
+
+/*
+ * Update the pg_statistic record.
+ */
+static void
+update_pg_statistic(Datum values[], bool nulls[])
+{
+	Relation	sd = table_open(StatisticRelationId, RowExclusiveLock);
+	CatalogIndexState indstate = CatalogOpenIndexes(sd);
+	HeapTuple	oldtup;
+	HeapTuple	stup;
+
+	/* Is there already a pg_statistic tuple for this attribute? */
+	oldtup = SearchSysCache3(STATRELATTINH,
+							 values[Anum_pg_statistic_starelid - 1],
+							 values[Anum_pg_statistic_staattnum - 1],
+							 values[Anum_pg_statistic_stainherit - 1]);
+
+	if (HeapTupleIsValid(oldtup))
+	{
+		/* Yes, replace it */
+		bool		replaces[Natts_pg_statistic];
+
+		for (int i = 0; i < Natts_pg_statistic; i++)
+			replaces[i] = true;
+
+		stup = heap_modify_tuple(oldtup, RelationGetDescr(sd),
+								 values, nulls, replaces);
+		ReleaseSysCache(oldtup);
+		CatalogTupleUpdateWithInfo(sd, &stup->t_self, stup, indstate);
+	}
+	else
+	{
+		/* No, insert new tuple */
+		stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
+		CatalogTupleInsertWithInfo(sd, stup, indstate);
+	}
+
+	heap_freetuple(stup);
+	CatalogCloseIndexes(indstate);
+	table_close(sd, NoLock);
+}
+
 /*
  * Set statistics for a given pg_class entry.
  *
@@ -210,3 +1007,161 @@ pg_set_relation_stats(PG_FUNCTION_ARGS)
 
 	PG_RETURN_VOID();
 }
+
+static
+void use_stats_slot(Datum *values, bool *nulls, int slotidx,
+					int16 stakind, Oid staop, Oid stacoll,
+					Datum stanumbers, bool stanumbers_isnull,
+					Datum stavalues, bool stavalues_isnull)
+{
+	if (slotidx >= STATISTIC_NUM_SLOTS)
+		ereport(ERROR,
+				(errmsg("maximum number of statistics slots exceeded: %d", slotidx + 1)));
+
+	/* slot should not be taken */
+	Assert(values[Anum_pg_statistic_stakind1 - 1 + slotidx] == (Datum) 0);
+	Assert(values[Anum_pg_statistic_staop1 - 1 + slotidx] == (Datum) 0);
+	Assert(values[Anum_pg_statistic_stacoll1 - 1 + slotidx] == (Datum) 0);
+	Assert(values[Anum_pg_statistic_stanumbers1 - 1 + slotidx] == (Datum) 0);
+	Assert(values[Anum_pg_statistic_stavalues1 - 1 + slotidx] == (Datum) 0);
+
+	/* nulls should be false for non-NULL attributes, true for nullable */
+	Assert(!nulls[Anum_pg_statistic_stakind1 - 1 + slotidx]);
+	Assert(!nulls[Anum_pg_statistic_staop1 - 1 + slotidx]);
+	Assert(!nulls[Anum_pg_statistic_stacoll1 - 1 + slotidx]);
+	Assert(nulls[Anum_pg_statistic_stanumbers1 - 1 + slotidx]);
+	Assert(nulls[Anum_pg_statistic_stavalues1 - 1 + slotidx]);
+
+	values[Anum_pg_statistic_stakind1 - 1 + slotidx] = stakind;
+	values[Anum_pg_statistic_staop1 - 1 + slotidx] = staop;
+	values[Anum_pg_statistic_stacoll1 - 1 + slotidx] = stacoll;
+
+	if (!stanumbers_isnull)
+	{
+		values[Anum_pg_statistic_stanumbers1 - 1 + slotidx] = stanumbers;
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + slotidx] = false;
+	}
+	if (!stavalues_isnull)
+	{
+		values[Anum_pg_statistic_stavalues1 - 1 + slotidx] = stavalues;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + slotidx] = false;
+	}
+}
+
+/*
+ * Import statistics for a given relation attribute.
+ *
+ * This will insert/replace a row in pg_statistic for the given relation and
+ * attribute name.
+ *
+ * The function takes input parameters that correspond to columns in the view
+ * pg_stats.
+ *
+ * Of those, the columns attname, inherited, null_frac, avg_width, and
+ * n_distinct all correspond to NOT NULL columns in pg_statistic. These
+ * parameters have no default value and passing NULL to them will result
+ * in an error.
+ *
+ * If there is no attribute with a matching attname in the relation, the
+ * function will raise an error. Likewise for setting inherited statistics
+ * on a table that is not partitioned.
+ *
+ * The remaining parameters all belong to a specific stakind. Some stakinds
+ * have multiple parameters, and in those cases both parameters must be
+ * NOT NULL or both NULL, otherwise an error will be raised.
+ *
+ * Omitting a parameter or explicitly passing NULL means that that particular
+ * stakind is not associated with the attribute.
+ *
+ * Parameters that are NOT NULL will be inspected for consistency checks,
+ * any of which can raise an error.
+ *
+ * Parameters corresponding to ANYARRAY columns are instead passed in as text
+ * values, which is a valid input string for an array of the type or element
+ * type of the attribute. Any error generated by the array_in() function will
+ * in turn fail the function.
+ */
+Datum
+pg_set_attribute_stats(PG_FUNCTION_ARGS)
+{
+	Oid			reloid;
+	Name		attname;
+	AttrNumber	attnum;
+	int			version	= PG_VERSION_NUM;
+	int			elevel	= ERROR;
+	bool		inherited;
+	float		null_frac;
+	int			avg_width;
+	float		n_distinct;
+
+	if (PG_ARGISNULL(0))
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relation cannot be NULL")));
+		return false;
+	}
+	reloid = PG_GETARG_OID(0);
+
+	if (PG_ARGISNULL(1))
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("attname cannot be NULL")));
+		return false;
+	}
+	attname = PG_GETARG_NAME(1);
+	attnum = get_attnum(reloid, NameStr(*attname));
+
+	if (PG_ARGISNULL(2))
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("inherited cannot be NULL")));
+		return false;
+	}
+	inherited = PG_GETARG_BOOL(2);
+
+	if (PG_ARGISNULL(3))
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("null_frac cannot be NULL")));
+		return false;
+	}
+	null_frac = PG_GETARG_FLOAT4(3);
+
+	if (PG_ARGISNULL(4))
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("avg_width cannot be NULL")));
+		return false;
+	}
+	avg_width = PG_GETARG_INT32(4);
+
+	if (PG_ARGISNULL(5))
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("n_distinct cannot be NULL")));
+		return false;
+	}
+	n_distinct = PG_GETARG_FLOAT4(5);
+
+	attribute_statistics_update(
+		reloid, attnum, version, elevel, inherited,
+		null_frac, avg_width, n_distinct,
+		PG_GETARG_DATUM(6), PG_ARGISNULL(6),
+		PG_GETARG_DATUM(7), PG_ARGISNULL(7),
+		PG_GETARG_DATUM(8), PG_ARGISNULL(8),
+		PG_GETARG_DATUM(9), PG_ARGISNULL(9),
+		PG_GETARG_DATUM(10), PG_ARGISNULL(10),
+		PG_GETARG_DATUM(11), PG_ARGISNULL(11),
+		PG_GETARG_DATUM(12), PG_ARGISNULL(12),
+		PG_GETARG_DATUM(13), PG_ARGISNULL(13),
+		PG_GETARG_DATUM(14), PG_ARGISNULL(14),
+		PG_GETARG_DATUM(15), PG_ARGISNULL(15));
+
+	PG_RETURN_VOID();
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index d700dd50f7b..fbd1a8a384d 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12263,5 +12263,12 @@
   proargtypes => 'oid int4 float4 int4',
   proargnames => '{relation,relpages,reltuples,relallvisible}',
   prosrc => 'pg_set_relation_stats' },
+{ oid => '8049',
+  descr => 'set statistics on attribute',
+  proname => 'pg_set_attribute_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'void',
+  proargtypes => 'oid name bool float4 int4 float4 text _float4 text float4 text _float4 _float4 text float4 text',
+  proargnames => '{relation,attname,inherited,null_frac,avg_width,n_distinct,most_common_vals,most_common_freqs,histogram_bounds,correlation,most_common_elems,most_common_elem_freqs,elem_count_histogram,range_length_histogram,range_empty_frac,range_bounds_histogram}',
+  prosrc => 'pg_set_attribute_stats' },
 
 ]
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 7f2bf18716d..73d3b541dda 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -127,4 +127,7 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												int nclauses);
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
+extern Datum pg_set_relation_stats(PG_FUNCTION_ARGS);
+extern Datum pg_set_attribute_stats(PG_FUNCTION_ARGS);
+
 #endif							/* STATISTICS_H */
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index f727cce9765..3a26b1f7d67 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -93,7 +93,650 @@ WHERE oid = 'stats_import.test'::regclass;
        18 |       401 |             5
 (1 row)
 
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => '0'::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  cache lookup failed for attribute 0 of relation 0
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => NULL::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  relation cannot be NULL
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => NULL::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  attname cannot be NULL
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => NULL::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  inherited cannot be NULL
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => NULL::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  null_frac cannot be NULL
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => NULL::integer,
+    n_distinct => 0.3::real);
+ERROR:  avg_width cannot be NULL
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => NULL::real);
+ERROR:  n_distinct cannot be NULL
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT stanullfrac, stawidth, stadistinct
+FROM pg_statistic
+WHERE starelid = 'stats_import.test'::regclass;
+ stanullfrac | stawidth | stadistinct 
+-------------+----------+-------------
+         0.1 |        2 |         0.3
+(1 row)
+
+-- warn: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_freqs => '{0.1,0.2,0.3}'::real[]
+    );
+ERROR:  most_common_vals cannot be NULL when most_common_freqs is NOT NULL
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{1,2,3}'::text
+    );
+ERROR:  most_common_freqs cannot be NULL when most_common_vals is NOT NULL
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2023-09-30,2024-10-31,3}'::text,
+    most_common_freqs => '{0.2,0.1}'::real[]
+    );
+ERROR:  invalid input syntax for type integer: "2023-09-30"
+-- warning: mcv cast failure
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,four,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+ERROR:  invalid input syntax for type integer: "four"
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: histogram elements null value
+-- this generates no warnings, but perhaps it should
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,NULL,3,4}'::text
+    );
+ERROR:  histogram_bounds array cannot contain NULL values
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,2,3,4}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 0.5::real);
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |         0.5 |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have mcelem
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{1,3}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+ERROR:  Relation test attname id is a scalar type, cannot have stats of type most_common_elems, ignored
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,two}'::text
+    );
+ERROR:  most_common_elem_freqs cannot be NULL when most_common_elems is NOT NULL
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
+    );
+ERROR:  most_common_elems cannot be NULL when most_common_elem_freqs is NOT NULL
+-- ok: mcelem
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             | {one,three}       | {0.3,0.2,0.2,0.3,0}    |                      |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ERROR:  Relation test attname id is a scalar type, cannot have stats of type elem_count_histogram, ignored
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
+ stats_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ERROR:  Relation test attname id is not a range type, cannot have stats of type range_length_histogram
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ERROR:  range_empty_frac cannot be NULL when range_length_histogram is NOT NULL
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real
+    );
+ERROR:  range_length_histogram cannot be NULL when range_empty_frac is NOT NULL
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
+(1 row)
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ERROR:  Relation test attname id is not a range type, cannot have stats of type range_bounds_histogram
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
+ stats_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      |                        |                  | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+(1 row)
+
+-- warn: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{"[2,3)","[1,2)","[3,4)"}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[],
+    histogram_bounds => '{"[1,2)","[2,3)","[3,4)","[4,5)"}'::text,
+    correlation => 1.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    range_empty_frac => -0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ERROR:  maximum number of statistics slots exceeded: 6
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
+-- Generate statistics on table with data
+ANALYZE stats_import.test;
+CREATE TABLE stats_import.test_clone ( LIKE stats_import.test );
+CREATE INDEX is_odd_clone ON stats_import.test_clone(((comp).a % 2 = 1));
+--
+-- Copy stats from test to test_clone, and is_odd to is_odd_clone
+--
+SELECT s.schemaname, s.tablename, s.attname, s.inherited
+FROM pg_catalog.pg_stats AS s
+CROSS JOIN LATERAL
+    pg_catalog.pg_set_attribute_stats(
+        relation => ('stats_import.' || s.tablename || '_clone')::regclass::oid,
+        attname => s.attname,
+        inherited => s.inherited,
+        null_frac => s.null_frac,
+        avg_width => s.avg_width,
+        n_distinct => s.n_distinct,
+        most_common_vals => s.most_common_vals::text,
+        most_common_freqs => s.most_common_freqs,
+        histogram_bounds => s.histogram_bounds::text,
+        correlation => s.correlation,
+        most_common_elems => s.most_common_elems::text,
+        most_common_elem_freqs => s.most_common_elem_freqs,
+        elem_count_histogram => s.elem_count_histogram,
+        range_bounds_histogram => s.range_bounds_histogram::text,
+        range_empty_frac => s.range_empty_frac,
+        range_length_histogram => s.range_length_histogram::text) AS r
+WHERE s.schemaname = 'stats_import'
+AND s.tablename IN ('test', 'is_odd')
+ORDER BY s.tablename, s.attname, s.inherited;
+  schemaname  | tablename | attname | inherited 
+--------------+-----------+---------+-----------
+ stats_import | is_odd    | expr    | f
+ stats_import | test      | arange  | f
+ stats_import | test      | comp    | f
+ stats_import | test      | id      | f
+ stats_import | test      | name    | f
+ stats_import | test      | tags    | f
+(6 rows)
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+   relname    | num_stats 
+--------------+-----------
+ is_odd       |         1
+ is_odd_clone |         1
+ test         |         5
+ test_clone   |         5
+(4 rows)
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
 DROP SCHEMA stats_import CASCADE;
-NOTICE:  drop cascades to 2 other objects
+NOTICE:  drop cascades to 3 other objects
 DETAIL:  drop cascades to type stats_import.complex_type
 drop cascades to table stats_import.test
+drop cascades to table stats_import.test_clone
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index effd5b892bf..4fa6ea41519 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -76,4 +76,548 @@ SELECT relpages, reltuples, relallvisible
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => '0'::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => NULL::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => NULL::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => NULL::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => NULL::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => NULL::integer,
+    n_distinct => 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => NULL::real);
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+SELECT stanullfrac, stawidth, stadistinct
+FROM pg_statistic
+WHERE starelid = 'stats_import.test'::regclass;
+
+-- warn: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_freqs => '{0.1,0.2,0.3}'::real[]
+    );
+
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{1,2,3}'::text
+    );
+
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2023-09-30,2024-10-31,3}'::text,
+    most_common_freqs => '{0.2,0.1}'::real[]
+    );
+
+-- warning: mcv cast failure
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,four,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: histogram elements null value
+-- this generates no warnings, but perhaps it should
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,NULL,3,4}'::text
+    );
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,2,3,4}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 0.5::real);
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: scalars can't have mcelem
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{1,3}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,two}'::text
+    );
+
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
+    );
+
+-- ok: mcelem
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- warn: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real
+    );
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{"[2,3)","[1,2)","[3,4)"}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[],
+    histogram_bounds => '{"[1,2)","[2,3)","[3,4)","[4,5)"}'::text,
+    correlation => 1.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    range_empty_frac => -0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+
+CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
+
+-- Generate statistics on table with data
+ANALYZE stats_import.test;
+
+CREATE TABLE stats_import.test_clone ( LIKE stats_import.test );
+
+CREATE INDEX is_odd_clone ON stats_import.test_clone(((comp).a % 2 = 1));
+
+--
+-- Copy stats from test to test_clone, and is_odd to is_odd_clone
+--
+SELECT s.schemaname, s.tablename, s.attname, s.inherited
+FROM pg_catalog.pg_stats AS s
+CROSS JOIN LATERAL
+    pg_catalog.pg_set_attribute_stats(
+        relation => ('stats_import.' || s.tablename || '_clone')::regclass::oid,
+        attname => s.attname,
+        inherited => s.inherited,
+        null_frac => s.null_frac,
+        avg_width => s.avg_width,
+        n_distinct => s.n_distinct,
+        most_common_vals => s.most_common_vals::text,
+        most_common_freqs => s.most_common_freqs,
+        histogram_bounds => s.histogram_bounds::text,
+        correlation => s.correlation,
+        most_common_elems => s.most_common_elems::text,
+        most_common_elem_freqs => s.most_common_elem_freqs,
+        elem_count_histogram => s.elem_count_histogram,
+        range_bounds_histogram => s.range_bounds_histogram::text,
+        range_empty_frac => s.range_empty_frac,
+        range_length_histogram => s.range_length_histogram::text) AS r
+WHERE s.schemaname = 'stats_import'
+AND s.tablename IN ('test', 'is_odd')
+ORDER BY s.tablename, s.attname, s.inherited;
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test_clone'::regclass;
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test'::regclass;
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd_clone'::regclass;
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd'::regclass;
+
 DROP SCHEMA stats_import CASCADE;
-- 
2.34.1

#184Jeff Davis
pgsql@j-davis.com
In reply to: Jeff Davis (#183)
2 attachment(s)
Re: Statistics Import and Export

On Thu, 2024-08-15 at 01:57 -0700, Jeff Davis wrote:

On Sun, 2024-08-04 at 01:09 -0400, Corey Huinker wrote:

I believe 0001 and 0002 are in good shape API-wise, and I can
start
getting those committed. I will try to clean up the code in the
process.

Attached v26j.

I did a lot of refactoring, and it's starting to take the shape I had
in mind. Some of it is surely just style preference, but I think it
reads more nicely and I caught a couple bugs along the way. The
function attribute_statsitics_update() is significantly shorter. (Thank
you for a good set of tests, by the way, which sped up the refactoring
process.)

Attached v27j.

Questions:

* Remind me why the new stats completely replace the new row, rather
than updating only the statistic kinds that are specified?
* I'm not sure what the type_is_scalar() function was doing before,
but I just removed it. If it can't find the element type, then it skips
over the kinds that require it.
* I introduced some hard errors. These happen when it can't find the
table, or the attribute, or doesn't have permissions. I don't see any
reason to demote those to a WARNING. Even for the restore case,
analagous errors happen for COPY, etc.
* I'm still sorting through some of the type info derivations. I
think we need better explanations about why it's doing exactly the
things it's doing, e.g. for tsvector and multiranges.

Regards,
Jeff Davis

Attachments:

v27j-0001-Create-function-pg_set_relation_stats.patchtext/x-patch; charset=UTF-8; name=v27j-0001-Create-function-pg_set_relation_stats.patchDownload
From 93bb8e2dfeab17d5291273e6343f61e40e501633 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 24 Jul 2024 23:45:26 -0400
Subject: [PATCH v27j 1/2] Create function pg_set_relation_stats.

This function is used to tweak statistics on any relation that the user
owns.

The first parameter, relation, is used to identify the the relation to be
modified.

The remaining parameters correspond to the statistics attributes in
pg_class: relpages, reltuples, and relallisvible.

This function allows the user to tweak pg_class statistics values,
allowing the user to inflate rowcounts, table size, and visibility to see
what effect those changes will have on the the query planner.

The function has no return value.

Author: Corey Huinker
Discussion: https://postgr.es/m/CADkLM=e0jM7m0GFV44nNtvL22CtnFUu+pekppCVE=DOBe58RTQ@mail.gmail.com
---
 doc/src/sgml/func.sgml                     |  65 +++++++
 src/backend/statistics/Makefile            |   1 +
 src/backend/statistics/import_stats.c      | 207 +++++++++++++++++++++
 src/backend/statistics/meson.build         |   1 +
 src/include/catalog/pg_proc.dat            |   9 +
 src/test/regress/expected/stats_import.out |  99 ++++++++++
 src/test/regress/parallel_schedule         |   2 +-
 src/test/regress/sql/stats_import.sql      |  79 ++++++++
 8 files changed, 462 insertions(+), 1 deletion(-)
 create mode 100644 src/backend/statistics/import_stats.c
 create mode 100644 src/test/regress/expected/stats_import.out
 create mode 100644 src/test/regress/sql/stats_import.sql

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 5dd95d73a1a..02d93f6c925 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29938,6 +29938,71 @@ DETAIL:  Make sure pg_wal_replay_wait() isn't called within a transaction with a
     in identifying the specific disk files associated with database objects.
    </para>
 
+   <table id="functions-admin-statsimport">
+    <title>Database Object Statistics Import Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry">
+        <para role="func_signature">
+         <indexterm>
+          <primary>pg_set_relation_stats</primary>
+         </indexterm>
+         <function>pg_set_relation_stats</function> ( <parameter>relation</parameter> <type>regclass</type>, <parameter>relpages</parameter> <type>integer</type>, <parameter>reltuples</parameter> <type>real</type>, <parameter>relallvisible</parameter> <type>integer</type> )
+         <returnvalue>void</returnvalue>
+        </para>
+        <para>
+         Updates table-level statistics for the given relation to the
+         specified values. The parameters correspond to columns in <link
+         linkend="catalog-pg-class"><structname>pg_class</structname></link>.
+        </para>
+        <para>
+         Ordinarily, these statistics are collected automatically or updated
+         as a part of <xref linkend="sql-vacuum"/> or <xref
+         linkend="sql-analyze"/>, so it's not necessary to call this
+         function. However, it may be useful when testing the effects of
+         statistics on the planner to understand or anticipate plan changes.
+        </para>
+        <para>
+         The caller must have the <literal>MAINTAIN</literal> privilege on
+         the table or be the owner of the database.
+        </para>
+        <para>
+         The value of <structfield>relpages</structfield> must be greater than
+         or equal to <literal>0</literal>,
+         <structfield>reltuples</structfield> must be greater than or equal to
+         <literal>-1.0</literal>, and <structfield>relallvisible</structfield>
+         must be greater than or equal to <literal>0</literal>.
+        </para>
+        <warning>
+         <para>
+          Changes made by this function are likely to be overwritten by <link
+          linkend="autovacuum">autovacuum</link> (or manual
+          <command>VACUUM</command> or <command>ANALYZE</command>) and should
+          be considered temporary.
+         </para>
+         <para>
+          The signature of this function may change in new major releases if
+          there are changes to the statistics being tracked.
+         </para>
+        </warning>
+       </entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
    <table id="functions-admin-dblocation">
     <title>Database Object Location Functions</title>
     <tgroup cols="1">
diff --git a/src/backend/statistics/Makefile b/src/backend/statistics/Makefile
index 89cf8c27973..5e776c02218 100644
--- a/src/backend/statistics/Makefile
+++ b/src/backend/statistics/Makefile
@@ -15,6 +15,7 @@ include $(top_builddir)/src/Makefile.global
 OBJS = \
 	dependencies.o \
 	extended_stats.o \
+	import_stats.o \
 	mcv.o \
 	mvdistinct.o
 
diff --git a/src/backend/statistics/import_stats.c b/src/backend/statistics/import_stats.c
new file mode 100644
index 00000000000..fede53f6634
--- /dev/null
+++ b/src/backend/statistics/import_stats.c
@@ -0,0 +1,207 @@
+/*-------------------------------------------------------------------------
+ * import_stats.c
+ *
+ *	  PostgreSQL statistics import
+ *
+ * Code supporting the direct importation of relation statistics, similar to
+ * what is done by the ANALYZE command.
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *       src/backend/statistics/import_stats.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_database.h"
+#include "miscadmin.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/syscache.h"
+
+static bool relation_statistics_update(Oid reloid, int version,
+									   int32 relpages, float4 reltuples,
+									   int32 relallvisible, int elevel);
+static void check_privileges(Relation rel);
+
+/*
+ * Internal function for modifying statistics for a relation.
+ */
+static bool
+relation_statistics_update(Oid reloid, int version, int32 relpages,
+						   float4 reltuples, int32 relallvisible, int elevel)
+{
+	Relation		relation;
+	Relation		crel;
+	HeapTuple		ctup;
+	Form_pg_class	pgcform;
+
+	if (relpages < -1)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relpages cannot be < -1")));
+		return false;
+	}
+
+	if (reltuples < -1.0)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("reltuples cannot be < -1.0")));
+		return false;
+	}
+
+	if (relallvisible < -1)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relallvisible cannot be < -1")));
+		return false;
+	}
+
+	/*
+	 * Open relation with ShareUpdateExclusiveLock, consistent with
+	 * ANALYZE. Only needed for permission check and then we close it (but
+	 * retain the lock).
+	 */
+	relation = table_open(reloid, ShareUpdateExclusiveLock);
+
+	check_privileges(relation);
+
+	table_close(relation, NoLock);
+
+	/*
+	 * Take RowExclusiveLock on pg_class, consistent with
+	 * vac_update_relstats().
+	 */
+	crel = table_open(RelationRelationId, RowExclusiveLock);
+
+	ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(reloid));
+	if (!HeapTupleIsValid(ctup))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_OBJECT_IN_USE),
+				 errmsg("pg_class entry for relid %u not found", reloid)));
+		return false;
+	}
+
+	pgcform = (Form_pg_class) GETSTRUCT(ctup);
+
+	/* only update pg_class if there is a meaningful change */
+	if (pgcform->relpages != relpages ||
+		pgcform->reltuples != reltuples ||
+		pgcform->relallvisible != relallvisible)
+	{
+		int			cols[3] = {
+			Anum_pg_class_relpages,
+			Anum_pg_class_reltuples,
+			Anum_pg_class_relallvisible,
+		};
+		Datum		values[3] = {
+			Int32GetDatum(relpages),
+			Float4GetDatum(reltuples),
+			Int32GetDatum(relallvisible),
+		};
+		bool		nulls[3] = {false, false, false};
+
+		TupleDesc	tupdesc = RelationGetDescr(crel);
+		HeapTuple	newtup;
+
+		CatalogIndexState indstate = CatalogOpenIndexes(crel);
+
+		newtup = heap_modify_tuple_by_cols(ctup, tupdesc, 3, cols, values,
+										   nulls);
+
+		CatalogTupleUpdateWithInfo(crel, &newtup->t_self, newtup, indstate);
+		heap_freetuple(newtup);
+		CatalogCloseIndexes(indstate);
+	}
+
+	/* release the lock, consistent with vac_update_relstats() */
+	table_close(crel, RowExclusiveLock);
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * A role has privileges to set statistics on the relation if any of the
+ * following are true:
+ *   - the role owns the current database and the relation is not shared
+ *   - the role has the MAINTAIN privilege on the relation
+ */
+static void
+check_privileges(Relation rel)
+{
+	AclResult               aclresult;
+
+	if (object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId()) &&
+		!rel->rd_rel->relisshared)
+		return;
+
+	aclresult = pg_class_aclcheck(RelationGetRelid(rel), GetUserId(),
+								  ACL_MAINTAIN);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult,
+					   get_relkind_objtype(rel->rd_rel->relkind),
+					   NameStr(rel->rd_rel->relname));
+}
+
+/*
+ * Set statistics for a given pg_class entry.
+ *
+ * Use a transactional update, and assume statistics come from the current
+ * server version.
+ *
+ * Not intended for bulk import of statistics from older versions.
+ */
+Datum
+pg_set_relation_stats(PG_FUNCTION_ARGS)
+{
+	Oid			reloid;
+	int			version = PG_VERSION_NUM;
+	int			elevel = ERROR;
+	int32		relpages;
+	float		reltuples;
+	int32		relallvisible;
+
+	if (PG_ARGISNULL(0))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relation cannot be NULL")));
+	else
+		reloid = PG_GETARG_OID(0);
+
+	if (PG_ARGISNULL(1))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relpages cannot be NULL")));
+	else
+		relpages = PG_GETARG_INT32(1);
+
+	if (PG_ARGISNULL(2))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("reltuples cannot be NULL")));
+	else
+		reltuples = PG_GETARG_FLOAT4(2);
+
+	if (PG_ARGISNULL(3))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relallvisible cannot be NULL")));
+	else
+		relallvisible = PG_GETARG_INT32(3);
+
+
+	relation_statistics_update(reloid, version, relpages, reltuples,
+							   relallvisible, elevel);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/statistics/meson.build b/src/backend/statistics/meson.build
index 73b29a3d50a..849df3bf323 100644
--- a/src/backend/statistics/meson.build
+++ b/src/backend/statistics/meson.build
@@ -3,6 +3,7 @@
 backend_sources += files(
   'dependencies.c',
   'extended_stats.c',
+  'import_stats.c',
   'mcv.c',
   'mvdistinct.c',
 )
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 4abc6d95262..d700dd50f7b 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12255,4 +12255,13 @@
   proargnames => '{summarized_tli,summarized_lsn,pending_lsn,summarizer_pid}',
   prosrc => 'pg_get_wal_summarizer_state' },
 
+# Statistics Import
+{ oid => '8048',
+  descr => 'set statistics on relation',
+  proname => 'pg_set_relation_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'void',
+  proargtypes => 'oid int4 float4 int4',
+  proargnames => '{relation,relpages,reltuples,relallvisible}',
+  prosrc => 'pg_set_relation_stats' },
+
 ]
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
new file mode 100644
index 00000000000..f727cce9765
--- /dev/null
+++ b/src/test/regress/expected/stats_import.out
@@ -0,0 +1,99 @@
+CREATE SCHEMA stats_import;
+CREATE TYPE stats_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+CREATE TABLE stats_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+-- starting stats
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+        0 |        -1 |             0
+(1 row)
+
+-- error: regclass not found
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 0::Oid,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+ERROR:  could not open relation with OID 0
+-- error: relpages NULL
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => NULL::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+ERROR:  relpages cannot be NULL
+-- error: reltuples NULL
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => NULL::real,
+        relallvisible => 4::integer);
+ERROR:  reltuples cannot be NULL
+-- error: relallvisible NULL
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => NULL::integer);
+ERROR:  relallvisible cannot be NULL
+-- named arguments
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+ pg_set_relation_stats 
+-----------------------
+ 
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       17 |       400 |             4
+(1 row)
+
+-- positional arguments
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        'stats_import.test'::regclass,
+        18::integer,
+        401.0::real,
+        5::integer);
+ pg_set_relation_stats 
+-----------------------
+ 
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       18 |       401 |             5
+(1 row)
+
+DROP SCHEMA stats_import CASCADE;
+NOTICE:  drop cascades to 2 other objects
+DETAIL:  drop cascades to type stats_import.complex_type
+drop cascades to table stats_import.test
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 2429ec2bbaa..85fc85bfa03 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,7 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import
 
 # ----------
 # Load huge amounts of data
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
new file mode 100644
index 00000000000..effd5b892bf
--- /dev/null
+++ b/src/test/regress/sql/stats_import.sql
@@ -0,0 +1,79 @@
+CREATE SCHEMA stats_import;
+
+CREATE TYPE stats_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+
+CREATE TABLE stats_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+
+-- starting stats
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+-- error: regclass not found
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 0::Oid,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+
+-- error: relpages NULL
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => NULL::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+
+-- error: reltuples NULL
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => NULL::real,
+        relallvisible => 4::integer);
+
+-- error: relallvisible NULL
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => NULL::integer);
+
+-- named arguments
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+-- positional arguments
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        'stats_import.test'::regclass,
+        18::integer,
+        401.0::real,
+        5::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+DROP SCHEMA stats_import CASCADE;
-- 
2.34.1

v27j-0002-Create-function-pg_set_attribute_stats.patchtext/x-patch; charset=UTF-8; name=v27j-0002-Create-function-pg_set_attribute_stats.patchDownload
From abd713bac2f25a6ab743fba1c6a486e21fb06cad Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Thu, 8 Aug 2024 17:56:22 -0700
Subject: [PATCH v27j 2/2] Create function pg_set_attribute_stats.

---
 doc/src/sgml/func.sgml                     |  60 ++
 src/backend/catalog/system_functions.sql   |  22 +
 src/backend/statistics/import_stats.c      | 827 +++++++++++++++++++++
 src/include/catalog/pg_proc.dat            |   7 +
 src/include/statistics/statistics.h        |   3 +
 src/test/regress/expected/stats_import.out | 645 +++++++++++++++-
 src/test/regress/sql/stats_import.sql      | 544 ++++++++++++++
 7 files changed, 2107 insertions(+), 1 deletion(-)

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

#185Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#184)
Re: Statistics Import and Export

function attribute_statsitics_update() is significantly shorter. (Thank
you for a good set of tests, by the way, which sped up the refactoring
process.)

yw

* Remind me why the new stats completely replace the new row, rather
than updating only the statistic kinds that are specified?

because:
- complexity
- we would then need a mechanism to then tell it to *delete* a stakind
- we'd have to figure out how to reorder the remaining stakinds, or spend
effort finding a matching stakind in the existing row to know to replace it
- "do what analyze does" was an initial goal and as a result many test
cases directly compared pg_statistic rows from an original table to an
empty clone table to see if the "copy" had fidelity.

* I'm not sure what the type_is_scalar() function was doing before,
but I just removed it. If it can't find the element type, then it skips
over the kinds that require it.

that may be sufficient,

* I introduced some hard errors. These happen when it can't find the
table, or the attribute, or doesn't have permissions. I don't see any
reason to demote those to a WARNING. Even for the restore case,
analagous errors happen for COPY, etc.

I can accept that reasoning.

* I'm still sorting through some of the type info derivations. I
think we need better explanations about why it's doing exactly the
things it's doing, e.g. for tsvector and multiranges.

I don't have the specifics of each, but any such cases were derived from
similar behaviors in the custom typanalyze functions, and the lack of a
custom typanalyze function for a given type was taken as evidence that the
type was adequately handled by the default rules. I can see that this is an
argument for having a second stats-specific custom typanalyze function for
datatypes that need them, but I wasn't ready to go that far myself.

#186Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#185)
1 attachment(s)
Re: Statistics Import and Export

On Thu, 2024-08-15 at 20:53 -0400, Corey Huinker wrote:

  * Remind me why the new stats completely replace the new row,
rather
than updating only the statistic kinds that are specified?

because:
- complexity

I don't think it significantly impacts the overall complexity. We have
a ShareUpdateExclusiveLock on the relation, so there's no concurrency
to deal with, and an upsert operation is not many more lines of code.

- we would then need a mechanism to then tell it to *delete* a
stakind

That sounds useful regardless. I have introduced pg_clear_*_stats()
functions.

- we'd have to figure out how to reorder the remaining stakinds, or
spend effort finding a matching stakind in the existing row to know
to replace it

Right. I initialized the values/nulls arrays based on the existing
tuple, if any, and created a set_stats_slot() function that searches
for either a matching stakind or the first empty slot.

- "do what analyze does" was an initial goal and as a result many
test cases directly compared pg_statistic rows from an original table
to an empty clone table to see if the "copy" had fidelity.

Can't we just clear the stats first to achieve the same effect?

I have attached version 28j as one giant patch covering what was
previously 0001-0003. It's a bit rough (tests in particular need some
work), but it implelements the logic to replace only those values
specified rather than the whole tuple.

At least for the interactive "set" variants of the functions, I think
it's an improvement. It feels more natural to just change one stat
without wiping out all the others. I realize a lot of the statistics
depend on each other, but the point is not to replace ANALYZE, the
point is to experiment with planner scenarios. What do others think?

For the "restore" variants, I'm not sure it matters a lot because the
stats will already be empty. If it does matter, we could pretty easily
define the "restore" variants to wipe out existing stats when loading
the table, though I'm not sure if that's a good thing or not.

I also made more use of FunctionCallInfo structures to communicate
between functions rather than huge parameter lists. I believe that
reduced the line count substantially, and made it easier to transform
the argument pairs in the "restore" variants into the positional
arguments for the "set" variants.

Regards,
Jeff Davis

Attachments:

v28j-0001-Create-functions-pg_set_relation_stats-pg_clear.patchtext/x-patch; charset=UTF-8; name=v28j-0001-Create-functions-pg_set_relation_stats-pg_clear.patchDownload
From 21599a31f5d266421a9b9470d68817e055437bcb Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 24 Jul 2024 23:45:26 -0400
Subject: [PATCH v28j] Create functions pg_set_relation_stats(),
 pg_clear_relation_stats(), pg_restore_relation_stats(),
 pg_set_attribute_stats(), pg_clear_attribute_stats(), and
 pg_restore_attribute_stats().

--- CATVERSION BUMP ---
---
 doc/src/sgml/func.sgml                     |  299 +++++
 src/backend/catalog/system_functions.sql   |   31 +
 src/backend/statistics/Makefile            |    1 +
 src/backend/statistics/import_stats.c      | 1263 ++++++++++++++++++++
 src/backend/statistics/meson.build         |    1 +
 src/include/catalog/pg_proc.dat            |   48 +
 src/include/statistics/statistics.h        |    3 +
 src/test/regress/expected/stats_import.out |  766 ++++++++++++
 src/test/regress/parallel_schedule         |    2 +-
 src/test/regress/sql/stats_import.sql      |  623 ++++++++++
 10 files changed, 3036 insertions(+), 1 deletion(-)
 create mode 100644 src/backend/statistics/import_stats.c
 create mode 100644 src/test/regress/expected/stats_import.out
 create mode 100644 src/test/regress/sql/stats_import.sql

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 461fc3f437c..9abf7927ab7 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29949,6 +29949,305 @@ DETAIL:  Make sure pg_wal_replay_wait() isn't called within a transaction with a
     in identifying the specific disk files associated with database objects.
    </para>
 
+   <table id="functions-admin-statsimport">
+    <title>Database Object Statistics Import Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry">
+        <para role="func_signature">
+         <indexterm>
+          <primary>pg_set_relation_stats</primary>
+         </indexterm>
+         <function>pg_set_relation_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>
+         <optional>, <parameter>relpages</parameter> <type>integer</type></optional>
+         <optional>, <parameter>reltuples</parameter> <type>real</type></optional>
+         <optional>, <parameter>relallvisible</parameter> <type>integer</type></optional> )
+         <returnvalue>boolean</returnvalue>
+        </para>
+        <para>
+         Updates table-level statistics for the given relation to the
+         specified values. The parameters correspond to columns in <link
+         linkend="catalog-pg-class"><structname>pg_class</structname></link>. Unspecified
+         or <literal>NULL</literal> values leave the setting
+         unchanged. Returns <literal>true</literal> if a change was made;
+         <literal>false</literal> otherwise.
+        </para>
+        <para>
+         Ordinarily, these statistics are collected automatically or updated
+         as a part of <xref linkend="sql-vacuum"/> or <xref
+         linkend="sql-analyze"/>, so it's not necessary to call this
+         function. However, it may be useful when testing the effects of
+         statistics on the planner to understand or anticipate plan changes.
+        </para>
+        <para>
+         The caller must have the <literal>MAINTAIN</literal> privilege on
+         the table or be the owner of the database.
+        </para>
+        <para>
+         The value of <structfield>relpages</structfield> must be greater than
+         or equal to <literal>0</literal>,
+         <structfield>reltuples</structfield> must be greater than or equal to
+         <literal>-1.0</literal>, and <structfield>relallvisible</structfield>
+         must be greater than or equal to <literal>0</literal>.
+        </para>
+        <warning>
+         <para>
+          Changes made by this function are likely to be overwritten by <link
+          linkend="autovacuum">autovacuum</link> (or manual
+          <command>VACUUM</command> or <command>ANALYZE</command>) and should
+          be considered temporary.
+         </para>
+         <para>
+          The signature of this function may change in new major releases if
+          there are changes to the statistics being tracked.
+         </para>
+        </warning>
+       </entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry">
+        <para role="func_signature">
+         <indexterm>
+          <primary>pg_clear_relation_stats</primary>
+         </indexterm>
+         <function>pg_clear_relation_stats</function> ( <parameter>relation</parameter> <type>regclass</type> )
+         <returnvalue>boolean</returnvalue>
+        </para>
+        <para>
+         Clears table-level statistics for the given relation, as though the
+         table was newly created. Returns <literal>true</literal> if a change
+         was made; <literal>false</literal> otherwise.
+        </para>
+        <para>
+         The caller must have the <literal>MAINTAIN</literal> privilege on
+         the table or be the owner of the database.
+        </para>
+        <warning>
+         <para>
+          Changes made by this function are likely to be overwritten by <link
+          linkend="autovacuum">autovacuum</link> (or manual
+          <command>VACUUM</command> or <command>ANALYZE</command>) and should
+          be considered temporary.
+         </para>
+         <para>
+          The signature of this function may change in new major releases if
+          there are changes to the statistics being tracked.
+         </para>
+        </warning>
+       </entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_restore_relation_stats</primary>
+        </indexterm>
+        <function>pg_restore_relation_stats</function> (
+        <literal>VARIADIC</literal> <parameter>kwargs</parameter> <type>"any"</type> )
+        <returnvalue>boolean</returnvalue>
+        </para>
+        <para>
+         Similar to <function>pg_set_relation_stats()</function>, but intended
+         for bulk restore of relation statistics. The tracked statistics may
+         change from version to version, so the primary purpose of this
+         function is to maintain a consistent function signature to avoid
+         errors when restoring statistics from previous versions.
+        </para>
+        <para>
+         Arguments are passed as pairs of <replaceable>argname</replaceable>
+         and <replaceable>argvalue</replaceable>, where
+         <replaceable>argname</replaceable> corresponds to a named argument in
+         <function>pg_set_relation_stats()</function> and
+         <replaceable>argvalue</replaceable> is of the corresponding type.
+        </para>
+        <para>
+         Additionally, this function supports argument name
+         <literal>version</literal> of type <type>integer</type>, which
+         specifies the version from which the statistics originated, improving
+         intepretation of older statistics.
+        </para>
+        <para>
+         For example, to set the <structname>relpages</structname> and
+         <structname>reltuples</structname> of the table
+         <structname>mytable</structname>:
+<programlisting>
+ SELECT pg_restore_relation_stats(
+    'relation',  'mytable'::regclass,
+    'relpages',  173::integer,
+    'reltuples', 10000::float4);
+</programlisting>
+        </para>
+        <para>
+         Minor errors are reported as a <literal>WARNING</literal> and
+         ignored, and remaining statistics specified will still be restored.
+        </para>
+       </entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry">
+        <para role="func_signature">
+         <indexterm>
+          <primary>pg_set_attribute_stats</primary>
+         </indexterm>
+         <function>pg_set_attribute_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>attname</parameter> <type>name</type>,
+         <parameter>inherited</parameter> <type>boolean</type>
+         <optional>, <parameter>null_frac</parameter> <type>real</type></optional>
+         <optional>, <parameter>avg_width</parameter> <type>integer</type></optional>
+         <optional>, <parameter>n_distinct</parameter> <type>real</type></optional>
+         <optional>, <parameter>most_common_vals</parameter> <type>text</type>, <parameter>most_common_freqs</parameter> <type>real[]</type> </optional>
+         <optional>, <parameter>histogram_bounds</parameter> <type>text</type> </optional>
+         <optional>, <parameter>correlation</parameter> <type>real</type> </optional>
+         <optional>, <parameter>most_common_elems</parameter> <type>text</type>, <parameter>most_common_elem_freqs</parameter> <type>real[]</type> </optional>
+         <optional>, <parameter>elem_count_histogram</parameter> <type>real[]</type> </optional>
+         <optional>, <parameter>range_length_histogram</parameter> <type>text</type> </optional>
+         <optional>, <parameter>range_empty_frac</parameter> <type>real</type> </optional>
+         <optional>, <parameter>range_bounds_histogram</parameter> <type>text</type> </optional> )
+         <returnvalue>booolean</returnvalue>
+        </para>
+        <para>
+         Creates or updates attribute-level statistics for the given relation
+         and attribute name to the specified values. The parameters correspond
+         to to attributes of the same name found in the <link
+         linkend="view-pg-stats"><structname>pg_stats</structname></link>
+         view. Returns <literal>true</literal> if a change was made;
+         <literal>false</literal> otherwise.
+        </para>
+        <para>
+         All parameters default to <literal>NULL</literal>, which leave the
+         corresponding statistic unchanged.
+        </para>
+        <para>
+         Ordinarily, these statistics are collected automatically or updated
+         as a part of <xref linkend="sql-vacuum"/> or <xref
+         linkend="sql-analyze"/>, so it's not necessary to call this
+         function. However, it may be useful when testing the effects of
+         statistics on the planner to understand or anticipate plan changes.
+        </para>
+        <para>
+         The caller must have the <literal>MAINTAIN</literal> privilege on
+         the table or be the owner of the database.
+        </para>
+        <warning>
+         <para>
+          Changes made by this function are likely to be overwritten by <link
+          linkend="autovacuum">autovacuum</link> (or manual
+          <command>VACUUM</command> or <command>ANALYZE</command>) and should
+          be considered temporary.
+         </para>
+         <para>
+          The signature of this function may change in new major releases if
+          there are changes to the statistics being tracked.
+         </para>
+        </warning>
+       </entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry">
+        <para role="func_signature">
+         <indexterm>
+          <primary>pg_clear_attribute_stats</primary>
+         </indexterm>
+         <function>pg_clear_attribute_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>attname</parameter> <type>name</type>,
+         <parameter>inherited</parameter> <type>boolean</type> )
+         <returnvalue>boolean</returnvalue>
+        </para>
+        <para>
+         Clears table-level statistics for the given relation attribute, as
+         though the table was newly created. Returns <literal>true</literal>
+         if a change was made; <literal>false</literal> otherwise.
+        </para>
+        <para>
+         The caller must have the <literal>MAINTAIN</literal> privilege on
+         the table or be the owner of the database.
+        </para>
+        <warning>
+         <para>
+          Changes made by this function are likely to be overwritten by <link
+          linkend="autovacuum">autovacuum</link> (or manual
+          <command>VACUUM</command> or <command>ANALYZE</command>) and should
+          be considered temporary.
+         </para>
+         <para>
+          The signature of this function may change in new major releases if
+          there are changes to the statistics being tracked.
+         </para>
+        </warning>
+       </entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_restore_attribute_stats</primary>
+        </indexterm>
+        <function>pg_restore_attribute_stats</function> (
+        <literal>VARIADIC</literal> <parameter>kwargs</parameter> <type>"any"</type> )
+        <returnvalue>boolean</returnvalue>
+        </para>
+        <para>
+         Similar to <function>pg_set_attribute_stats()</function>, but
+         intended for bulk restore of attribute statistics. The tracked
+         statistics may change from version to version, so the primary purpose
+         of this function is to maintain a consistent function signature to
+         avoid errors when restoring statistics from previous versions.
+        </para>
+        <para>
+         Arguments are passed as pairs of <replaceable>argname</replaceable>
+         and <replaceable>argvalue</replaceable>, where
+         <replaceable>argname</replaceable> corresponds to a named argument in
+         <function>pg_set_attribute_stats()</function> and
+         <replaceable>argvalue</replaceable> is of the corresponding type.
+        </para>
+        <para>
+         Additionally, this function supports argument name
+         <literal>version</literal> of type <type>integer</type>, which
+         specifies the version from which the statistics originated, improving
+         intepretation of older statistics.
+        </para>
+        <para>
+         For example, to set the <structname>avg_width</structname> and
+         <structname>null_frac</structname> for the attribute
+         <structname>col1</structname> of the table
+         <structname>mytable</structname>:
+<programlisting>
+ SELECT pg_restore_attribute_stats(
+    'relation',    'mytable'::regclass,
+    'attname',     'col1'::name,
+    'inherited',   false,
+    'avg_width',   125::integer,
+    'null_frac',   0.5::real);
+</programlisting>
+        </para>
+        <para>
+         Minor errors are reported as a <literal>WARNING</literal> and
+         ignored, and remaining statistics specified will still be restored.
+        </para>
+       </entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
    <table id="functions-admin-dblocation">
     <title>Database Object Location Functions</title>
     <tgroup cols="1">
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 623b9539b15..b3b2705f4bb 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -639,6 +639,37 @@ LANGUAGE INTERNAL
 CALLED ON NULL INPUT VOLATILE PARALLEL SAFE
 AS 'pg_stat_reset_slru';
 
+CREATE OR REPLACE FUNCTION
+  pg_set_attribute_stats(relation regclass,
+                         attname name,
+                         inherited bool,
+                         null_frac real DEFAULT NULL,
+                         avg_width integer DEFAULT NULL,
+                         n_distinct real DEFAULT NULL,
+                         most_common_vals text DEFAULT NULL,
+                         most_common_freqs real[] DEFAULT NULL,
+                         histogram_bounds text DEFAULT NULL,
+                         correlation real DEFAULT NULL,
+                         most_common_elems text DEFAULT NULL,
+                         most_common_elem_freqs real[] DEFAULT NULL,
+                         elem_count_histogram real[] DEFAULT NULL,
+                         range_length_histogram text DEFAULT NULL,
+                         range_empty_frac real DEFAULT NULL,
+                         range_bounds_histogram text DEFAULT NULL)
+RETURNS bool
+LANGUAGE INTERNAL
+CALLED ON NULL INPUT VOLATILE
+AS 'pg_set_attribute_stats';
+
+CREATE OR REPLACE FUNCTION
+  pg_clear_attribute_stats(relation regclass,
+                           attname name,
+                           inherited bool)
+RETURNS bool
+LANGUAGE INTERNAL
+CALLED ON NULL INPUT VOLATILE
+AS 'pg_clear_attribute_stats';
+
 --
 -- The default permissions for functions mean that anyone can execute them.
 -- A number of functions shouldn't be executable by just anyone, but rather
diff --git a/src/backend/statistics/Makefile b/src/backend/statistics/Makefile
index 89cf8c27973..5e776c02218 100644
--- a/src/backend/statistics/Makefile
+++ b/src/backend/statistics/Makefile
@@ -15,6 +15,7 @@ include $(top_builddir)/src/Makefile.global
 OBJS = \
 	dependencies.o \
 	extended_stats.o \
+	import_stats.o \
 	mcv.o \
 	mvdistinct.o
 
diff --git a/src/backend/statistics/import_stats.c b/src/backend/statistics/import_stats.c
new file mode 100644
index 00000000000..ea87a8e849a
--- /dev/null
+++ b/src/backend/statistics/import_stats.c
@@ -0,0 +1,1263 @@
+/*-------------------------------------------------------------------------
+ * import_stats.c
+ *
+ *	  PostgreSQL statistics import
+ *
+ * Code supporting the direct importation of relation statistics, similar to
+ * what is done by the ANALYZE command.
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *       src/backend/statistics/import_stats.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_collation.h"
+#include "catalog/pg_database.h"
+#include "catalog/pg_operator.h"
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "nodes/nodeFuncs.h"
+#include "statistics/statistics.h"
+#include "utils/acl.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/syscache.h"
+
+#define DEFAULT_RELPAGES		Int32GetDatum(0)
+#define DEFAULT_RELTUPLES		Float4GetDatum(-1.0)
+#define DEFAULT_RELALLVISIBLE	Int32GetDatum(0)
+
+#define DEFAULT_NULL_FRAC      Float4GetDatum(0.0)
+#define DEFAULT_AVG_WIDTH      Int32GetDatum(0)        /* unknown */
+#define DEFAULT_N_DISTINCT     Float4GetDatum(0.0)     /* unknown */
+
+/*
+ * Positional argument numbers, names, and types for
+ * relation_statistics_update() and attribute_statistics_update().
+ */
+
+typedef enum
+{
+	RELATION_ARG = 0,
+	RELPAGES_ARG,
+	RELTUPLES_ARG,
+	RELALLVISIBLE_ARG,
+	NUM_RELATION_STATS_ARGS
+} relation_stats_argnum;
+
+typedef enum
+{
+	ATTRELATION_ARG = 0,
+	ATTNAME_ARG,
+	INHERITED_ARG,
+	NULL_FRAC_ARG,
+	AVG_WIDTH_ARG,
+	N_DISTINCT_ARG,
+	MOST_COMMON_VALS_ARG,
+	MOST_COMMON_FREQS_ARG,
+	HISTOGRAM_BOUNDS_ARG,
+	CORRELATION_ARG,
+	MOST_COMMON_ELEMS_ARG,
+	MOST_COMMON_ELEM_FREQS_ARG,
+	ELEM_COUNT_HISTOGRAM_ARG,
+	RANGE_LENGTH_HISTOGRAM_ARG,
+	RANGE_EMPTY_FRAC_ARG,
+	RANGE_BOUNDS_HISTOGRAM_ARG,
+	NUM_ATTRIBUTE_STATS_ARGS
+} attribute_stats_argnum;
+
+struct arginfo
+{
+	const char	*argname;
+	Oid			 argtype;
+};
+
+static struct arginfo relarginfo[] =
+{
+	[RELATION_ARG]			  = {"relation", REGCLASSOID},
+	[RELPAGES_ARG]			  = {"relpages", INT4OID},
+	[RELTUPLES_ARG]			  = {"reltuples", FLOAT4OID},
+	[RELALLVISIBLE_ARG]		  = {"relallvisible", INT4OID},
+	[NUM_RELATION_STATS_ARGS] = {0}
+};
+
+static struct arginfo attarginfo[] =
+{
+	[ATTRELATION_ARG]			 = {"relation", REGCLASSOID},
+	[ATTNAME_ARG]				 = {"attname", NAMEOID},
+	[INHERITED_ARG]				 = {"inherited", BOOLOID},
+	[NULL_FRAC_ARG]				 = {"null_frac", FLOAT4OID},
+	[AVG_WIDTH_ARG]				 = {"avg_width", INT4OID},
+	[N_DISTINCT_ARG]			 = {"n_distinct", FLOAT4OID},
+	[MOST_COMMON_VALS_ARG]		 = {"most_common_vals", TEXTOID},
+	[MOST_COMMON_FREQS_ARG]		 = {"most_common_freqs", FLOAT4ARRAYOID},
+	[HISTOGRAM_BOUNDS_ARG]		 = {"histogram_bounds", TEXTOID},
+	[CORRELATION_ARG]			 = {"correlation", FLOAT4OID},
+	[MOST_COMMON_ELEMS_ARG]		 = {"most_common_elems", TEXTOID},
+	[MOST_COMMON_ELEM_FREQS_ARG] = {"most_common_elems_freq", FLOAT4ARRAYOID},
+	[ELEM_COUNT_HISTOGRAM_ARG]	 = {"elem_count_histogram", FLOAT4ARRAYOID},
+	[RANGE_LENGTH_HISTOGRAM_ARG] = {"range_length_histogram", TEXTOID},
+	[RANGE_EMPTY_FRAC_ARG]		 = {"range_empty_frac", FLOAT4OID},
+	[RANGE_BOUNDS_HISTOGRAM_ARG] = {"range_bounds_histogram", TEXTOID},
+	[NUM_ATTRIBUTE_STATS_ARGS]	 = {0}
+};
+
+static bool relation_statistics_update(FunctionCallInfo fcinfo, int elevel);
+static bool attribute_statistics_update(FunctionCallInfo fcinfo, int elevel);
+static Node * get_attr_expr(Relation rel, int attnum);
+static void get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
+							   Oid *atttypid, int32 *atttypmod,
+							   char *atttyptype, Oid *atttypcoll,
+							   Oid *eq_opr, Oid *lt_opr);
+static bool get_elem_stat_type(Oid atttypid, char atttyptype, int elevel,
+							   Oid *elemtypid, Oid *elem_eq_opr);
+static Datum text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d,
+							   Oid typid, int32 typmod, int elevel, bool *ok);
+static void set_stats_slot(Datum *values, bool *nulls,
+						   int16 stakind, Oid staop, Oid stacoll,
+						   Datum stanumbers, bool stanumbers_isnull,
+						   Datum stavalues, bool stavalues_isnull);
+static void upsert_pg_statistic(Relation starel, HeapTuple oldtup,
+								Datum values[], bool nulls[]);
+static bool delete_pg_statistic(Oid reloid, AttrNumber attnum, bool stainherit);
+static void lock_check_privileges(Oid reloid);
+static void check_required_arg(FunctionCallInfo fcinfo,
+							   struct arginfo *arginfo, int argnum);
+static void check_arg_array(FunctionCallInfo fcinfo, struct arginfo *arginfo,
+							int argnum, int elevel);
+static void check_arg_pair(FunctionCallInfo fcinfo, struct arginfo *arginfo,
+						   int argnum1, int argnum2, int elevel);
+
+/*
+ * Internal function for modifying statistics for a relation.
+ */
+static bool
+relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
+{
+	Oid				reloid;
+	Relation		crel;
+	HeapTuple		ctup;
+	Form_pg_class	pgcform;
+	int				replaces[3]	= {0};
+	Datum			values[3]	= {0};
+	bool			nulls[3]	= {false, false, false};
+	int				ncols		= 0;
+	TupleDesc		tupdesc;
+	HeapTuple		newtup;
+
+
+	check_required_arg(fcinfo, relarginfo, RELATION_ARG);
+	reloid = PG_GETARG_OID(RELATION_ARG);
+
+	lock_check_privileges(reloid);
+
+	/*
+	 * Take RowExclusiveLock on pg_class, consistent with
+	 * vac_update_relstats().
+	 */
+	crel = table_open(RelationRelationId, RowExclusiveLock);
+
+	tupdesc = RelationGetDescr(crel);
+	ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(reloid));
+	if (!HeapTupleIsValid(ctup))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_OBJECT_IN_USE),
+				 errmsg("pg_class entry for relid %u not found", reloid)));
+		table_close(crel, RowExclusiveLock);
+		return false;
+	}
+
+	pgcform = (Form_pg_class) GETSTRUCT(ctup);
+
+	/* relpages */
+	if (!PG_ARGISNULL(RELPAGES_ARG))
+	{
+		int32 relpages = PG_GETARG_INT32(RELPAGES_ARG);
+
+		if (relpages < 0)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("relpages cannot be < 0")));
+			table_close(crel, RowExclusiveLock);
+			return false;
+		}
+
+		if (relpages != pgcform->relpages)
+		{
+			replaces[ncols] = Anum_pg_class_relpages;
+			values[ncols] = Int32GetDatum(relpages);
+			ncols++;
+		}
+	}
+
+	if (!PG_ARGISNULL(RELTUPLES_ARG))
+	{
+		float reltuples = PG_GETARG_FLOAT4(RELTUPLES_ARG);
+
+		if (reltuples < -1.0)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("reltuples cannot be < -1.0")));
+			table_close(crel, RowExclusiveLock);
+			return false;
+		}
+
+		if (reltuples != pgcform->reltuples)
+		{
+			replaces[ncols] = Anum_pg_class_reltuples;
+			values[ncols] = Float4GetDatum(reltuples);
+			ncols++;
+		}
+	}
+
+	if (!PG_ARGISNULL(RELALLVISIBLE_ARG))
+	{
+		int32 relallvisible = PG_GETARG_INT32(RELALLVISIBLE_ARG);
+
+		if (relallvisible < 0)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("relallvisible cannot be < 0")));
+			table_close(crel, RowExclusiveLock);
+			return false;
+		}
+
+		if (relallvisible != pgcform->relallvisible)
+		{
+			replaces[ncols] = Anum_pg_class_relallvisible;
+			values[ncols] = Int32GetDatum(relallvisible);
+			ncols++;
+		}
+	}
+
+	/* only update pg_class if there is a meaningful change */
+	if (ncols == 0)
+	{
+		table_close(crel, RowExclusiveLock);
+		return false;
+	}
+
+	newtup = heap_modify_tuple_by_cols(ctup, tupdesc, ncols, replaces, values,
+									   nulls);
+
+	CatalogTupleUpdate(crel, &newtup->t_self, newtup);
+	heap_freetuple(newtup);
+
+	/* release the lock, consistent with vac_update_relstats() */
+	table_close(crel, RowExclusiveLock);
+
+	return true;
+}
+
+/*
+ * Insert or Update Attribute Statistics
+ *
+ * Major errors, such as the table not existing, the attribute not existing,
+ * or a permissions failure are always reported at ERROR. Other errors, such
+ * as a conversion failure, are reported at 'elevel', and a partial update
+ * will result.
+ *
+ * See pg_statistic.h for an explanation of how each statistic kind is
+ * stored. Custom statistics kinds are not supported.
+ *
+ * Depending on the statistics kind, we need to derive information from the
+ * attribute for which we're storing the stats. For instance, the MCVs are
+ * stored as an anyarray, and the representation of the array needs to store
+ * the correct element type, which must be derived from the attribute.
+ */
+static bool
+attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
+{
+	Oid			reloid;
+	Name	    attname;
+	bool		inherited;
+	AttrNumber	attnum;
+
+	Relation	starel;
+	HeapTuple	statup;
+
+	Oid			atttypid;
+	int32		atttypmod;
+	char		atttyptype;
+	Oid			atttypcoll;
+	Oid			eq_opr;
+	Oid			lt_opr;
+
+	Oid			elemtypid;
+	Oid			elem_eq_opr;
+
+	FmgrInfo	array_in_fn;
+
+	Datum		values[Natts_pg_statistic];
+	bool		nulls[Natts_pg_statistic];
+
+	check_required_arg(fcinfo, attarginfo, ATTRELATION_ARG);
+	reloid = PG_GETARG_OID(ATTRELATION_ARG);
+
+	check_required_arg(fcinfo, attarginfo, ATTNAME_ARG);
+	attname = PG_GETARG_NAME(ATTNAME_ARG);
+	attnum = get_attnum(reloid, NameStr(*attname));
+
+	if (PG_ARGISNULL(INHERITED_ARG))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("\"%s\" cannot be NULL",
+						attarginfo[INHERITED_ARG].argname)));
+		PG_RETURN_BOOL(false);
+	}
+	inherited = PG_GETARG_BOOL(INHERITED_ARG);
+
+	/*
+	 * Check argument sanity. If some arguments are unusable, emit at elevel
+	 * and set the corresponding argument to NULL in fcinfo.
+	 *
+	 * NB: may modify fcinfo
+	 */
+
+	check_arg_array(fcinfo, attarginfo, MOST_COMMON_FREQS_ARG, elevel);
+	check_arg_array(fcinfo, attarginfo, MOST_COMMON_ELEM_FREQS_ARG, elevel);
+	check_arg_array(fcinfo, attarginfo, ELEM_COUNT_HISTOGRAM_ARG, elevel);
+
+	/* STATISTIC_KIND_MCV */
+	check_arg_pair(fcinfo, attarginfo,
+				   MOST_COMMON_VALS_ARG, MOST_COMMON_FREQS_ARG,
+				   elevel);
+
+	/* STATISTIC_KIND_MCELEM */
+	check_arg_pair(fcinfo, attarginfo,
+				   MOST_COMMON_ELEMS_ARG, MOST_COMMON_ELEM_FREQS_ARG,
+				   elevel);
+
+	/* STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM */
+	check_arg_pair(fcinfo, attarginfo,
+				   RANGE_LENGTH_HISTOGRAM_ARG, RANGE_EMPTY_FRAC_ARG,
+				   elevel);
+
+	lock_check_privileges(reloid);
+
+	/* derive information from attribute */
+	get_attr_stat_type(reloid, attnum, elevel,
+					   &atttypid, &atttypmod,
+					   &atttyptype, &atttypcoll,
+					   &eq_opr, &lt_opr);
+
+	/* if needed, derive element type */
+	if (!PG_ARGISNULL(MOST_COMMON_ELEMS_ARG) ||
+		!PG_ARGISNULL(ELEM_COUNT_HISTOGRAM_ARG))
+	{
+		if (!get_elem_stat_type(atttypid, atttyptype, elevel,
+								&elemtypid, &elem_eq_opr))
+		{
+			ereport(elevel,
+					(errmsg("unable to determine element type of attribute \"%s\"", NameStr(*attname)),
+					 errdetail("Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.")));
+			elemtypid = InvalidOid;
+			elem_eq_opr = InvalidOid;
+			fcinfo->args[MOST_COMMON_ELEMS_ARG].isnull = true;
+			fcinfo->args[ELEM_COUNT_HISTOGRAM_ARG].isnull = true;
+		}
+	}
+
+	/* histogram and correlation require less-than operator */
+	if ((!PG_ARGISNULL(HISTOGRAM_BOUNDS_ARG) || !PG_ARGISNULL(CORRELATION_ARG)) &&
+		!OidIsValid(lt_opr))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("could not determine less-than operator for attribute \"%s\"", NameStr(*attname)),
+				 errdetail("Cannot set STATISTIC_KIND_HISTOGRAM or STATISTIC_KIND_CORRELATION.")));
+		fcinfo->args[HISTOGRAM_BOUNDS_ARG].isnull = true;
+		fcinfo->args[CORRELATION_ARG].isnull = true;
+	}
+
+	/* only range types can have range stats */
+	if ((!PG_ARGISNULL(RANGE_LENGTH_HISTOGRAM_ARG) || !PG_ARGISNULL(RANGE_BOUNDS_HISTOGRAM_ARG)) &&
+		!(atttyptype == TYPTYPE_RANGE || atttyptype == TYPTYPE_MULTIRANGE))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("attribute \"%s\" is not a range type", NameStr(*attname)),
+				 errdetail("Cannot set STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM or STATISTIC_KIND_BOUNDS_HISTOGRAM.")));
+
+		fcinfo->args[RANGE_LENGTH_HISTOGRAM_ARG].isnull = true;
+		fcinfo->args[RANGE_EMPTY_FRAC_ARG].isnull = true;
+	}
+
+	fmgr_info(F_ARRAY_IN, &array_in_fn);
+
+	starel = table_open(StatisticRelationId, RowExclusiveLock);
+
+	statup = SearchSysCache3(STATRELATTINH, reloid, attnum, inherited);
+	if (HeapTupleIsValid(statup))
+	{
+		/* initialize from existing tuple */
+		heap_deform_tuple(statup, RelationGetDescr(starel), values, nulls);
+	}
+	else
+	{
+		/*
+		 * Initialize nulls array to be false for all non-NULL attributes, and
+		 * true for all nullable attributes.
+		 */
+		for (int i = 0; i < Natts_pg_statistic; i++)
+		{
+			values[i] = (Datum) 0;
+			if (i < Anum_pg_statistic_stanumbers1 - 1)
+				nulls[i] = false;
+			else
+				nulls[i] = true;
+		}
+
+		values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(reloid);
+		values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(attnum);
+		values[Anum_pg_statistic_stainherit - 1] = BoolGetDatum(inherited);
+		values[Anum_pg_statistic_stanullfrac - 1] = DEFAULT_NULL_FRAC;
+		values[Anum_pg_statistic_stawidth - 1] = DEFAULT_AVG_WIDTH;
+		values[Anum_pg_statistic_stadistinct - 1] = DEFAULT_N_DISTINCT;
+	}
+
+	if (!PG_ARGISNULL(NULL_FRAC_ARG))
+		values[Anum_pg_statistic_stanullfrac - 1] = PG_GETARG_DATUM(NULL_FRAC_ARG);
+	if (!PG_ARGISNULL(AVG_WIDTH_ARG))
+		values[Anum_pg_statistic_stawidth - 1] = PG_GETARG_DATUM(AVG_WIDTH_ARG);
+	if (!PG_ARGISNULL(N_DISTINCT_ARG))
+		values[Anum_pg_statistic_stadistinct - 1] = PG_GETARG_DATUM(N_DISTINCT_ARG);
+
+	/*
+	 * STATISTIC_KIND_MCV
+	 * 
+	 * Convert most_common_vals from text to anyarray, where the element type
+	 * is the attribute type, and store in stavalues. Store most_common_freqs
+	 * in stanumbers.
+	 */
+	if (!PG_ARGISNULL(MOST_COMMON_VALS_ARG))
+	{
+		bool		converted;
+		Datum		stanumbers = PG_GETARG_DATUM(MOST_COMMON_FREQS_ARG);
+		Datum		stavalues = text_to_stavalues("most_common_vals",
+												  &array_in_fn,
+												  PG_GETARG_DATUM(MOST_COMMON_VALS_ARG),
+												  atttypid, atttypmod,
+												  elevel, &converted);
+
+		if (converted)
+		{
+			set_stats_slot(values, nulls,
+						   STATISTIC_KIND_MCV,
+						   eq_opr, atttypcoll,
+						   stanumbers, false, stavalues, false);
+		}
+		else
+		{
+			fcinfo->args[MOST_COMMON_VALS_ARG].isnull = true;
+			fcinfo->args[MOST_COMMON_FREQS_ARG].isnull = true;
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_HISTOGRAM
+	 *
+	 * histogram_bounds: ANYARRAY::text
+	 */
+	if (!PG_ARGISNULL(HISTOGRAM_BOUNDS_ARG))
+	{
+		Datum		stavalues;
+		bool		converted = false;
+
+		stavalues = text_to_stavalues("histogram_bounds",
+									  &array_in_fn,
+									  PG_GETARG_DATUM(HISTOGRAM_BOUNDS_ARG),
+									  atttypid, atttypmod, elevel,
+									  &converted);
+
+		if (converted)
+		{
+			set_stats_slot(values, nulls,
+						   STATISTIC_KIND_HISTOGRAM,
+						   lt_opr, atttypcoll,
+						   0, true, stavalues, false);
+		}
+		else
+			fcinfo->args[HISTOGRAM_BOUNDS_ARG].isnull = true;
+	}
+
+	/*
+	 * STATISTIC_KIND_CORRELATION
+	 *
+	 * correlation: real
+	 */
+	if (!PG_ARGISNULL(CORRELATION_ARG))
+	{
+		Datum		elems[] = {PG_GETARG_DATUM(CORRELATION_ARG)};
+		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+		Datum		stanumbers = PointerGetDatum(arry);
+
+		set_stats_slot(values, nulls,
+					   STATISTIC_KIND_CORRELATION,
+					   lt_opr, atttypcoll,
+					   stanumbers, false, 0, true);
+	}
+
+	/*
+	 * STATISTIC_KIND_MCELEM
+	 *
+	 * most_common_elem_freqs: real[]
+	 *
+	 * most_common_elems     : ANYARRAY::text
+	 */
+	if (!PG_ARGISNULL(MOST_COMMON_ELEMS_ARG))
+	{
+		Datum		stanumbers = PG_GETARG_DATUM(MOST_COMMON_ELEM_FREQS_ARG);
+		bool		converted = false;
+		Datum		stavalues;
+
+		stavalues = text_to_stavalues("most_common_elems",
+									  &array_in_fn,
+									  PG_GETARG_DATUM(MOST_COMMON_ELEMS_ARG),
+									  elemtypid, atttypmod,
+									  elevel, &converted);
+
+		if (converted)
+		{
+			set_stats_slot(values, nulls,
+						   STATISTIC_KIND_MCELEM,
+						   elem_eq_opr, atttypcoll,
+						   stanumbers, false, stavalues, false);
+		}
+		else
+		{
+			/* the mc_elem stat did not write */
+			fcinfo->args[MOST_COMMON_ELEMS_ARG].isnull = true;
+			fcinfo->args[MOST_COMMON_ELEM_FREQS_ARG].isnull = true;
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_DECHIST
+	 *
+	 * elem_count_histogram:	real[]
+	 */
+	if (!PG_ARGISNULL(ELEM_COUNT_HISTOGRAM_ARG))
+	{
+		Datum		stanumbers = PG_GETARG_DATUM(ELEM_COUNT_HISTOGRAM_ARG);
+
+		set_stats_slot(values, nulls,
+					   STATISTIC_KIND_DECHIST,
+					   elem_eq_opr, atttypcoll,
+					   stanumbers, false, 0, true);
+	}
+
+	/*
+	 * STATISTIC_KIND_BOUNDS_HISTOGRAM
+	 *
+	 * range_bounds_histogram: ANYARRAY::text
+	 *
+	 * This stakind appears before STATISTIC_KIND_BOUNDS_HISTOGRAM even though
+	 * it is numerically greater, and all other stakinds appear in numerical
+	 * order. We duplicate this quirk to make before/after tests of
+	 * pg_statistic records easier.
+	 */
+	if (!PG_ARGISNULL(RANGE_BOUNDS_HISTOGRAM_ARG))
+	{
+		bool		converted = false;
+		Datum		stavalues;
+
+		stavalues = text_to_stavalues("range_bounds_histogram",
+									  &array_in_fn,
+									  PG_GETARG_DATUM(RANGE_BOUNDS_HISTOGRAM_ARG),
+									  atttypid, atttypmod,
+									  elevel, &converted);
+
+		if (converted)
+		{
+			set_stats_slot(values, nulls,
+						   STATISTIC_KIND_BOUNDS_HISTOGRAM,
+						   InvalidOid, InvalidOid,
+						   0, true, stavalues, false);
+		}
+		else
+			fcinfo->args[RANGE_BOUNDS_HISTOGRAM_ARG].isnull = true;
+	}
+
+	/*
+	 * STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM
+	 *
+	 * range_empty_frac: real
+	 *
+	 * range_length_histogram:  double precision[]::text
+	 */
+	if (!PG_ARGISNULL(RANGE_LENGTH_HISTOGRAM_ARG))
+	{
+		/* The anyarray is always a float8[] for this stakind */
+		Datum		elems[] = {PG_GETARG_DATUM(RANGE_EMPTY_FRAC_ARG)};
+		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+		Datum		stanumbers = PointerGetDatum(arry);
+
+		bool		converted = false;
+		Datum		stavalues;
+
+		stavalues = text_to_stavalues("range_length_histogram",
+									  &array_in_fn,
+									  PG_GETARG_DATUM(RANGE_LENGTH_HISTOGRAM_ARG),
+									  FLOAT8OID, 0, elevel, &converted);
+
+		if (converted)
+		{
+			set_stats_slot(values, nulls,
+						   STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM,
+						   Float8LessOperator, InvalidOid,
+						   stanumbers, false, stavalues, false);
+		}
+		else
+		{
+			fcinfo->args[RANGE_EMPTY_FRAC_ARG].isnull = true;
+			fcinfo->args[RANGE_LENGTH_HISTOGRAM_ARG].isnull = true;
+		}
+	}
+
+	upsert_pg_statistic(starel, statup, values, nulls);
+
+	if (HeapTupleIsValid(statup))
+		ReleaseSysCache(statup);
+	table_close(starel, RowExclusiveLock);
+
+	return true;
+}
+
+/*
+ * If this relation is an index and that index has expressions in it, and
+ * the attnum specified is known to be an expression, then we must walk
+ * the list attributes up to the specified attnum to get the right
+ * expression.
+ */
+static Node *
+get_attr_expr(Relation rel, int attnum)
+{
+	if ((rel->rd_rel->relkind == RELKIND_INDEX
+		 || (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+		&& (rel->rd_indexprs != NIL)
+		&& (rel->rd_index->indkey.values[attnum - 1] == 0))
+	{
+		ListCell   *indexpr_item = list_head(rel->rd_indexprs);
+
+		for (int i = 0; i < attnum - 1; i++)
+			if (rel->rd_index->indkey.values[i] == 0)
+				indexpr_item = lnext(rel->rd_indexprs, indexpr_item);
+
+		if (indexpr_item == NULL)	/* shouldn't happen */
+			elog(ERROR, "too few entries in indexprs list");
+
+		return (Node *) lfirst(indexpr_item);
+	}
+	return NULL;
+}
+
+/*
+ * Derive type information from the attribute.
+ */
+static void
+get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
+				   Oid *atttypid, int32 *atttypmod,
+				   char *atttyptype, Oid *atttypcoll,
+				   Oid *eq_opr, Oid *lt_opr)
+{
+	Relation rel = relation_open(reloid, AccessShareLock);
+	Form_pg_attribute attr;
+	HeapTuple	atup;
+	Node	   *expr;
+	TypeCacheEntry *typcache;
+
+	atup = SearchSysCache2(ATTNUM, ObjectIdGetDatum(reloid),
+						   Int16GetDatum(attnum));
+
+	/* Attribute not found */
+	if (!HeapTupleIsValid(atup))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_COLUMN),
+				 errmsg("attribute %d of relation with OID %u does not exist",
+						attnum, reloid)));
+
+	attr = (Form_pg_attribute) GETSTRUCT(atup);
+
+	if (attr->attisdropped)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_COLUMN),
+				 errmsg("attribute %d of relation with OID %u does not exist",
+						attnum, RelationGetRelid(rel))));
+
+	expr = get_attr_expr(rel, attr->attnum);
+
+	if (expr == NULL)
+	{
+		*atttypid = attr->atttypid;
+		*atttypmod = attr->atttypmod;
+		*atttypcoll = attr->attcollation;
+	}
+	else
+	{
+		*atttypid = exprType(expr);
+		*atttypmod = exprTypmod(expr);
+
+		/* TODO: better explanation */
+		/*
+		 * If a collation has been specified for the index column, use that in
+		 * preference to anything else; but if not, fall back to whatever we
+		 * can get from the expression.
+		 */
+		if (OidIsValid(attr->attcollation))
+			*atttypcoll = attr->attcollation;
+		else
+			*atttypcoll = exprCollation(expr);
+	}
+	ReleaseSysCache(atup);
+
+	/* TODO: better explanation */
+	/* if it's a multirange, step down to the range type */
+	if (type_is_multirange(*atttypid))
+		*atttypid = get_multirange_range(*atttypid);
+
+	typcache = lookup_type_cache(*atttypid, TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR);
+	*atttyptype = typcache->typtype;
+	*eq_opr = typcache->eq_opr;
+	*lt_opr = typcache->lt_opr;
+
+	/* TODO: explain special case for tsvector */ 
+	if (*atttypid == TSVECTOROID)
+		*atttypcoll = DEFAULT_COLLATION_OID;
+
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Derive element type information from the attribute type.
+ */
+static bool
+get_elem_stat_type(Oid atttypid, char atttyptype, int elevel,
+				   Oid *elemtypid, Oid *elem_eq_opr)
+{
+	TypeCacheEntry *elemtypcache;
+
+	/* TODO: explain special case for tsvector */
+	if (atttypid == TSVECTOROID)
+		*elemtypid = TEXTOID;
+	else if (atttyptype == TYPTYPE_RANGE)
+		*elemtypid = get_range_subtype(atttypid);
+	else
+		*elemtypid = get_base_element_type(atttypid);
+
+	if (!OidIsValid(*elemtypid))
+		return false;
+
+	elemtypcache = lookup_type_cache(*elemtypid, TYPECACHE_EQ_OPR);
+	if (!OidIsValid(elemtypcache->eq_opr))
+		return false;
+
+	*elem_eq_opr = elemtypcache->eq_opr;
+
+	return true;
+}
+
+/*
+ * Cast a text datum into an array with element type elemtypid.
+ *
+ * If an error is encountered, capture it and re-throw at elevel, and set ok
+ * to false. If the resulting array contains NULLs, raise an error at elevel
+ * and set ok to false. Otherwise, set ok to true.
+ */
+static Datum
+text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid typid,
+				  int32 typmod, int elevel, bool *ok)
+{
+	LOCAL_FCINFO(fcinfo, 8);
+	char	   *s;
+	Datum		result;
+	ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+	escontext.details_wanted = true;
+
+	s = TextDatumGetCString(d);
+
+	InitFunctionCallInfoData(*fcinfo, array_in, 3, InvalidOid,
+							 (Node *) &escontext, NULL);
+
+	fcinfo->args[0].value = CStringGetDatum(s);
+	fcinfo->args[0].isnull = false;
+	fcinfo->args[1].value = ObjectIdGetDatum(typid);
+	fcinfo->args[1].isnull = false;
+	fcinfo->args[2].value = Int32GetDatum(typmod);
+	fcinfo->args[2].isnull = false;
+
+	result = FunctionCallInvoke(fcinfo);
+
+	pfree(s);
+
+	if (SOFT_ERROR_OCCURRED(&escontext))
+	{
+		if (elevel != ERROR)
+			escontext.error_data->elevel = elevel;
+		ThrowErrorData(escontext.error_data);
+		*ok = false;
+		return (Datum)0;
+	}
+
+	if (array_contains_nulls(DatumGetArrayTypeP(result)))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("\"%s\" array cannot contain NULL values", staname)));
+		*ok = false;
+		return (Datum)0;
+	}
+
+	*ok = true;
+
+	return result;
+}
+
+/*
+ * Find and update the slot with the given stakind, or use the first empty
+ * slot.
+ */
+static void
+set_stats_slot(Datum *values, bool *nulls,
+			   int16 stakind, Oid staop, Oid stacoll,
+			   Datum stanumbers, bool stanumbers_isnull,
+			   Datum stavalues, bool stavalues_isnull)
+{
+	int slotidx;
+	int first_empty = -1;
+
+	/* find existing slot with given stakind */
+	for (slotidx = 0; slotidx < STATISTIC_NUM_SLOTS; slotidx++)
+	{
+		AttrNumber stakind_attnum = Anum_pg_statistic_stakind1 - 1 + slotidx;
+		if (first_empty < 0 &&
+			DatumGetInt16(values[stakind_attnum]) == 0)
+			first_empty = slotidx;
+		if (DatumGetInt16(values[stakind_attnum]) == stakind)
+			break;
+	}
+
+	if (slotidx >= STATISTIC_NUM_SLOTS && first_empty >= 0)
+		slotidx = first_empty;
+
+	if (slotidx >= STATISTIC_NUM_SLOTS)
+		ereport(ERROR,
+				(errmsg("maximum number of statistics slots exceeded: %d",
+						slotidx + 1)));
+
+	values[Anum_pg_statistic_stakind1 - 1 + slotidx] = stakind;
+	values[Anum_pg_statistic_staop1 - 1 + slotidx] = staop;
+	values[Anum_pg_statistic_stacoll1 - 1 + slotidx] = stacoll;
+
+	if (!stanumbers_isnull)
+	{
+		values[Anum_pg_statistic_stanumbers1 - 1 + slotidx] = stanumbers;
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + slotidx] = false;
+	}
+	if (!stavalues_isnull)
+	{
+		values[Anum_pg_statistic_stavalues1 - 1 + slotidx] = stavalues;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + slotidx] = false;
+	}
+}
+
+/*
+ * Upsert the pg_statistic record.
+ */
+static void
+upsert_pg_statistic(Relation starel, HeapTuple oldtup,
+					Datum values[], bool nulls[])
+{
+	HeapTuple newtup;
+
+	if (HeapTupleIsValid(oldtup))
+	{
+		bool		replaces[Natts_pg_statistic];
+
+		for (int i = 0; i < Natts_pg_statistic; i++)
+			replaces[i] = true;
+
+		newtup = heap_modify_tuple(oldtup, RelationGetDescr(starel),
+								   values, nulls, replaces);
+		CatalogTupleUpdate(starel, &newtup->t_self, newtup);
+	}
+	else
+	{
+		newtup = heap_form_tuple(RelationGetDescr(starel), values, nulls);
+		CatalogTupleInsert(starel, newtup);
+	}
+
+	heap_freetuple(newtup);
+}
+
+/*
+ * Delete pg_statistic record.
+ */
+static bool
+delete_pg_statistic(Oid reloid, AttrNumber attnum, bool stainherit)
+{
+	Relation	sd = table_open(StatisticRelationId, RowExclusiveLock);
+	HeapTuple	oldtup;
+
+	/* Is there already a pg_statistic tuple for this attribute? */
+	oldtup = SearchSysCache3(STATRELATTINH,
+							 ObjectIdGetDatum(reloid),
+							 Int16GetDatum(attnum),
+							 BoolGetDatum(stainherit));
+
+	if (HeapTupleIsValid(oldtup))
+	{
+		CatalogTupleDelete(sd, &oldtup->t_self);
+		ReleaseSysCache(oldtup);
+		table_close(sd, RowExclusiveLock);
+		return true;
+	}
+
+	table_close(sd, RowExclusiveLock);
+	return false;
+}
+
+/*
+ * Lock relation in ShareUpdateExclusive mode, check privileges, and close the
+ * relation (but retain the lock).
+ *
+ * A role has privileges to set statistics on the relation if any of the
+ * following are true:
+ *   - the role owns the current database and the relation is not shared
+ *   - the role has the MAINTAIN privilege on the relation
+ */
+static void
+lock_check_privileges(Oid reloid)
+{
+	Relation	rel = relation_open(reloid, ShareUpdateExclusiveLock);
+
+	if (rel->rd_rel->relisshared)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("cannot modify statistics for shared relation")));
+
+	if (!object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId()))
+	{
+		AclResult aclresult = pg_class_aclcheck(RelationGetRelid(rel),
+												GetUserId(),
+												ACL_MAINTAIN);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult,
+						   get_relkind_objtype(rel->rd_rel->relkind),
+						   NameStr(rel->rd_rel->relname));
+	}
+
+	relation_close(rel, NoLock);
+}
+
+static bool
+check_arg_type(const char *argname, Oid argtype, Oid expectedtype, int elevel)
+{
+	if (argtype != expectedtype)
+	{
+		ereport(elevel,
+				(errmsg("argument \"%s\" has type \"%s\", expected type \"%s\"",
+						argname, format_type_be(argtype),
+						format_type_be(expectedtype))));
+		return false;
+	}
+
+	return true;
+}
+
+static void
+check_required_arg(FunctionCallInfo fcinfo, struct arginfo *arginfo,
+				   int argnum)
+{
+	if (PG_ARGISNULL(argnum))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("\"%s\" cannot be NULL",
+						arginfo[argnum].argname)));
+}
+
+/*
+ * Check that array argument is one dimensional with no NULLs.
+ *
+ * If not, emit at elevel, and set argument to NULL in fcinfo.
+ */
+static void
+check_arg_array(FunctionCallInfo fcinfo, struct arginfo *arginfo,
+				int argnum, int elevel)
+{
+	ArrayType  *arr;
+
+	if (PG_ARGISNULL(argnum))
+		return;
+
+	arr = DatumGetArrayTypeP(PG_GETARG_DATUM(argnum));
+
+	if (ARR_NDIM(arr) != 1)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("\"%s\" cannot be a multidimensional array",
+						arginfo[argnum].argname)));
+		fcinfo->args[argnum].isnull = true;
+	}
+
+	if (array_contains_nulls(arr))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("\"%s\" array cannot contain NULL values",
+						arginfo[argnum].argname)));
+		fcinfo->args[argnum].isnull = true;
+	}
+}
+
+/*
+ * Enforce parameter pairs that must be specified together for a particular
+ * stakind, such as most_common_vals and most_common_freqs for
+ * STATISTIC_KIND_MCV. If one is NULL and the other is not, emit at elevel,
+ * and ignore the stakind by setting both to NULL in fcinfo.
+ */
+static void
+check_arg_pair(FunctionCallInfo fcinfo, struct arginfo *arginfo,
+			   int argnum1, int argnum2, int elevel)
+{
+	if (PG_ARGISNULL(argnum1) && !PG_ARGISNULL(argnum2))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("\"%s\" must be specified when \"%s\" is specified",
+						arginfo[argnum1].argname,
+						arginfo[argnum2].argname)));
+		fcinfo->args[argnum2].isnull = true;
+	}
+	if (!PG_ARGISNULL(argnum1) && PG_ARGISNULL(argnum2))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("\"%s\" must be specified when \"%s\" is specified",
+						arginfo[argnum2].argname,
+						arginfo[argnum1].argname)));
+		fcinfo->args[argnum1].isnull = true;
+	}
+}
+
+/*
+ * Set statistics for a given pg_class entry.
+ */
+Datum
+pg_set_relation_stats(PG_FUNCTION_ARGS)
+{
+	PG_RETURN_BOOL(relation_statistics_update(fcinfo, ERROR));
+}
+
+/*
+ * Find the argument number for the given argument name, returning -1 if not
+ * found.
+ */
+static int
+get_argnum(const char *argname, struct arginfo *arginfo, int elevel)
+{
+	int argnum;
+
+	for (argnum = 0; arginfo[argnum].argname != NULL; argnum++)
+		if (strcasecmp(argname, arginfo[argnum].argname) == 0)
+			return argnum;
+
+	ereport(elevel,
+			(errmsg("unrecognized argument name: \"%s\"", argname)));
+
+	return -1;
+}
+
+/*
+ * Translate variadic argument pairs from 'pairs_fcinfo' into a
+ * 'positional_fcinfo' appropriate for calling relation_statistics_update() or
+ * attribute_statistics_update() with positional arguments.
+ *
+ * Caller should have already initialized positional_fcinfo with a size
+ * appropriate for calling the intended positional function, and arginfo
+ * should also match the intended positional function.
+ */
+static void
+fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
+						   FunctionCallInfo positional_fcinfo,
+						   struct arginfo *arginfo,
+						   int elevel)
+{
+	Datum   *args;
+	bool    *argnulls;
+	Oid		*types;
+	int      nargs;
+
+	for (int i = 0; arginfo[i].argname != NULL; i++)
+		positional_fcinfo->args[i].isnull = true;
+
+	nargs = extract_variadic_args(pairs_fcinfo, 0, true,
+								  &args, &types, &argnulls);
+
+	if (nargs % 2 != 0)
+		ereport(ERROR,
+				errmsg("need even number of variable arguments"));
+
+	for (int i = 0; i < nargs; i+=2)
+	{
+		int		 argnum;
+		char	*argname;
+
+		if (argnulls[i] || types[i] != TEXTOID)
+			ereport(ERROR, (errmsg("need text argument names")));
+
+		if (argnulls[i+1])
+			continue;
+
+		argname = TextDatumGetCString(args[i]);
+
+		/*
+		 * Version is not a valid positional argument. In the future, it can
+		 * be used to interpret older statistics properly, but it is ignored
+		 * for now.
+		 */
+		if (strcasecmp(argname, "version") == 0)
+			continue;
+		
+		argnum = get_argnum(argname, arginfo, elevel);
+
+		if (argnum >= 0 &&
+			check_arg_type(argname, types[i+1], arginfo[argnum].argtype,
+						   elevel))
+		{
+			positional_fcinfo->args[argnum].value = args[i+1];
+			positional_fcinfo->args[argnum].isnull = false;
+		}
+	}
+}
+
+/*
+ * Clear statistics for a given pg_class entry; that is, set back to initial
+ * stats for a newly-created table.
+ */
+Datum
+pg_clear_relation_stats(PG_FUNCTION_ARGS)
+{
+	LOCAL_FCINFO(newfcinfo, 4);
+
+	InitFunctionCallInfoData(*newfcinfo, NULL, 4, InvalidOid, NULL, NULL);
+
+	newfcinfo->args[0].value = PG_GETARG_OID(0);
+	newfcinfo->args[0].isnull = PG_ARGISNULL(0);
+	newfcinfo->args[1].value = DEFAULT_RELPAGES;
+	newfcinfo->args[1].isnull = false;
+	newfcinfo->args[2].value = DEFAULT_RELTUPLES;
+	newfcinfo->args[2].isnull = false;
+	newfcinfo->args[3].value = DEFAULT_RELALLVISIBLE;
+	newfcinfo->args[3].isnull = false;
+
+	PG_RETURN_BOOL(relation_statistics_update(newfcinfo, ERROR));
+}
+
+Datum
+pg_restore_relation_stats(PG_FUNCTION_ARGS)
+{
+	LOCAL_FCINFO(positional_fcinfo, NUM_RELATION_STATS_ARGS);
+
+	InitFunctionCallInfoData(*positional_fcinfo, NULL,
+							 NUM_RELATION_STATS_ARGS,
+							 InvalidOid, NULL, NULL);
+
+	fill_fcinfo_from_arg_pairs(fcinfo, positional_fcinfo, relarginfo,
+							   WARNING);
+	PG_RETURN_BOOL(relation_statistics_update(positional_fcinfo, WARNING));
+}
+
+/*
+ * Import statistics for a given relation attribute.
+ *
+ * This will insert/replace a row in pg_statistic for the given relation and
+ * attribute name.
+ *
+ * The function takes input parameters that correspond to columns in the view
+ * pg_stats.
+ *
+ * Of those, the columns attname, inherited, null_frac, avg_width, and
+ * n_distinct all correspond to NOT NULL columns in pg_statistic. These
+ * parameters have no default value and passing NULL to them will result
+ * in an error.
+ *
+ * If there is no attribute with a matching attname in the relation, the
+ * function will raise an error. Likewise for setting inherited statistics
+ * on a table that is not partitioned.
+ *
+ * The remaining parameters all belong to a specific stakind. Some stakinds
+ * have multiple parameters, and in those cases both parameters must be
+ * NOT NULL or both NULL, otherwise an error will be raised.
+ *
+ * Omitting a parameter or explicitly passing NULL means that that particular
+ * stakind is not associated with the attribute.
+ *
+ * Parameters that are NOT NULL will be inspected for consistency checks,
+ * any of which can raise an error.
+ *
+ * Parameters corresponding to ANYARRAY columns are instead passed in as text
+ * values, which is a valid input string for an array of the type or element
+ * type of the attribute. Any error generated by the array_in() function will
+ * in turn fail the function.
+ */
+Datum
+pg_set_attribute_stats(PG_FUNCTION_ARGS)
+{
+	PG_RETURN_BOOL(attribute_statistics_update(fcinfo, ERROR));
+}
+
+/*
+ * Delete statistics for the given attribute.
+ */
+Datum
+pg_clear_attribute_stats(PG_FUNCTION_ARGS)
+{
+	Oid			reloid;
+	Name		attname;
+	AttrNumber	attnum;
+	bool		inherited;
+
+	check_required_arg(fcinfo, attarginfo, ATTRELATION_ARG);
+	reloid = PG_GETARG_OID(ATTRELATION_ARG);
+
+	check_required_arg(fcinfo, attarginfo, ATTNAME_ARG);
+	attname = PG_GETARG_NAME(ATTNAME_ARG);
+	attnum = get_attnum(reloid, NameStr(*attname));
+
+	check_required_arg(fcinfo, attarginfo, INHERITED_ARG);
+	inherited = PG_GETARG_BOOL(INHERITED_ARG);
+
+	lock_check_privileges(reloid);
+
+	PG_RETURN_BOOL(delete_pg_statistic(reloid, attnum, inherited));
+}
+
+Datum
+pg_restore_attribute_stats(PG_FUNCTION_ARGS)
+{
+	LOCAL_FCINFO(positional_fcinfo, NUM_ATTRIBUTE_STATS_ARGS);
+
+	InitFunctionCallInfoData(*positional_fcinfo, NULL, NUM_ATTRIBUTE_STATS_ARGS,
+							 InvalidOid, NULL, NULL);
+
+	fill_fcinfo_from_arg_pairs(fcinfo, positional_fcinfo, attarginfo, WARNING);
+	PG_RETURN_BOOL(attribute_statistics_update(positional_fcinfo, WARNING));
+}
diff --git a/src/backend/statistics/meson.build b/src/backend/statistics/meson.build
index 73b29a3d50a..849df3bf323 100644
--- a/src/backend/statistics/meson.build
+++ b/src/backend/statistics/meson.build
@@ -3,6 +3,7 @@
 backend_sources += files(
   'dependencies.c',
   'extended_stats.c',
+  'import_stats.c',
   'mcv.c',
   'mvdistinct.c',
 )
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 4abc6d95262..80e224beb18 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12255,4 +12255,52 @@
   proargnames => '{summarized_tli,summarized_lsn,pending_lsn,summarizer_pid}',
   prosrc => 'pg_get_wal_summarizer_state' },
 
+# Statistics Import
+{ oid => '8048',
+  descr => 'set statistics on relation',
+  proname => 'pg_set_relation_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'regclass int4 float4 int4',
+  proargnames => '{relation,relpages,reltuples,relallvisible}',
+  prosrc => 'pg_set_relation_stats' },
+{ oid => '8049',
+  descr => 'clear statistics on relation',
+  proname => 'pg_clear_relation_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'regclass',
+  proargnames => '{relation}',
+  prosrc => 'pg_clear_relation_stats' },
+{ oid => '8050',
+  descr => 'restore statistics on relation',
+  proname => 'pg_restore_relation_stats', provolatile => 'v', proisstrict => 'f',
+  provariadic => 'any',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'any',
+  proargnames => '{relation}',
+  proargmodes => '{v}',
+  prosrc => 'pg_restore_relation_stats' },
+{ oid => '8051',
+  descr => 'set statistics on attribute',
+  proname => 'pg_set_attribute_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'regclass name bool float4 int4 float4 text _float4 text float4 text _float4 _float4 text float4 text',
+  proargnames => '{relation,attname,inherited,null_frac,avg_width,n_distinct,most_common_vals,most_common_freqs,histogram_bounds,correlation,most_common_elems,most_common_elem_freqs,elem_count_histogram,range_length_histogram,range_empty_frac,range_bounds_histogram}',
+  prosrc => 'pg_set_attribute_stats' },
+{ oid => '8052',
+  descr => 'clear statistics on attribute',
+  proname => 'pg_clear_attribute_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'regclass name bool',
+  proargnames => '{relation}',
+  prosrc => 'pg_clear_attribute_stats' },
+{ oid => '8053',
+  descr => 'restore statistics on attribute',
+  proname => 'pg_restore_attribute_stats', provolatile => 'v', proisstrict => 'f',
+  provariadic => 'any',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'any',
+  proargnames => '{relation}',
+  proargmodes => '{v}',
+  prosrc => 'pg_restore_attribute_stats' },
+
 ]
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 7f2bf18716d..73d3b541dda 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -127,4 +127,7 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												int nclauses);
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
+extern Datum pg_set_relation_stats(PG_FUNCTION_ARGS);
+extern Datum pg_set_attribute_stats(PG_FUNCTION_ARGS);
+
 #endif							/* STATISTICS_H */
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
new file mode 100644
index 00000000000..5edfb7451b5
--- /dev/null
+++ b/src/test/regress/expected/stats_import.out
@@ -0,0 +1,766 @@
+CREATE SCHEMA stats_import;
+CREATE TYPE stats_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+CREATE TABLE stats_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+-- starting stats
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+        0 |        -1 |             0
+(1 row)
+
+-- error: regclass not found
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 0::Oid,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+ERROR:  could not open relation with OID 0
+-- relpages default
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => NULL::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+ pg_set_relation_stats 
+-----------------------
+ t
+(1 row)
+
+-- reltuples default
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => NULL::real,
+        relallvisible => 4::integer);
+ pg_set_relation_stats 
+-----------------------
+ t
+(1 row)
+
+-- relallvisible default
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => NULL::integer);
+ pg_set_relation_stats 
+-----------------------
+ f
+(1 row)
+
+-- named arguments
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+ pg_set_relation_stats 
+-----------------------
+ f
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       17 |       400 |             4
+(1 row)
+
+-- positional arguments
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        'stats_import.test'::regclass,
+        18::integer,
+        401.0::real,
+        5::integer);
+ pg_set_relation_stats 
+-----------------------
+ t
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       18 |       401 |             5
+(1 row)
+
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => '0'::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  could not open relation with OID 0
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => NULL::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  "relation" cannot be NULL
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => NULL::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  "attname" cannot be NULL
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => NULL::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  "inherited" cannot be NULL
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => NULL::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => NULL::integer,
+    n_distinct => 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => NULL::real);
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT stanullfrac, stawidth, stadistinct
+FROM pg_statistic
+WHERE starelid = 'stats_import.test'::regclass;
+ stanullfrac | stawidth | stadistinct 
+-------------+----------+-------------
+         0.1 |        2 |         0.3
+(1 row)
+
+-- warn: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_freqs => '{0.1,0.2,0.3}'::real[]
+    );
+ERROR:  "most_common_vals" must be specified when "most_common_freqs" is specified
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{1,2,3}'::text
+    );
+ERROR:  "most_common_freqs" must be specified when "most_common_vals" is specified
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2023-09-30,2024-10-31,3}'::text,
+    most_common_freqs => '{0.2,0.1}'::real[]
+    );
+ERROR:  invalid input syntax for type integer: "2023-09-30"
+-- warning: mcv cast failure
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,four,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+ERROR:  invalid input syntax for type integer: "four"
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: histogram elements null value
+-- this generates no warnings, but perhaps it should
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,NULL,3,4}'::text
+    );
+ERROR:  "histogram_bounds" array cannot contain NULL values
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,2,3,4}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 0.5::real);
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |         0.5 |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have mcelem
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{1,3}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+ERROR:  unable to determine element type of attribute "id"
+DETAIL:  Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,two}'::text
+    );
+ERROR:  "most_common_elems_freq" must be specified when "most_common_elems" is specified
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
+    );
+ERROR:  "most_common_elems" must be specified when "most_common_elems_freq" is specified
+-- ok: mcelem
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             | {one,three}       | {0.3,0.2,0.2,0.3,0}    |                      |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ERROR:  unable to determine element type of attribute "id"
+DETAIL:  Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ERROR:  "elem_count_histogram" array cannot contain NULL values
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
+ stats_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             | {one,three}       | {0.3,0.2,0.2,0.3,0}    | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ERROR:  attribute "id" is not a range type
+DETAIL:  Cannot set STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM or STATISTIC_KIND_BOUNDS_HISTOGRAM.
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ERROR:  "range_empty_frac" must be specified when "range_length_histogram" is specified
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real
+    );
+ERROR:  "range_length_histogram" must be specified when "range_empty_frac" is specified
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
+(1 row)
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ERROR:  attribute "id" is not a range type
+DETAIL:  Cannot set STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM or STATISTIC_KIND_BOUNDS_HISTOGRAM.
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
+ stats_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+(1 row)
+
+-- warn: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{"[2,3)","[1,2)","[3,4)"}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[],
+    histogram_bounds => '{"[1,2)","[2,3)","[3,4)","[4,5)"}'::text,
+    correlation => 1.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    range_empty_frac => -0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ERROR:  maximum number of statistics slots exceeded: 6
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
+-- Generate statistics on table with data
+ANALYZE stats_import.test;
+CREATE TABLE stats_import.test_clone ( LIKE stats_import.test );
+CREATE INDEX is_odd_clone ON stats_import.test_clone(((comp).a % 2 = 1));
+--
+-- Copy stats from test to test_clone, and is_odd to is_odd_clone
+--
+SELECT s.schemaname, s.tablename, s.attname, s.inherited
+FROM pg_catalog.pg_stats AS s
+CROSS JOIN LATERAL
+    pg_catalog.pg_set_attribute_stats(
+        relation => ('stats_import.' || s.tablename || '_clone')::regclass::oid,
+        attname => s.attname,
+        inherited => s.inherited,
+        null_frac => s.null_frac,
+        avg_width => s.avg_width,
+        n_distinct => s.n_distinct,
+        most_common_vals => s.most_common_vals::text,
+        most_common_freqs => s.most_common_freqs,
+        histogram_bounds => s.histogram_bounds::text,
+        correlation => s.correlation,
+        most_common_elems => s.most_common_elems::text,
+        most_common_elem_freqs => s.most_common_elem_freqs,
+        elem_count_histogram => s.elem_count_histogram,
+        range_bounds_histogram => s.range_bounds_histogram::text,
+        range_empty_frac => s.range_empty_frac,
+        range_length_histogram => s.range_length_histogram::text) AS r
+WHERE s.schemaname = 'stats_import'
+AND s.tablename IN ('test', 'is_odd')
+ORDER BY s.tablename, s.attname, s.inherited;
+  schemaname  | tablename | attname | inherited 
+--------------+-----------+---------+-----------
+ stats_import | is_odd    | expr    | f
+ stats_import | test      | arange  | f
+ stats_import | test      | comp    | f
+ stats_import | test      | id      | f
+ stats_import | test      | name    | f
+ stats_import | test      | tags    | f
+(6 rows)
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+   relname    | num_stats 
+--------------+-----------
+ is_odd       |         1
+ is_odd_clone |         1
+ test         |         5
+ test_clone   |         5
+(4 rows)
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+DROP SCHEMA stats_import CASCADE;
+NOTICE:  drop cascades to 3 other objects
+DETAIL:  drop cascades to type stats_import.complex_type
+drop cascades to table stats_import.test
+drop cascades to table stats_import.test_clone
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 2429ec2bbaa..85fc85bfa03 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,7 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import
 
 # ----------
 # Load huge amounts of data
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
new file mode 100644
index 00000000000..0582c27146a
--- /dev/null
+++ b/src/test/regress/sql/stats_import.sql
@@ -0,0 +1,623 @@
+CREATE SCHEMA stats_import;
+
+CREATE TYPE stats_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+
+CREATE TABLE stats_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+
+-- starting stats
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+-- error: regclass not found
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 0::Oid,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+
+-- relpages default
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => NULL::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+
+-- reltuples default
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => NULL::real,
+        relallvisible => 4::integer);
+
+-- relallvisible default
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => NULL::integer);
+
+-- named arguments
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+-- positional arguments
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        'stats_import.test'::regclass,
+        18::integer,
+        401.0::real,
+        5::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => '0'::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => NULL::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => NULL::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => NULL::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => NULL::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => NULL::integer,
+    n_distinct => 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => NULL::real);
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+SELECT stanullfrac, stawidth, stadistinct
+FROM pg_statistic
+WHERE starelid = 'stats_import.test'::regclass;
+
+-- warn: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_freqs => '{0.1,0.2,0.3}'::real[]
+    );
+
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{1,2,3}'::text
+    );
+
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2023-09-30,2024-10-31,3}'::text,
+    most_common_freqs => '{0.2,0.1}'::real[]
+    );
+
+-- warning: mcv cast failure
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,four,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: histogram elements null value
+-- this generates no warnings, but perhaps it should
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,NULL,3,4}'::text
+    );
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,2,3,4}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 0.5::real);
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: scalars can't have mcelem
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{1,3}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,two}'::text
+    );
+
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
+    );
+
+-- ok: mcelem
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- warn: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real
+    );
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{"[2,3)","[1,2)","[3,4)"}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[],
+    histogram_bounds => '{"[1,2)","[2,3)","[3,4)","[4,5)"}'::text,
+    correlation => 1.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    range_empty_frac => -0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+
+CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
+
+-- Generate statistics on table with data
+ANALYZE stats_import.test;
+
+CREATE TABLE stats_import.test_clone ( LIKE stats_import.test );
+
+CREATE INDEX is_odd_clone ON stats_import.test_clone(((comp).a % 2 = 1));
+
+--
+-- Copy stats from test to test_clone, and is_odd to is_odd_clone
+--
+SELECT s.schemaname, s.tablename, s.attname, s.inherited
+FROM pg_catalog.pg_stats AS s
+CROSS JOIN LATERAL
+    pg_catalog.pg_set_attribute_stats(
+        relation => ('stats_import.' || s.tablename || '_clone')::regclass::oid,
+        attname => s.attname,
+        inherited => s.inherited,
+        null_frac => s.null_frac,
+        avg_width => s.avg_width,
+        n_distinct => s.n_distinct,
+        most_common_vals => s.most_common_vals::text,
+        most_common_freqs => s.most_common_freqs,
+        histogram_bounds => s.histogram_bounds::text,
+        correlation => s.correlation,
+        most_common_elems => s.most_common_elems::text,
+        most_common_elem_freqs => s.most_common_elem_freqs,
+        elem_count_histogram => s.elem_count_histogram,
+        range_bounds_histogram => s.range_bounds_histogram::text,
+        range_empty_frac => s.range_empty_frac,
+        range_length_histogram => s.range_length_histogram::text) AS r
+WHERE s.schemaname = 'stats_import'
+AND s.tablename IN ('test', 'is_odd')
+ORDER BY s.tablename, s.attname, s.inherited;
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test_clone'::regclass;
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test'::regclass;
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd_clone'::regclass;
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd'::regclass;
+
+DROP SCHEMA stats_import CASCADE;
-- 
2.34.1

#187jian he
jian.universality@gmail.com
In reply to: Jeff Davis (#186)
Re: Statistics Import and Export

On Sat, Aug 24, 2024 at 4:50 AM Jeff Davis <pgsql@j-davis.com> wrote:

I have attached version 28j as one giant patch covering what was
previously 0001-0003. It's a bit rough (tests in particular need some
work), but it implelements the logic to replace only those values
specified rather than the whole tuple.

hi.
I did some review for v28j

git am shows some whitespace error.

+extern Datum pg_set_relation_stats(PG_FUNCTION_ARGS);
+extern Datum pg_set_attribute_stats(PG_FUNCTION_ARGS);
is unnecessary?
+       <entry role="func_table_entry">
+        <para role="func_signature">
+         <indexterm>
+          <primary>pg_set_relation_stats</primary>
+         </indexterm>
+         <function>pg_set_relation_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>
+         <optional>, <parameter>relpages</parameter>
<type>integer</type></optional>
+         <optional>, <parameter>reltuples</parameter>
<type>real</type></optional>
+         <optional>, <parameter>relallvisible</parameter>
<type>integer</type></optional> )
+         <returnvalue>boolean</returnvalue>
+        </para>
+        <para>
+         Updates table-level statistics for the given relation to the
+         specified values. The parameters correspond to columns in <link
+         linkend="catalog-pg-class"><structname>pg_class</structname></link>.
Unspecified
+         or <literal>NULL</literal> values leave the setting
+         unchanged. Returns <literal>true</literal> if a change was made;
+         <literal>false</literal> otherwise.
+        </para>
are these <optional> flags wrong? there is only one function currently:
pg_set_relation_stats(relation regclass, relpages integer, reltuples
real, relallvisible integer)
i think you want
pg_set_relation_stats(relation regclass, relpages integer default
null, reltuples real default null, relallvisible integer default null)
we can add following in src/backend/catalog/system_functions.sql:

select * from pg_set_relation_stats('emp'::regclass);
CREATE OR REPLACE FUNCTION
pg_set_relation_stats(
relation regclass,
relpages integer default null,
reltuples real default null,
relallvisible integer default null)
RETURNS bool
LANGUAGE INTERNAL
CALLED ON NULL INPUT VOLATILE
AS 'pg_set_relation_stats';

typedef enum ...
need to add src/tools/pgindent/typedefs.list

+/*
+ * Check that array argument is one dimensional with no NULLs.
+ *
+ * If not, emit at elevel, and set argument to NULL in fcinfo.
+ */
+static void
+check_arg_array(FunctionCallInfo fcinfo, struct arginfo *arginfo,
+ int argnum, int elevel)
+{
+ ArrayType  *arr;
+
+ if (PG_ARGISNULL(argnum))
+ return;
+
+ arr = DatumGetArrayTypeP(PG_GETARG_DATUM(argnum));
+
+ if (ARR_NDIM(arr) != 1)
+ {
+ ereport(elevel,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("\"%s\" cannot be a multidimensional array",
+ arginfo[argnum].argname)));
+ fcinfo->args[argnum].isnull = true;
+ }
+
+ if (array_contains_nulls(arr))
+ {
+ ereport(elevel,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("\"%s\" array cannot contain NULL values",
+ arginfo[argnum].argname)));
+ fcinfo->args[argnum].isnull = true;
+ }
+}
this part elevel should always be ERROR?
if so, we can just
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),

relation_statistics_update and other functions
may need to check relkind?
since relpages, reltuples, relallvisible not meaning to all of relkind?

#188Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#186)
Re: Statistics Import and Export

I have attached version 28j as one giant patch covering what was
previously 0001-0003. It's a bit rough (tests in particular need some
work), but it implelements the logic to replace only those values
specified rather than the whole tuple.

I like what you did restoring the parameter enums, especially now that they
can be leveraged for the expected type oids data structure.

At least for the interactive "set" variants of the functions, I think
it's an improvement. It feels more natural to just change one stat
without wiping out all the others. I realize a lot of the statistics
depend on each other, but the point is not to replace ANALYZE, the
point is to experiment with planner scenarios. What do others think?

When I first heard that was what you wanted to do, I was very uneasy about
it. The way you implemented it (one function to wipe out/reset all existing
stats, and then the _set_ function works as an upsert) puts my mind at
ease. The things I really wanted to avoid were gaps in the stakind array
(which can't happen as you wrote it) and getting the stakinds out of order
(admittedly that's more a tidiness issue, but pg_statistic before/after
fidelity is kept, so I'm happy).

For the "restore" variants, I'm not sure it matters a lot because the

stats will already be empty. If it does matter, we could pretty easily
define the "restore" variants to wipe out existing stats when loading
the table, though I'm not sure if that's a good thing or not.

I agree, and I'm leaning towards doing the clear, because "restore" to me
implies that what resides there exactly matches what was in the function
call, regardless of what might have been there before. But you're also
right, "restore" is expected to be used on default/missing stats, and the
restore_* call generated is supposed to be comprehensive of all stats that
were there at time of dump/upgrade, so impact would be minimal.

I also made more use of FunctionCallInfo structures to communicate
between functions rather than huge parameter lists. I believe that
reduced the line count substantially, and made it easier to transform
the argument pairs in the "restore" variants into the positional
arguments for the "set" variants.

You certainly did, and I see where it pays off given that _set_ / _restore_
functions are just different ways of ordering the shared internal function
call.

Observation: there is currently no way to delete a stakind, keeping the
rest of the record. It's certainly possible to compose a SQL query that
gets the current values, invokes pg_clear_* and then pg_set_* using the
values that are meant to be kept, and in fact that pattern is how I
imagined the pg_set_* functions would be used when they overwrote
everything in the tuple. So I am fine with going forward with this paradigm.

The code mentions that more explanation should be given for the special
cases (tsvector, etc) and that explanation is basically "this code follows
what the corresponding custom typanalyze() function does". In the future,
it may make sense to have custom typimport() functions for datatypes that
have a custom typanalzye(), which would solve the issue of handling custom
stakinds.

I'll continue to work on this.

p.s. dropping invalid email address from the thread

#189Corey Huinker
corey.huinker@gmail.com
In reply to: jian he (#187)
Re: Statistics Import and Export

git am shows some whitespace error.

Jeff indicated that this was more of a stylistic/clarity reworking. I'll be
handling it again for now.

+extern Datum pg_set_relation_stats(PG_FUNCTION_ARGS);
+extern Datum pg_set_attribute_stats(PG_FUNCTION_ARGS);
is unnecessary?

They're autogenerated from pg_proc.dat. I was (pleasantly) surprised too.

this part elevel should always be ERROR?
if so, we can just

I'm personally dis-inclined to error on any of these things, so I'll be
leaving it as is. I suspect that the proper balance lies between all-ERROR
and all-WARNING, but time will tell which.

relation_statistics_update and other functions
may need to check relkind?
since relpages, reltuples, relallvisible not meaning to all of relkind?

I'm not able to understand either of your questions, can you elaborate on
them?

#190jian he
jian.universality@gmail.com
In reply to: Corey Huinker (#189)
1 attachment(s)
Re: Statistics Import and Export

On Fri, Sep 6, 2024 at 1:34 AM Corey Huinker <corey.huinker@gmail.com> wrote:

this part elevel should always be ERROR?
if so, we can just

I'm personally dis-inclined to error on any of these things, so I'll be leaving it as is. I suspect that the proper balance lies between all-ERROR and all-WARNING, but time will tell which.

somehow, i get it now.

relation_statistics_update and other functions
may need to check relkind?
since relpages, reltuples, relallvisible not meaning to all of relkind?

I'm not able to understand either of your questions, can you elaborate on them?

Please check my attached changes.
also see the attached cf-bot commit message.

1. make sure these three functions: 'pg_set_relation_stats',
'pg_restore_relation_stats','pg_clear_relation_stats' proisstrict to true.
because in
pg_class catalog, these three attributes (relpages, reltuples, relallvisible) is
marked as not null. updating it to null will violate these constraints.
tom also mention this at [

2.refactor relation_statistics_update. first sanity check first argument
("relation").
not all kinds of relation can pass on
relation_statistics_update, for example view. so do the sanity check.
also do sanity check for the remaining 3 arguments.
if not ok, ereport(elevel...), return false immediately.

3.add some tests for partitioned table, view, and materialized view.

4. minor sanity check output of "attnum = get_attnum(reloid,
NameStr(*attname));"

5.
create table t(a int, b int);
alter table t drop column b;
SELECT pg_catalog.pg_set_attribute_stats(
relation => 't'::regclass,
attname => 'b'::name,
inherited => false::boolean,
null_frac => 0.1::real,
avg_width => 2::integer,
n_distinct => 0.3::real);

ERROR: attribute 0 of relation with OID 34316 does not exist
The error message is not good, i think.
Also, in this case, I think we may need soft errors.
instead of returning ERROR, make it return FALSE would be more ok.

6. there are no "inherited => true::boolean,"
tests for pg_set_attribute_stats.
aslo there are no partitioned table related tests on stats_import.sql.
I think we should add some.

7. the doc output, functions-admin.html, there are 4 same warnings.
Maybe one is enough?

8. lock_check_privileges function issue.
------------------------------------------------
--asume there is a superuser jian
create role alice NOSUPERUSER LOGIN;
create role bob NOSUPERUSER LOGIN;
create role carol NOSUPERUSER LOGIN;
alter database test owner to alice
GRANT CONNECT, CREATE on database test to bob;
\c test bob
create schema one;
create table one.t(a int);
create table one.t1(a int);
set session AUTHORIZATION; --switch to superuser.
alter table one.t1 owner to carol;
\c test alice
--now current database owner alice cannot do ANYTHING WITH table one.t1,
like ANALYZE, SELECT, INSERT, MAINTAIN etc.

so i think your relation_statistics_update->lock_check_privileges part is wrong?
also the doc:
"The caller must have the MAINTAIN privilege on the table or be the
owner of the database."
should be
"The caller must have the MAINTAIN privilege on the table or be the
owner of the table"
?

Attachments:

v28-0001-minor-refactor-based-on-28j.no-cfbotapplication/octet-stream; name=v28-0001-minor-refactor-based-on-28j.no-cfbotDownload
From ad16b462761eb56f47840237bfb21f9fa5606afa Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Sun, 8 Sep 2024 00:23:06 +0800
Subject: [PATCH v28 1/1] minor refactor based on 28j

1. make sure these three functions: 'pg_set_relation_stats',
'pg_restore_relation_stats','pg_clear_relation_stats' proisstrict to true.
because in pg_class catalog, these three attributes (relpages, reltuples,
relallvisible) is marked as not null. updating it to null will violate these
constraints.

2. refactor relation_statistics_update. first sanity check first argument
("relation").  not all kinds of relation can pass on relation_statistics_update,
so do the sanity check.  also do sanity check for the remaining 3 arguments.  if
not ok, bail it, return false immediately.

3. add some tests for partitioned table, view, and materialized view.

4. minor sanity check output of "attnum = get_attnum(reloid,
NameStr(*attname));"

5.
    create table t(a int, b int);
    alter table t drop column b;
    SELECT pg_catalog.pg_set_attribute_stats(
        relation => 't'::regclass, attname => 'b'::name,
        inherited => false::boolean, null_frac => 0.1::real,
        avg_width => 2::integer,
        n_distinct => 0.3::real);
output
ERROR:  attribute 0 of relation with OID 34316 does not exist

The error message is not good, i think.
Also, in this case, I think we may need soft errors.
instead of returning ERROR, make it return FALSE would be more ok.
---
 src/backend/statistics/import_stats.c      | 215 ++++++++++++++-------
 src/include/catalog/pg_proc.dat            |   6 +-
 src/test/regress/expected/stats_import.out |  81 +++++++-
 src/test/regress/sql/stats_import.sql      |  43 ++++-
 4 files changed, 258 insertions(+), 87 deletions(-)

diff --git a/src/backend/statistics/import_stats.c b/src/backend/statistics/import_stats.c
index ea87a8e849..9fa3bdda6f 100644
--- a/src/backend/statistics/import_stats.c
+++ b/src/backend/statistics/import_stats.c
@@ -148,17 +148,108 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	Relation		crel;
 	HeapTuple		ctup;
 	Form_pg_class	pgcform;
+	int32 			relpages;
+	float 			reltuples;
+	int32 			relallvisible;
+	Relation		rel;
 	int				replaces[3]	= {0};
 	Datum			values[3]	= {0};
 	bool			nulls[3]	= {false, false, false};
-	int				ncols		= 0;
 	TupleDesc		tupdesc;
 	HeapTuple		newtup;
 
-
-	check_required_arg(fcinfo, relarginfo, RELATION_ARG);
 	reloid = PG_GETARG_OID(RELATION_ARG);
+	relpages = PG_GETARG_INT32(RELPAGES_ARG);
+	reltuples = PG_GETARG_FLOAT4(RELTUPLES_ARG);
+	relallvisible = PG_GETARG_INT32(RELALLVISIBLE_ARG);
 
+	/* first argument (input relation) sanity check */
+	rel = relation_open(reloid, AccessShareLock);
+
+	/*
+	 * Silently ignore tables that are temp tables of other backends trying to
+	 * set these is rather pointless, since their contents are probably not
+	 * up-to-date on disk.
+	 */
+	if (RELATION_IS_OTHER_TEMP(rel))
+	{
+		relation_close(rel, AccessShareLock);
+		return false;
+	}
+
+	/* we don't manually set table statistics for system tables */
+	if (IsCatalogNamespace(rel->rd_rel->relnamespace) ||
+		IsToastNamespace(rel->rd_rel->relnamespace))
+	{
+		relation_close(rel, AccessShareLock);
+		return false;
+	}
+
+	if (rel->rd_rel->relkind == RELKIND_RELATION ||
+		rel->rd_rel->relkind == RELKIND_MATVIEW)
+	{
+		/* this hould be fine.*/
+	}
+	else if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* TODO is foreign table OK? */
+		ereport(WARNING,
+				(errmsg("skipping \"%s\" --- cannot update statistic on foreign table",
+						RelationGetRelationName(rel))));
+		relation_close(rel, AccessShareLock);
+		return false;
+	}
+	else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+	{
+		ereport(WARNING,
+				(errmsg("skipping \"%s\" --- cannot update statistic on partitioned table",
+						RelationGetRelationName(rel))));
+		relation_close(rel, AccessShareLock);
+		return false;
+	}
+	else
+	{
+		ereport(WARNING,
+				(errmsg("skipping \"%s\" --- cannot update statistic non-tables or special system tables",
+						RelationGetRelationName(rel))));
+		relation_close(rel, AccessShareLock);
+		return false;
+	}
+	relation_close(rel, AccessShareLock);
+
+	/* input argument relpages, reltuples, relallvisible sanity check. */
+	if (relpages < 0)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relpages cannot be < 0")));
+		return false;
+	}
+
+	if (reltuples < -1.0)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("reltuples cannot be < -1.0")));
+		return false;
+	}
+
+	if (relallvisible < 0)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relallvisible cannot be < 0")));
+		return false;
+	}
+
+	/* ensure relpages strictly larger than relallvisible */
+	if (relallvisible > relpages)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relallvisible cannot larger than relpages")));
+		return false;
+	}
 	lock_check_privileges(reloid);
 
 	/*
@@ -180,78 +271,24 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 
 	pgcform = (Form_pg_class) GETSTRUCT(ctup);
 
-	/* relpages */
-	if (!PG_ARGISNULL(RELPAGES_ARG))
+	if (relpages != pgcform->relpages ||
+		reltuples != pgcform->reltuples ||
+		relallvisible != pgcform->relallvisible)
 	{
-		int32 relpages = PG_GETARG_INT32(RELPAGES_ARG);
-
-		if (relpages < 0)
-		{
-			ereport(elevel,
-					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("relpages cannot be < 0")));
-			table_close(crel, RowExclusiveLock);
-			return false;
-		}
-
-		if (relpages != pgcform->relpages)
-		{
-			replaces[ncols] = Anum_pg_class_relpages;
-			values[ncols] = Int32GetDatum(relpages);
-			ncols++;
-		}
+		replaces[0] = Anum_pg_class_relpages;
+		replaces[1] = Anum_pg_class_reltuples;
+		replaces[2] = Anum_pg_class_relallvisible;
+		values[0] = Int32GetDatum(relpages);
+		values[1] = Float4GetDatum(reltuples);
+		values[2] = Int32GetDatum(relallvisible);
 	}
-
-	if (!PG_ARGISNULL(RELTUPLES_ARG))
-	{
-		float reltuples = PG_GETARG_FLOAT4(RELTUPLES_ARG);
-
-		if (reltuples < -1.0)
-		{
-			ereport(elevel,
-					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("reltuples cannot be < -1.0")));
-			table_close(crel, RowExclusiveLock);
-			return false;
-		}
-
-		if (reltuples != pgcform->reltuples)
-		{
-			replaces[ncols] = Anum_pg_class_reltuples;
-			values[ncols] = Float4GetDatum(reltuples);
-			ncols++;
-		}
-	}
-
-	if (!PG_ARGISNULL(RELALLVISIBLE_ARG))
-	{
-		int32 relallvisible = PG_GETARG_INT32(RELALLVISIBLE_ARG);
-
-		if (relallvisible < 0)
-		{
-			ereport(elevel,
-					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("relallvisible cannot be < 0")));
-			table_close(crel, RowExclusiveLock);
-			return false;
-		}
-
-		if (relallvisible != pgcform->relallvisible)
-		{
-			replaces[ncols] = Anum_pg_class_relallvisible;
-			values[ncols] = Int32GetDatum(relallvisible);
-			ncols++;
-		}
-	}
-
-	/* only update pg_class if there is a meaningful change */
-	if (ncols == 0)
+	else
 	{
 		table_close(crel, RowExclusiveLock);
 		return false;
 	}
 
-	newtup = heap_modify_tuple_by_cols(ctup, tupdesc, ncols, replaces, values,
+	newtup = heap_modify_tuple_by_cols(ctup, tupdesc, 3, replaces, values,
 									   nulls);
 
 	CatalogTupleUpdate(crel, &newtup->t_self, newtup);
@@ -312,6 +349,23 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	attname = PG_GETARG_NAME(ATTNAME_ARG);
 	attnum = get_attnum(reloid, NameStr(*attname));
 
+	if (attnum < 0)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("statistics update on system columns is not supported")));
+		return false;
+	}
+
+	if (attnum == 0)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("attribute \"%s\" of relation OID %d does not exist",
+							NameStr(*attname), reloid)));
+		return false;
+	}
+
 	if (PG_ARGISNULL(INHERITED_ARG))
 	{
 		ereport(elevel,
@@ -440,7 +494,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 
 	/*
 	 * STATISTIC_KIND_MCV
-	 * 
+	 *
 	 * Convert most_common_vals from text to anyarray, where the element type
 	 * is the attribute type, and store in stavalues. Store most_common_freqs
 	 * in stanumbers.
@@ -689,8 +743,8 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
 	if (!HeapTupleIsValid(atup))
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
-				 errmsg("attribute %d of relation with OID %u does not exist",
-						attnum, reloid)));
+				 errmsg("attribute %d of relation \"%s\" does not exist",
+						attnum, RelationGetRelationName(rel))));
 
 	attr = (Form_pg_attribute) GETSTRUCT(atup);
 
@@ -736,7 +790,7 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
 	*eq_opr = typcache->eq_opr;
 	*lt_opr = typcache->lt_opr;
 
-	/* TODO: explain special case for tsvector */ 
+	/* TODO: explain special case for tsvector */
 	if (*atttypid == TSVECTOROID)
 		*atttypcoll = DEFAULT_COLLATION_OID;
 
@@ -1242,6 +1296,23 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 	attname = PG_GETARG_NAME(ATTNAME_ARG);
 	attnum = get_attnum(reloid, NameStr(*attname));
 
+	if (attnum < 0)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("statistics update on system columns is not supported")));
+		PG_RETURN_BOOL(false);
+	}
+
+	if (attnum == 0)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("attribute \"%s\" of relation OID %d does not exist",
+				 		 NameStr(*attname), reloid)));
+		PG_RETURN_BOOL(false);
+	}
+
 	check_required_arg(fcinfo, attarginfo, INHERITED_ARG);
 	inherited = PG_GETARG_BOOL(INHERITED_ARG);
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 80e224beb1..d1cf035f18 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12258,21 +12258,21 @@
 # Statistics Import
 { oid => '8048',
   descr => 'set statistics on relation',
-  proname => 'pg_set_relation_stats', provolatile => 'v', proisstrict => 'f',
+  proname => 'pg_set_relation_stats', provolatile => 'v', proisstrict => 't',
   proparallel => 'u', prorettype => 'bool',
   proargtypes => 'regclass int4 float4 int4',
   proargnames => '{relation,relpages,reltuples,relallvisible}',
   prosrc => 'pg_set_relation_stats' },
 { oid => '8049',
   descr => 'clear statistics on relation',
-  proname => 'pg_clear_relation_stats', provolatile => 'v', proisstrict => 'f',
+  proname => 'pg_clear_relation_stats', provolatile => 'v', proisstrict => 't',
   proparallel => 'u', prorettype => 'bool',
   proargtypes => 'regclass',
   proargnames => '{relation}',
   prosrc => 'pg_clear_relation_stats' },
 { oid => '8050',
   descr => 'restore statistics on relation',
-  proname => 'pg_restore_relation_stats', provolatile => 'v', proisstrict => 'f',
+  proname => 'pg_restore_relation_stats', provolatile => 'v', proisstrict => 't',
   provariadic => 'any',
   proparallel => 'u', prorettype => 'bool',
   proargtypes => 'any',
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index 5edfb7451b..2af1b064c0 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -12,14 +12,59 @@ CREATE TABLE stats_import.test(
     arange int4range,
     tags text[]
 );
+CREATE TABLE stats_import.part(
+    id INTEGER generated by default as identity ,
+    name text,
+	CONSTRAINT part_pkey PRIMARY KEY (id)
+) partition by range (id);
+CREATE TABLE stats_import.part1 PARTITION OF stats_import.part FOR VALUES FROM (1) TO (10);
+CREATE view stats_import.partv as select id, name from stats_import.part;
+CREATE materialized view stats_import.partmv as select id, name from stats_import.part;
+--false for partitioned table
+SELECT  pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.part'::regclass,
+        relpages => 11::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+WARNING:  skipping "part" --- cannot update statistic on partitioned table
+ pg_set_relation_stats 
+-----------------------
+ f
+(1 row)
+
+--false for partitioned index
+SELECT  pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.part_pkey'::regclass,
+        relpages => 11::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+WARNING:  skipping "part_pkey" --- cannot update statistic non-tables or special system tables
+ pg_set_relation_stats 
+-----------------------
+ f
+(1 row)
+
+--false for view
+SELECT  pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.partv'::regclass,
+        relpages => 11::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+WARNING:  skipping "partv" --- cannot update statistic non-tables or special system tables
+ pg_set_relation_stats 
+-----------------------
+ f
+(1 row)
+
 -- starting stats
 SELECT relpages, reltuples, relallvisible
 FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
+WHERE oid in ('stats_import.test'::regclass, 'stats_import.partmv'::regclass);
  relpages | reltuples | relallvisible 
 ----------+-----------+---------------
         0 |        -1 |             0
-(1 row)
+        0 |        -1 |             0
+(2 rows)
 
 -- error: regclass not found
 SELECT
@@ -38,7 +83,7 @@ SELECT
         relallvisible => 4::integer);
  pg_set_relation_stats 
 -----------------------
- t
+ 
 (1 row)
 
 -- reltuples default
@@ -50,7 +95,7 @@ SELECT
         relallvisible => 4::integer);
  pg_set_relation_stats 
 -----------------------
- t
+ 
 (1 row)
 
 -- relallvisible default
@@ -62,7 +107,7 @@ SELECT
         relallvisible => NULL::integer);
  pg_set_relation_stats 
 -----------------------
- f
+ 
 (1 row)
 
 -- named arguments
@@ -74,16 +119,29 @@ SELECT
         relallvisible => 4::integer);
  pg_set_relation_stats 
 -----------------------
- f
+ t
+(1 row)
+
+-- named arguments for materialized view
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.partmv'::regclass,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+ pg_set_relation_stats 
+-----------------------
+ t
 (1 row)
 
 SELECT relpages, reltuples, relallvisible
 FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
+WHERE oid in ('stats_import.test'::regclass, 'stats_import.partmv'::regclass);
  relpages | reltuples | relallvisible 
 ----------+-----------+---------------
        17 |       400 |             4
-(1 row)
+       17 |       400 |             4
+(2 rows)
 
 -- positional arguments
 SELECT
@@ -113,7 +171,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
     null_frac => 0.1::real,
     avg_width => 2::integer,
     n_distinct => 0.3::real);
-ERROR:  could not open relation with OID 0
+ERROR:  attribute "id" of relation OID 0 does not exist
 -- error: relation null
 SELECT pg_catalog.pg_set_attribute_stats(
     relation => NULL::oid,
@@ -760,7 +818,10 @@ WHERE s.starelid = 'stats_import.is_odd'::regclass;
 (0 rows)
 
 DROP SCHEMA stats_import CASCADE;
-NOTICE:  drop cascades to 3 other objects
+NOTICE:  drop cascades to 6 other objects
 DETAIL:  drop cascades to type stats_import.complex_type
 drop cascades to table stats_import.test
+drop cascades to table stats_import.part
+drop cascades to view stats_import.partv
+drop cascades to materialized view stats_import.partmv
 drop cascades to table stats_import.test_clone
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index 0582c27146..79e6a9c6b4 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -15,10 +15,41 @@ CREATE TABLE stats_import.test(
     tags text[]
 );
 
+CREATE TABLE stats_import.part(
+    id INTEGER generated by default as identity ,
+    name text,
+	CONSTRAINT part_pkey PRIMARY KEY (id)
+) partition by range (id);
+
+CREATE TABLE stats_import.part1 PARTITION OF stats_import.part FOR VALUES FROM (1) TO (10);
+CREATE view stats_import.partv as select id, name from stats_import.part;
+CREATE materialized view stats_import.partmv as select id, name from stats_import.part;
+
+--false for partitioned table
+SELECT  pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.part'::regclass,
+        relpages => 11::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+
+--false for partitioned index
+SELECT  pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.part_pkey'::regclass,
+        relpages => 11::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+
+--false for view
+SELECT  pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.partv'::regclass,
+        relpages => 11::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+
 -- starting stats
 SELECT relpages, reltuples, relallvisible
 FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
+WHERE oid in ('stats_import.test'::regclass, 'stats_import.partmv'::regclass);
 
 -- error: regclass not found
 SELECT
@@ -60,9 +91,17 @@ SELECT
         reltuples => 400.0::real,
         relallvisible => 4::integer);
 
+-- named arguments for materialized view
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.partmv'::regclass,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+
 SELECT relpages, reltuples, relallvisible
 FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
+WHERE oid in ('stats_import.test'::regclass, 'stats_import.partmv'::regclass);
 
 -- positional arguments
 SELECT
-- 
2.34.1

#191Corey Huinker
corey.huinker@gmail.com
In reply to: jian he (#190)
7 attachment(s)
Re: Statistics Import and Export

Please check my attached changes.
also see the attached cf-bot commit message.

1. make sure these three functions: 'pg_set_relation_stats',
'pg_restore_relation_stats','pg_clear_relation_stats' proisstrict to true.
because in
pg_class catalog, these three attributes (relpages, reltuples,
relallvisible) is
marked as not null. updating it to null will violate these constraints.
tom also mention this at [

Things have changed a bit since then, and the purpose of the functions has
changed, so the considerations are now different. The function signature
could change in the future as new pg_class stats are added, and it might
not still be strict.

2.refactor relation_statistics_update. first sanity check first argument
("relation").
not all kinds of relation can pass on
relation_statistics_update, for example view. so do the sanity check.
also do sanity check for the remaining 3 arguments.
if not ok, ereport(elevel...), return false immediately.

I have added checks for non-stats-having pg_class types.

3.add some tests for partitioned table, view, and materialized view.

We can do that, but they're all just relations, the underlying mechanism is
the same. All we'd be testing is that there is no check actively preventing
statistics import for those types.

4. minor sanity check output of "attnum = get_attnum(reloid,
NameStr(*attname));"

While this check makes sense, it falls into the same category as the sanity
checks mentioned in #2. Not against it, but others have found value in just
allowing these things.

5.
create table t(a int, b int);
alter table t drop column b;
SELECT pg_catalog.pg_set_attribute_stats(
relation => 't'::regclass,
attname => 'b'::name,
inherited => false::boolean,
null_frac => 0.1::real,
avg_width => 2::integer,
n_distinct => 0.3::real);

ERROR: attribute 0 of relation with OID 34316 does not exist
The error message is not good, i think.
Also, in this case, I think we may need soft errors.
instead of returning ERROR, make it return FALSE would be more ok.

I agree that we can extract the name of the oid for a better error message.
Added.

The ERROR vs WARNING debate is ongoing.

6. there are no "inherited => true::boolean,"
tests for pg_set_attribute_stats.
aslo there are no partitioned table related tests on stats_import.sql.
I think we should add some.

There aren't any, but that does get tested in the pg_upgrade test.

7. the doc output, functions-admin.html, there are 4 same warnings.
Maybe one is enough?

Perhaps, if we had a good place to put that unified message.

8. lock_check_privileges function issue.
------------------------------------------------
--asume there is a superuser jian
create role alice NOSUPERUSER LOGIN;
create role bob NOSUPERUSER LOGIN;
create role carol NOSUPERUSER LOGIN;
alter database test owner to alice
GRANT CONNECT, CREATE on database test to bob;
\c test bob
create schema one;
create table one.t(a int);
create table one.t1(a int);
set session AUTHORIZATION; --switch to superuser.
alter table one.t1 owner to carol;
\c test alice
--now current database owner alice cannot do ANYTHING WITH table one.t1,
like ANALYZE, SELECT, INSERT, MAINTAIN etc.

Interesting.

I've taken most of Jeff's work, reincorporated it into roughly the same
patch structure as before, and am posting it now.

Highlights:

- import_stats.c is broken up into stats_utils.c, relation_stats.c, and
attribute_stats.c. This is done in light of the existence of
extended_stats.c, and the fact that we will have to eventually add stats
import to extended stats.
- Many of Jian's suggestions were accepted.
- Reorganized test structure to leverage pg_clear_* functions as a way to
cleanse the stats palette between pg_set* function tests and pg_restore*
function tests.
- Rebased up to 95d6e9af07d2e5af2fdd272e72b5b552bad3ea0a on master, which
incorporates Nathan's recent work on pg_upgrade.

Attachments:

v29-0003-Add-relkind-check-to-stats_lock_check_privileges.patchtext/x-patch; charset=US-ASCII; name=v29-0003-Add-relkind-check-to-stats_lock_check_privileges.patchDownload
From 4dec197062fa2d4676de902b7c0f7a6c7e096acc Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Mon, 16 Sep 2024 05:58:23 -0400
Subject: [PATCH v29 3/7] Add relkind check to stats_lock_check_privileges().

Prevent statistics modificaton functions from operating on relation
kinds that cannot store statistics. This includes all of the kinds that
can be ANALYZEd, plus index types.
---
 src/backend/statistics/stats_utils.c       | 14 ++++++++++++++
 src/test/regress/expected/stats_import.out | 15 ++++++++++++++-
 src/test/regress/sql/stats_import.sql      | 10 ++++++++++
 3 files changed, 38 insertions(+), 1 deletion(-)

diff --git a/src/backend/statistics/stats_utils.c b/src/backend/statistics/stats_utils.c
index f51144d4ac..4780e98c17 100644
--- a/src/backend/statistics/stats_utils.c
+++ b/src/backend/statistics/stats_utils.c
@@ -135,6 +135,20 @@ void
 stats_lock_check_privileges(Oid reloid)
 {
 	Relation	rel = relation_open(reloid, ShareUpdateExclusiveLock);
+	const char	relkind = rel->rd_rel->relkind;
+
+	/* All of the types that can be used with ANALYZE, plus indexes */
+	if (relkind != RELKIND_RELATION &&
+		relkind != RELKIND_INDEX &&
+		relkind != RELKIND_MATVIEW &&
+		relkind != RELKIND_FOREIGN_TABLE &&
+		relkind != RELKIND_PARTITIONED_TABLE &&
+		relkind != RELKIND_PARTITIONED_INDEX)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("cannot modify statistics for relations of kind '%c'", relkind)));
+	}
 
 	if (rel->rd_rel->relisshared)
 		ereport(ERROR,
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index 951dd6a10e..61223949f6 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -122,7 +122,20 @@ WHERE oid = 'stats_import.test'::regclass;
         0 |        -1 |             0
 (1 row)
 
+-- invalid relkinds for statistics
+CREATE SEQUENCE stats_import.testseq;
+CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
+SELECT
+    pg_catalog.pg_clear_relation_stats(
+        'stats_import.testseq'::regclass);
+ERROR:  cannot modify statistics for relations of kind 'S'
+SELECT
+    pg_catalog.pg_clear_relation_stats(
+        'stats_import.testview'::regclass);
+ERROR:  cannot modify statistics for relations of kind 'v'
 DROP SCHEMA stats_import CASCADE;
-NOTICE:  drop cascades to 2 other objects
+NOTICE:  drop cascades to 4 other objects
 DETAIL:  drop cascades to type stats_import.complex_type
 drop cascades to table stats_import.test
+drop cascades to sequence stats_import.testseq
+drop cascades to view stats_import.testview
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index 40199dd453..3e9f6d9124 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -85,4 +85,14 @@ SELECT relpages, reltuples, relallvisible
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
+-- invalid relkinds for statistics
+CREATE SEQUENCE stats_import.testseq;
+CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
+SELECT
+    pg_catalog.pg_clear_relation_stats(
+        'stats_import.testseq'::regclass);
+SELECT
+    pg_catalog.pg_clear_relation_stats(
+        'stats_import.testview'::regclass);
+
 DROP SCHEMA stats_import CASCADE;
-- 
2.46.0

v29-0001-Create-statistics-manipulation-utility-functions.patchtext/x-patch; charset=US-ASCII; name=v29-0001-Create-statistics-manipulation-utility-functions.patchDownload
From 4bd77f59b1953ea419230b861b3632dc534bf0e2 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Mon, 16 Sep 2024 03:11:47 -0400
Subject: [PATCH v29 1/7] Create statistics manipulation utility functions.

Creates the following functions, all of which are in support of the
manipulation (import, deletion, etc) of statistics of all kinds
(relation, attribute, extended).

- stats_check_required_arg()

Ensure that a given argument exist.

- stats_check_arg_type():

Check that a given argument is of the expected type.

- stats_check_arg_array():

Check that an argument is a one-dimensional array with no NULL values.

- stats_check_arg_pair():

Check that a given arg pair is either both present and or both missing,
and in the event of a mismatch disable the stat that is present.

- stats_lock_check_privileges():

Ensure that the caller has the permissions to modify statistics for the
given relation.
---
 src/include/statistics/stats_utils.h |  36 +++++++
 src/backend/statistics/Makefile      |   3 +-
 src/backend/statistics/meson.build   |   1 +
 src/backend/statistics/stats_utils.c | 156 +++++++++++++++++++++++++++
 4 files changed, 195 insertions(+), 1 deletion(-)
 create mode 100644 src/include/statistics/stats_utils.h
 create mode 100644 src/backend/statistics/stats_utils.c

diff --git a/src/include/statistics/stats_utils.h b/src/include/statistics/stats_utils.h
new file mode 100644
index 0000000000..a6926ce79a
--- /dev/null
+++ b/src/include/statistics/stats_utils.h
@@ -0,0 +1,36 @@
+/*-------------------------------------------------------------------------
+ *
+ * statistics.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.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef STATS_UTILS_H
+#define STATS_UTILS_H
+
+#include "fmgr.h"
+#include "postgres_ext.h"
+
+typedef struct StatsArgInfo
+{
+	const char *argname;
+	Oid			argtype;
+} StatsArgInfo;
+
+extern void stats_check_required_arg(FunctionCallInfo fcinfo,
+									 StatsArgInfo *arginfo, int argnum);
+extern bool stats_check_arg_type(const char *argname, Oid argtype,
+								 Oid expectedtype, int elevel);
+extern void stats_check_arg_array(FunctionCallInfo fcinfo, StatsArgInfo *arginfo,
+								  int argnum, int elevel);
+extern void stats_check_arg_pair(FunctionCallInfo fcinfo, StatsArgInfo *arginfo,
+								 int argnum1, int argnum2, int elevel);
+
+extern void stats_lock_check_privileges(Oid reloid);
+
+#endif							/* STATS_UTILS_H */
diff --git a/src/backend/statistics/Makefile b/src/backend/statistics/Makefile
index 89cf8c2797..3ffc8f38e6 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 \
+	stats_utils.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/statistics/meson.build b/src/backend/statistics/meson.build
index 73b29a3d50..74c5dc6afa 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',
+  'stats_utils.c'
 )
diff --git a/src/backend/statistics/stats_utils.c b/src/backend/statistics/stats_utils.c
new file mode 100644
index 0000000000..f51144d4ac
--- /dev/null
+++ b/src/backend/statistics/stats_utils.c
@@ -0,0 +1,156 @@
+/*-------------------------------------------------------------------------
+ * stats_utils.c
+ *
+ *	  PostgreSQL statistics manipulation utilities.
+ *
+ * Code supporting the direct manipulation of statistics.
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *       src/backend/statistics/stats_privs.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/relation.h"
+#include "catalog/pg_database.h"
+#include "miscadmin.h"
+#include "statistics/stats_utils.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/acl.h"
+#include "utils/rel.h"
+
+/*
+ * Ensure that a given argument matched the expected type.
+ */
+bool
+stats_check_arg_type(const char *argname, Oid argtype, Oid expectedtype, int elevel)
+{
+	if (argtype != expectedtype)
+	{
+		ereport(elevel,
+				(errmsg("argument \"%s\" has type \"%s\", expected type \"%s\"",
+						argname, format_type_be(argtype),
+						format_type_be(expectedtype))));
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Ensure that a given argument is not null
+ */
+void
+stats_check_required_arg(FunctionCallInfo fcinfo, StatsArgInfo *arginfo,
+				   int argnum)
+{
+	if (PG_ARGISNULL(argnum))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("\"%s\" cannot be NULL",
+						arginfo[argnum].argname)));
+}
+
+/*
+ * Check that array argument is one dimensional with no NULLs.
+ *
+ * If not, emit at elevel, and set argument to NULL in fcinfo.
+ */
+void
+stats_check_arg_array(FunctionCallInfo fcinfo, StatsArgInfo *arginfo,
+				int argnum, int elevel)
+{
+	ArrayType  *arr;
+
+	if (PG_ARGISNULL(argnum))
+		return;
+
+	arr = DatumGetArrayTypeP(PG_GETARG_DATUM(argnum));
+
+	if (ARR_NDIM(arr) != 1)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("\"%s\" cannot be a multidimensional array",
+						arginfo[argnum].argname)));
+		fcinfo->args[argnum].isnull = true;
+	}
+
+	if (array_contains_nulls(arr))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("\"%s\" array cannot contain NULL values",
+						arginfo[argnum].argname)));
+		fcinfo->args[argnum].isnull = true;
+	}
+}
+
+/*
+ * Enforce parameter pairs that must be specified together for a particular
+ * stakind, such as most_common_vals and most_common_freqs for
+ * STATISTIC_KIND_MCV. If one is NULL and the other is not, emit at elevel,
+ * and ignore the stakind by setting both to NULL in fcinfo.
+ */
+void
+stats_check_arg_pair(FunctionCallInfo fcinfo, StatsArgInfo *arginfo,
+			   int argnum1, int argnum2, int elevel)
+{
+	if (PG_ARGISNULL(argnum1) && !PG_ARGISNULL(argnum2))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("\"%s\" must be specified when \"%s\" is specified",
+						arginfo[argnum1].argname,
+						arginfo[argnum2].argname)));
+		fcinfo->args[argnum2].isnull = true;
+	}
+	if (!PG_ARGISNULL(argnum1) && PG_ARGISNULL(argnum2))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("\"%s\" must be specified when \"%s\" is specified",
+						arginfo[argnum2].argname,
+						arginfo[argnum1].argname)));
+		fcinfo->args[argnum1].isnull = true;
+	}
+}
+
+/*
+ * Lock relation in ShareUpdateExclusive mode, check privileges, and close the
+ * relation (but retain the lock).
+ *
+ * A role has privileges to set statistics on the relation if any of the
+ * following are true:
+ *   - the role owns the current database and the relation is not shared
+ *   - the role has the MAINTAIN privilege on the relation
+ */
+void
+stats_lock_check_privileges(Oid reloid)
+{
+	Relation	rel = relation_open(reloid, ShareUpdateExclusiveLock);
+
+	if (rel->rd_rel->relisshared)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("cannot modify statistics for shared relation")));
+
+	if (!object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId()))
+	{
+		AclResult aclresult = pg_class_aclcheck(RelationGetRelid(rel),
+												GetUserId(),
+												ACL_MAINTAIN);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult,
+						   get_relkind_objtype(rel->rd_rel->relkind),
+						   NameStr(rel->rd_rel->relname));
+	}
+
+	relation_close(rel, NoLock);
+}
-- 
2.46.0

v29-0005-Create-functions-pg_restore_relation_stats-pg_re.patchtext/x-patch; charset=US-ASCII; name=v29-0005-Create-functions-pg_restore_relation_stats-pg_re.patchDownload
From 77b74acf89bb3b91ec3f3603b099034ea341a5af Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Tue, 17 Sep 2024 01:57:52 -0400
Subject: [PATCH v29 5/7] Create functions pg_restore_relation_stats,
 pg_restore_attribute_stats

These functions are variadic requivalents of the pg_set_*_stats()
functions, which have a function signature each stat getting its own
defined parameter (or parameter pair, as the case may be).

Such a rigid function signature would make future compability difficult,
and future compatibility is just what pg_dump and pg_upgrade need.

Instead, these functions have all input arguments put into a variable
argument list organized in name-value pairs. The leading or "keyword"
parameters must all be of type text and the string must exactly
correspond to the name of a statistics parameter in the corresponding
pg_set_X_stats function. The trailing or "value" parameter must be of
the type expected by the same-named parameter in the pg_set_X_stats
function. Names that do not match a parameter name and types that do not
match the expected type will emit a warning and be ignored.

The intention of these functions is to be used in pg_dump/pg_restore and
pg_upgrade to allow the user to avoid having to run vacuumdb
--analyze-in-stages after an upgrade or restore.
---
 src/include/catalog/pg_proc.dat            |  18 +
 src/include/statistics/stats_utils.h       |   5 +
 src/backend/statistics/attribute_stats.c   |  16 +-
 src/backend/statistics/relation_stats.c    |  72 +-
 src/backend/statistics/stats_utils.c       |  83 ++
 src/test/regress/expected/stats_import.out | 952 ++++++++++++++++++++-
 src/test/regress/sql/stats_import.sql      | 694 ++++++++++++++-
 doc/src/sgml/func.sgml                     | 157 ++++
 8 files changed, 1975 insertions(+), 22 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 4b7236bbc9..f4d8eb14ec 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12338,5 +12338,23 @@
   proargtypes => 'regclass name bool',
   proargnames => '{relation,attname,bool}',
   prosrc => 'pg_clear_attribute_stats' },
+{ oid => '8052',
+  descr => 'restore statistics on relation',
+  proname => 'pg_restore_relation_stats', provolatile => 'v', proisstrict => 'f',
+  provariadic => 'any',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'any',
+  proargnames => '{relation}',
+  proargmodes => '{v}',
+  prosrc => 'pg_restore_relation_stats' },
+{ oid => '8053',
+  descr => 'restore statistics on attribute',
+  proname => 'pg_restore_attribute_stats', provolatile => 'v', proisstrict => 'f',
+  provariadic => 'any',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'any',
+  proargnames => '{relation}',
+  proargmodes => '{v}',
+  prosrc => 'pg_restore_attribute_stats' },
 
 ]
diff --git a/src/include/statistics/stats_utils.h b/src/include/statistics/stats_utils.h
index a6926ce79a..9d723d9efd 100644
--- a/src/include/statistics/stats_utils.h
+++ b/src/include/statistics/stats_utils.h
@@ -33,4 +33,9 @@ extern void stats_check_arg_pair(FunctionCallInfo fcinfo, StatsArgInfo *arginfo,
 
 extern void stats_lock_check_privileges(Oid reloid);
 
+extern void stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
+											 FunctionCallInfo positional_fcinfo,
+											 StatsArgInfo *arginfo,
+											 int elevel);
+
 #endif							/* STATS_UTILS_H */
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index 0f95a1e46b..b93729fb0a 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -71,7 +71,7 @@ static struct StatsArgInfo attarginfo[] =
 	[HISTOGRAM_BOUNDS_ARG]		 = {"histogram_bounds", TEXTOID},
 	[CORRELATION_ARG]			 = {"correlation", FLOAT4OID},
 	[MOST_COMMON_ELEMS_ARG]		 = {"most_common_elems", TEXTOID},
-	[MOST_COMMON_ELEM_FREQS_ARG] = {"most_common_elems_freq", FLOAT4ARRAYOID},
+	[MOST_COMMON_ELEM_FREQS_ARG] = {"most_common_elem_freqs", FLOAT4ARRAYOID},
 	[ELEM_COUNT_HISTOGRAM_ARG]	 = {"elem_count_histogram", FLOAT4ARRAYOID},
 	[RANGE_LENGTH_HISTOGRAM_ARG] = {"range_length_histogram", TEXTOID},
 	[RANGE_EMPTY_FRAC_ARG]		 = {"range_empty_frac", FLOAT4OID},
@@ -831,3 +831,17 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 
 	PG_RETURN_BOOL(delete_pg_statistic(reloid, attnum, inherited));
 }
+
+Datum
+pg_restore_attribute_stats(PG_FUNCTION_ARGS)
+{
+	LOCAL_FCINFO(positional_fcinfo, NUM_ATTRIBUTE_STATS_ARGS);
+
+	InitFunctionCallInfoData(*positional_fcinfo, NULL, NUM_ATTRIBUTE_STATS_ARGS,
+							 InvalidOid, NULL, NULL);
+
+	stats_fill_fcinfo_from_arg_pairs(fcinfo, positional_fcinfo, attarginfo,
+									 WARNING);
+	PG_RETURN_BOOL(attribute_statistics_update(positional_fcinfo, WARNING));
+}
+
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index a6143cc674..adf409d652 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -15,6 +15,7 @@
  *-------------------------------------------------------------------------
  */
 
+#include <stdbool.h>
 #include "postgres.h"
 
 #include "access/heapam.h"
@@ -51,13 +52,19 @@ static StatsArgInfo relarginfo[] =
 	[NUM_RELATION_STATS_ARGS] = {0}
 };
 
-static bool relation_statistics_update(FunctionCallInfo fcinfo, int elevel);
+static bool relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool implace);
 
 /*
  * Internal function for modifying statistics for a relation.
+ *
+ * If inplace is set to true, the function will update pg_class using in-place
+ * update which is non-transactional. This is the same behavior as in found in
+ * the ANALYZE command. This behavior makes sense in an upgrade and restore
+ * situations where we don't want to bloat pg_class, but would be an
+ * undesirable surprise in most other situations.
  */
 static bool
-relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
+relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
 {
 	Oid				reloid;
 	Relation		crel;
@@ -68,8 +75,6 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	bool			nulls[3]	= {false, false, false};
 	int				ncols		= 0;
 	TupleDesc		tupdesc;
-	HeapTuple		newtup;
-
 
 	stats_check_required_arg(fcinfo, relarginfo, RELATION_ARG);
 	reloid = PG_GETARG_OID(RELATION_ARG);
@@ -111,8 +116,12 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 
 		if (relpages != pgcform->relpages)
 		{
-			replaces[ncols] = Anum_pg_class_relpages;
-			values[ncols] = Int32GetDatum(relpages);
+			if (inplace)
+				pgcform->relpages = relpages;
+			else {
+				replaces[ncols] = Anum_pg_class_relpages;
+				values[ncols] = Int32GetDatum(relpages);
+			}
 			ncols++;
 		}
 	}
@@ -132,8 +141,13 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 
 		if (reltuples != pgcform->reltuples)
 		{
-			replaces[ncols] = Anum_pg_class_reltuples;
-			values[ncols] = Float4GetDatum(reltuples);
+			if (inplace)
+				pgcform->reltuples = reltuples;
+			else
+			{
+				replaces[ncols] = Anum_pg_class_reltuples;
+				values[ncols] = Float4GetDatum(reltuples);
+			}
 			ncols++;
 		}
 	}
@@ -153,8 +167,12 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 
 		if (relallvisible != pgcform->relallvisible)
 		{
-			replaces[ncols] = Anum_pg_class_relallvisible;
-			values[ncols] = Int32GetDatum(relallvisible);
+			if (inplace)
+				pgcform->relallvisible = relallvisible;
+			{
+				replaces[ncols] = Anum_pg_class_relallvisible;
+				values[ncols] = Int32GetDatum(relallvisible);
+			}
 			ncols++;
 		}
 	}
@@ -166,11 +184,18 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 		return false;
 	}
 
-	newtup = heap_modify_tuple_by_cols(ctup, tupdesc, ncols, replaces, values,
-									   nulls);
+	if (inplace)
+		heap_inplace_update(crel, ctup);
+	else
+	{
+		HeapTuple		newtup;
 
-	CatalogTupleUpdate(crel, &newtup->t_self, newtup);
-	heap_freetuple(newtup);
+		newtup = heap_modify_tuple_by_cols(ctup, tupdesc, ncols, replaces, values,
+										nulls);
+
+		CatalogTupleUpdate(crel, &newtup->t_self, newtup);
+		heap_freetuple(newtup);
+	}
 
 	/* release the lock, consistent with vac_update_relstats() */
 	table_close(crel, RowExclusiveLock);
@@ -184,7 +209,7 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 Datum
 pg_set_relation_stats(PG_FUNCTION_ARGS)
 {
-	PG_RETURN_BOOL(relation_statistics_update(fcinfo, ERROR));
+	PG_RETURN_BOOL(relation_statistics_update(fcinfo, ERROR, false));
 }
 
 /*
@@ -207,5 +232,20 @@ pg_clear_relation_stats(PG_FUNCTION_ARGS)
 	newfcinfo->args[3].value = DEFAULT_RELALLVISIBLE;
 	newfcinfo->args[3].isnull = false;
 
-	PG_RETURN_BOOL(relation_statistics_update(newfcinfo, ERROR));
+	PG_RETURN_BOOL(relation_statistics_update(newfcinfo, ERROR, false));
+}
+
+Datum
+pg_restore_relation_stats(PG_FUNCTION_ARGS)
+{
+	LOCAL_FCINFO(positional_fcinfo, NUM_RELATION_STATS_ARGS);
+
+	InitFunctionCallInfoData(*positional_fcinfo, NULL,
+							 NUM_RELATION_STATS_ARGS,
+							 InvalidOid, NULL, NULL);
+
+	stats_fill_fcinfo_from_arg_pairs(fcinfo, positional_fcinfo, relarginfo,
+								     WARNING);
+
+	PG_RETURN_BOOL(relation_statistics_update(positional_fcinfo, WARNING, true));
 }
diff --git a/src/backend/statistics/stats_utils.c b/src/backend/statistics/stats_utils.c
index 4780e98c17..56699a669b 100644
--- a/src/backend/statistics/stats_utils.c
+++ b/src/backend/statistics/stats_utils.c
@@ -18,6 +18,7 @@
 
 #include "access/relation.h"
 #include "catalog/pg_database.h"
+#include "funcapi.h"
 #include "miscadmin.h"
 #include "statistics/stats_utils.h"
 #include "utils/array.h"
@@ -168,3 +169,85 @@ stats_lock_check_privileges(Oid reloid)
 
 	relation_close(rel, NoLock);
 }
+
+/*
+ * Find the argument number for the given argument name, returning -1 if not
+ * found.
+ */
+static int
+get_argnum(const char *argname, StatsArgInfo *arginfo, int elevel)
+{
+	int argnum;
+
+	for (argnum = 0; arginfo[argnum].argname != NULL; argnum++)
+		if (strcasecmp(argname, arginfo[argnum].argname) == 0)
+			return argnum;
+
+	ereport(elevel,
+			(errmsg("unrecognized argument name: \"%s\"", argname)));
+
+	return -1;
+}
+
+/*
+ * Translate variadic argument pairs from 'pairs_fcinfo' into a
+ * 'positional_fcinfo' appropriate for calling relation_statistics_update() or
+ * attribute_statistics_update() with positional arguments.
+ *
+ * Caller should have already initialized positional_fcinfo with a size
+ * appropriate for calling the intended positional function, and arginfo
+ * should also match the intended positional function.
+ */
+void
+stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
+								 FunctionCallInfo positional_fcinfo,
+								 StatsArgInfo *arginfo,
+								 int elevel)
+{
+	Datum   *args;
+	bool    *argnulls;
+	Oid		*types;
+	int      nargs;
+
+	for (int i = 0; arginfo[i].argname != NULL; i++)
+		positional_fcinfo->args[i].isnull = true;
+
+	nargs = extract_variadic_args(pairs_fcinfo, 0, true,
+								  &args, &types, &argnulls);
+
+	if (nargs % 2 != 0)
+		ereport(ERROR,
+				errmsg("need even number of variable arguments"));
+
+	for (int i = 0; i < nargs; i+=2)
+	{
+		int		 argnum;
+		char	*argname;
+
+		if (argnulls[i] || types[i] != TEXTOID)
+			ereport(ERROR, (errmsg("need text argument names")));
+
+		if (argnulls[i+1])
+			continue;
+
+		argname = TextDatumGetCString(args[i]);
+
+		/*
+		 * Version is not a valid positional argument. In the future, it can
+		 * be used to interpret older statistics properly, but it is ignored
+		 * for now.
+		 */
+		if (strcasecmp(argname, "version") == 0)
+			continue;
+		
+		argnum = get_argnum(argname, arginfo, elevel);
+
+		if (argnum >= 0 &&
+			stats_check_arg_type(argname, types[i+1], arginfo[argnum].argtype,
+								 elevel))
+		{
+			positional_fcinfo->args[argnum].value = args[i+1];
+			positional_fcinfo->args[argnum].isnull = false;
+		}
+	}
+}
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index 1a1a6fc6ff..6219c48aa3 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -263,7 +263,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
     most_common_freqs => '{0.2,0.1}'::real[]
     );
 ERROR:  invalid input syntax for type integer: "2023-09-30"
--- warning: mcv cast failure
+-- warn: mcv cast failure
 SELECT pg_catalog.pg_set_attribute_stats(
     relation => 'stats_import.test'::regclass,
     attname => 'id'::name,
@@ -388,7 +388,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
     n_distinct => -0.1::real,
     most_common_elems => '{one,two}'::text
     );
-ERROR:  "most_common_elems_freq" must be specified when "most_common_elems" is specified
+ERROR:  "most_common_elem_freqs" must be specified when "most_common_elems" is specified
 -- warn: mcelem / mcelem null mismatch part 2
 SELECT pg_catalog.pg_set_attribute_stats(
     relation => 'stats_import.test'::regclass,
@@ -399,7 +399,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
     n_distinct => -0.1::real,
     most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
     );
-ERROR:  "most_common_elems" must be specified when "most_common_elems_freq" is specified
+ERROR:  "most_common_elems" must be specified when "most_common_elem_freqs" is specified
 -- ok: mcelem
 SELECT pg_catalog.pg_set_attribute_stats(
     relation => 'stats_import.test'::regclass,
@@ -596,6 +596,741 @@ SELECT pg_catalog.pg_set_attribute_stats(
     );
 ERROR:  maximum number of statistics slots exceeded: 6
 --
+-- Clear attribute stats to try again with restore functions
+-- (relation stats were already cleared).
+--
+SELECT 
+  pg_catalog.pg_clear_attribute_stats(
+        'stats_import.test'::regclass,
+        s.attname,
+        s.inherited)
+FROM pg_catalog.pg_stats AS s
+WHERE s.schemaname = 'stats_import'
+AND s.tablename = 'test'
+ORDER BY s.attname, s.inherited;
+ pg_clear_attribute_stats 
+--------------------------
+ t
+ t
+ t
+(3 rows)
+
+-- reject: object doesn't exist
+SELECT *
+FROM pg_restore_relation_stats(
+        'relation', '0'::oid::regclass,
+        'version', 150000::integer,
+        'relpages', '17'::integer,
+        'reltuples', 400::real,
+        'relallvisible', 4::integer);
+ERROR:  could not open relation with OID 0
+-- ok: set all stats
+SELECT *
+FROM pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'version', 150000::integer,
+        'relpages', '17'::integer,
+        'reltuples', 400::real,
+        'relallvisible', 4::integer);
+ pg_restore_relation_stats 
+---------------------------
+ t
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       17 |       400 |             4
+(1 row)
+
+-- ok: just relpages
+SELECT *
+FROM pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'version', 150000::integer,
+        'relpages', '16'::integer);
+ pg_restore_relation_stats 
+---------------------------
+ t
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       16 |       400 |             4
+(1 row)
+
+-- ok: just reltuples
+SELECT *
+FROM pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'version', 150000::integer,
+        'reltuples', '500'::real);
+ pg_restore_relation_stats 
+---------------------------
+ t
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       16 |       500 |             4
+(1 row)
+
+-- ok: just relallvisible
+SELECT *
+FROM pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'version', 150000::integer,
+        'relallvisible', 5::integer);
+ pg_restore_relation_stats 
+---------------------------
+ t
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       16 |       500 |             5
+(1 row)
+
+-- warn: bad relpages type
+SELECT *
+FROM pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'version', 150000::integer,
+        'relpages', 'nope'::text,
+        'reltuples', 400.0::real,
+        'relallvisible', 4::integer);
+WARNING:  argument "relpages" has type "text", expected type "integer"
+ pg_restore_relation_stats 
+---------------------------
+ t
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       16 |       400 |             4
+(1 row)
+
+-- error: object does not exist
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', '0'::oid::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ERROR:  could not open relation with OID 0
+-- error: relation null
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', NULL::oid,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ERROR:  "relation" cannot be NULL
+-- error: attname null
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', NULL::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ERROR:  "attname" cannot be NULL
+-- error: inherited null
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', NULL::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ERROR:  "inherited" cannot be NULL
+-- ok: no stakinds
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.4::real,
+    'avg_width', 5::integer,
+    'n_distinct', 0.6::real);
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.4 |         5 |        0.6 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: null_frac null
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', NULL::real,
+    'avg_width', 3::integer,
+    'n_distinct', 0.4::real);
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.4 |         3 |        0.4 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: avg_width null
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.2::real,
+    'avg_width', NULL::integer,
+    'n_distinct', 0.5::real);
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.2 |         3 |        0.5 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: n_distinct null
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.3::real,
+    'avg_width', 4::integer,
+    'n_distinct', NULL::real);
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.3 |         4 |        0.5 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: mcv / mcf null mismatch part 1
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.6::real,
+    'avg_width', 7::integer,
+    'n_distinct', -0.7::real,
+    'most_common_freqs', '{0.1,0.2,0.3}'::real[]
+    );
+WARNING:  "most_common_vals" must be specified when "most_common_freqs" is specified
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.6 |         7 |       -0.7 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: mcv / mcf null mismatch part 2
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.7::real,
+    'avg_width', 8::integer,
+    'n_distinct', -0.8::real,
+    'most_common_vals', '{1,2,3}'::text
+    );
+WARNING:  "most_common_freqs" must be specified when "most_common_vals" is specified
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.7 |         8 |       -0.8 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: mcv / mcf type mismatch
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.8::real,
+    'avg_width', 9::integer,
+    'n_distinct', -0.9::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.2,0.1}'::double precision[]
+    );
+WARNING:  argument "most_common_freqs" has type "double precision[]", expected type "real[]"
+WARNING:  "most_common_freqs" must be specified when "most_common_vals" is specified
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.8 |         9 |       -0.9 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: mcv cast failure
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.9::real,
+    'avg_width', 10::integer,
+    'n_distinct', -0.4::real,
+    'most_common_vals', '{2,four,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+WARNING:  invalid input syntax for type integer: "four"
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.9 |        10 |       -0.4 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- ok: mcv+mcf
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 1::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.1 |         1 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: NULL in histogram array
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.2::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.2::real,
+    'histogram_bounds', '{1,NULL,3,4}'::text
+    );
+WARNING:  "histogram_bounds" array cannot contain NULL values
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.2 |         2 |       -0.2 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- ok: histogram_bounds
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.3::real,
+    'avg_width', 3::integer,
+    'n_distinct', -0.3::real,
+    'histogram_bounds', '{1,2,3,4}'::text );
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.3 |         3 |       -0.3 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: elem_count_histogram null element
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'tags'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.4::real,
+    'avg_width', 5::integer,
+    'n_distinct', -0.4::real,
+    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+WARNING:  "elem_count_histogram" array cannot contain NULL values
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | tags    | f         |       0.4 |         5 |       -0.4 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- ok: elem_count_histogram
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'tags'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 6::integer,
+    'n_distinct', -0.55::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
+ stats_import | test      | tags    | f         |       0.5 |         6 |      -0.55 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+(1 row)
+
+-- range stats on a scalar type
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.6::real,
+    'avg_width', 7::integer,
+    'n_distinct', -0.15::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+WARNING:  attribute "id" is not a range type
+DETAIL:  Cannot set STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM or STATISTIC_KIND_BOUNDS_HISTOGRAM.
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.6 |         7 |      -0.15 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'arange'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.7::real,
+    'avg_width', 8::integer,
+    'n_distinct', -0.25::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+WARNING:  "range_empty_frac" must be specified when "range_length_histogram" is specified
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | arange  | f         |       0.7 |         8 |      -0.25 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'arange'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.8::real,
+    'avg_width', 9::integer,
+    'n_distinct', -0.35::real,
+    'range_empty_frac', 0.5::real
+    );
+WARNING:  "range_length_histogram" must be specified when "range_empty_frac" is specified
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | arange  | f         |       0.8 |         9 |      -0.35 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- ok: range_empty_frac + range_length_hist
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'arange'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.9::real,
+    'avg_width', 1::integer,
+    'n_distinct', -0.19::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | arange  | f         |       0.9 |         1 |      -0.19 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
+(1 row)
+
+-- warn: range bounds histogram on scalar
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.29::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+WARNING:  attribute "id" is not a range type
+DETAIL:  Cannot set STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM or STATISTIC_KIND_BOUNDS_HISTOGRAM.
+WARNING:  invalid input syntax for type integer: "[-1,1)"
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.1 |         2 |      -0.29 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- ok: range_bounds_histogram
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'arange'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.2::real,
+    'avg_width', 3::integer,
+    'n_distinct', -0.39::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
+ stats_import | test      | arange  | f         |       0.2 |         3 |      -0.39 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+(1 row)
+
+-- warn: too many stat kinds
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'arange'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{"[2,3)","[1,3)","[3,9)"}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
+    'histogram_bounds', '{"[1,2)","[2,3)","[3,4)","[4,)"}'::text,
+    'correlation', 1.1::real,
+    'most_common_elems', '{3,1}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    'range_empty_frac', -0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text);
+ERROR:  maximum number of statistics slots exceeded: 6
+--
 -- Test the ability to exactly copy data from one table to an identical table,
 -- correctly reconstructing the stakind order as well as the staopN and
 -- stacollN values. Because oids are not stable across databases, we can only
@@ -787,6 +1522,217 @@ WHERE s.starelid = 'stats_import.is_odd'::regclass;
 ---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
 (0 rows)
 
+--
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+        1 |         4 |             0
+(1 row)
+
+--
+-- Clear clone stats to try again with pg_restore_attribute_stats
+--
+SELECT 
+  pg_catalog.pg_clear_attribute_stats(
+        ('stats_import.' || s.tablename)::regclass,
+        s.attname,
+        s.inherited)
+FROM pg_catalog.pg_stats AS s
+WHERE s.schemaname = 'stats_import'
+AND s.tablename IN ('test_clone', 'is_odd_clone')
+ORDER BY s.tablename, s.attname, s.inherited;
+ pg_clear_attribute_stats 
+--------------------------
+ t
+ t
+ t
+ t
+ t
+ t
+(6 rows)
+
+SELECT 
+SELECT COUNT(*)
+FROM pg_catalog.pg_stats AS s
+WHERE s.schemaname = 'stats_import'
+AND s.tablename IN ('test_clone', 'is_odd_clone');
+ERROR:  syntax error at or near "SELECT"
+LINE 2: SELECT COUNT(*)
+        ^
+--
+-- Copy stats from test to test_clone, and is_odd to is_odd_clone
+--
+SELECT s.schemaname, s.tablename, s.attname, s.inherited, r.*
+FROM pg_catalog.pg_stats AS s
+CROSS JOIN LATERAL
+    pg_catalog.pg_restore_attribute_stats(
+        'relation', ('stats_import.' || s.tablename || '_clone')::regclass,
+        'attname', s.attname,
+        'inherited', s.inherited,
+        'version', 150000,
+        'null_frac', s.null_frac,
+        'avg_width', s.avg_width,
+        'n_distinct', s.n_distinct,
+        'most_common_vals', s.most_common_vals::text,
+        'most_common_freqs', s.most_common_freqs,
+        'histogram_bounds', s.histogram_bounds::text,
+        'correlation', s.correlation,
+        'most_common_elems', s.most_common_elems::text,
+        'most_common_elem_freqs', s.most_common_elem_freqs,
+        'elem_count_histogram', s.elem_count_histogram,
+        'range_bounds_histogram', s.range_bounds_histogram::text,
+        'range_empty_frac', s.range_empty_frac,
+        'range_length_histogram', s.range_length_histogram::text) AS r
+WHERE s.schemaname = 'stats_import'
+AND s.tablename IN ('test', 'is_odd')
+ORDER BY s.tablename, s.attname, s.inherited;
+  schemaname  | tablename | attname | inherited | r 
+--------------+-----------+---------+-----------+---
+ stats_import | is_odd    | expr    | f         | t
+ stats_import | test      | arange  | f         | t
+ stats_import | test      | comp    | f         | t
+ stats_import | test      | id      | f         | t
+ stats_import | test      | name    | f         | t
+ stats_import | test      | tags    | f         | t
+(6 rows)
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+   relname    | num_stats 
+--------------+-----------
+ is_odd       |         1
+ is_odd_clone |         1
+ test         |         5
+ test_clone   |         5
+(4 rows)
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
 DROP SCHEMA stats_import CASCADE;
 NOTICE:  drop cascades to 5 other objects
 DETAIL:  drop cascades to type stats_import.complex_type
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index 5651f133c0..cbc363e9f4 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -205,7 +205,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
     most_common_freqs => '{0.2,0.1}'::real[]
     );
 
--- warning: mcv cast failure
+-- warn: mcv cast failure
 SELECT pg_catalog.pg_set_attribute_stats(
     relation => 'stats_import.test'::regclass,
     attname => 'id'::name,
@@ -470,6 +470,529 @@ SELECT pg_catalog.pg_set_attribute_stats(
     range_length_histogram => '{399,499,Infinity}'::text,
     range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
     );
+
+--
+-- Clear attribute stats to try again with restore functions
+-- (relation stats were already cleared).
+--
+SELECT 
+  pg_catalog.pg_clear_attribute_stats(
+        'stats_import.test'::regclass,
+        s.attname,
+        s.inherited)
+FROM pg_catalog.pg_stats AS s
+WHERE s.schemaname = 'stats_import'
+AND s.tablename = 'test'
+ORDER BY s.attname, s.inherited;
+
+-- reject: object doesn't exist
+SELECT *
+FROM pg_restore_relation_stats(
+        'relation', '0'::oid::regclass,
+        'version', 150000::integer,
+        'relpages', '17'::integer,
+        'reltuples', 400::real,
+        'relallvisible', 4::integer);
+
+-- ok: set all stats
+SELECT *
+FROM pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'version', 150000::integer,
+        'relpages', '17'::integer,
+        'reltuples', 400::real,
+        'relallvisible', 4::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+-- ok: just relpages
+SELECT *
+FROM pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'version', 150000::integer,
+        'relpages', '16'::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+-- ok: just reltuples
+SELECT *
+FROM pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'version', 150000::integer,
+        'reltuples', '500'::real);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+-- ok: just relallvisible
+SELECT *
+FROM pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'version', 150000::integer,
+        'relallvisible', 5::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+-- warn: bad relpages type
+SELECT *
+FROM pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'version', 150000::integer,
+        'relpages', 'nope'::text,
+        'reltuples', 400.0::real,
+        'relallvisible', 4::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+-- error: object does not exist
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', '0'::oid::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: relation null
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', NULL::oid,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: attname null
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', NULL::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: inherited null
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', NULL::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- ok: no stakinds
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.4::real,
+    'avg_width', 5::integer,
+    'n_distinct', 0.6::real);
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: null_frac null
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', NULL::real,
+    'avg_width', 3::integer,
+    'n_distinct', 0.4::real);
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: avg_width null
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.2::real,
+    'avg_width', NULL::integer,
+    'n_distinct', 0.5::real);
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: n_distinct null
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.3::real,
+    'avg_width', 4::integer,
+    'n_distinct', NULL::real);
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: mcv / mcf null mismatch part 1
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.6::real,
+    'avg_width', 7::integer,
+    'n_distinct', -0.7::real,
+    'most_common_freqs', '{0.1,0.2,0.3}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: mcv / mcf null mismatch part 2
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.7::real,
+    'avg_width', 8::integer,
+    'n_distinct', -0.8::real,
+    'most_common_vals', '{1,2,3}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: mcv / mcf type mismatch
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.8::real,
+    'avg_width', 9::integer,
+    'n_distinct', -0.9::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.2,0.1}'::double precision[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: mcv cast failure
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.9::real,
+    'avg_width', 10::integer,
+    'n_distinct', -0.4::real,
+    'most_common_vals', '{2,four,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- ok: mcv+mcf
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 1::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,1,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: NULL in histogram array
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.2::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.2::real,
+    'histogram_bounds', '{1,NULL,3,4}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- ok: histogram_bounds
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.3::real,
+    'avg_width', 3::integer,
+    'n_distinct', -0.3::real,
+    'histogram_bounds', '{1,2,3,4}'::text );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: elem_count_histogram null element
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'tags'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.4::real,
+    'avg_width', 5::integer,
+    'n_distinct', -0.4::real,
+    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- ok: elem_count_histogram
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'tags'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 6::integer,
+    'n_distinct', -0.55::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- range stats on a scalar type
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.6::real,
+    'avg_width', 7::integer,
+    'n_distinct', -0.15::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'arange'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.7::real,
+    'avg_width', 8::integer,
+    'n_distinct', -0.25::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'arange'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.8::real,
+    'avg_width', 9::integer,
+    'n_distinct', -0.35::real,
+    'range_empty_frac', 0.5::real
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- ok: range_empty_frac + range_length_hist
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'arange'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.9::real,
+    'avg_width', 1::integer,
+    'n_distinct', -0.19::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: range bounds histogram on scalar
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.29::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- ok: range_bounds_histogram
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'arange'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.2::real,
+    'avg_width', 3::integer,
+    'n_distinct', -0.39::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: too many stat kinds
+SELECT *
+FROM pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'arange'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{"[2,3)","[1,3)","[3,9)"}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
+    'histogram_bounds', '{"[1,2)","[2,3)","[3,4)","[4,)"}'::text,
+    'correlation', 1.1::real,
+    'most_common_elems', '{3,1}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    'range_empty_frac', -0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text);
+
 --
 -- Test the ability to exactly copy data from one table to an identical table,
 -- correctly reconstructing the stakind order as well as the staopN and
@@ -488,7 +1011,6 @@ UNION ALL
 SELECT 4, 'four', NULL, int4range(0,100), NULL;
 
 CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
-
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 
@@ -639,4 +1161,172 @@ FROM pg_statistic s
 JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
 WHERE s.starelid = 'stats_import.is_odd'::regclass;
 
+--
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+--
+-- Clear clone stats to try again with pg_restore_attribute_stats
+--
+SELECT 
+  pg_catalog.pg_clear_attribute_stats(
+        ('stats_import.' || s.tablename)::regclass,
+        s.attname,
+        s.inherited)
+FROM pg_catalog.pg_stats AS s
+WHERE s.schemaname = 'stats_import'
+AND s.tablename IN ('test_clone', 'is_odd_clone')
+ORDER BY s.tablename, s.attname, s.inherited;
+SELECT 
+
+SELECT COUNT(*)
+FROM pg_catalog.pg_stats AS s
+WHERE s.schemaname = 'stats_import'
+AND s.tablename IN ('test_clone', 'is_odd_clone');
+
+--
+-- Copy stats from test to test_clone, and is_odd to is_odd_clone
+--
+SELECT s.schemaname, s.tablename, s.attname, s.inherited, r.*
+FROM pg_catalog.pg_stats AS s
+CROSS JOIN LATERAL
+    pg_catalog.pg_restore_attribute_stats(
+        'relation', ('stats_import.' || s.tablename || '_clone')::regclass,
+        'attname', s.attname,
+        'inherited', s.inherited,
+        'version', 150000,
+        'null_frac', s.null_frac,
+        'avg_width', s.avg_width,
+        'n_distinct', s.n_distinct,
+        'most_common_vals', s.most_common_vals::text,
+        'most_common_freqs', s.most_common_freqs,
+        'histogram_bounds', s.histogram_bounds::text,
+        'correlation', s.correlation,
+        'most_common_elems', s.most_common_elems::text,
+        'most_common_elem_freqs', s.most_common_elem_freqs,
+        'elem_count_histogram', s.elem_count_histogram,
+        'range_bounds_histogram', s.range_bounds_histogram::text,
+        'range_empty_frac', s.range_empty_frac,
+        'range_length_histogram', s.range_length_histogram::text) AS r
+WHERE s.schemaname = 'stats_import'
+AND s.tablename IN ('test', 'is_odd')
+ORDER BY s.tablename, s.attname, s.inherited;
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test_clone'::regclass;
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test'::regclass;
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd_clone'::regclass;
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd'::regclass;
+
 DROP SCHEMA stats_import CASCADE;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 672d478a90..c7b791e7ac 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30324,6 +30324,163 @@ DETAIL:  Make sure pg_wal_replay_wait() isn't called within a transaction with a
        </entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_restore_relation_stats</primary>
+        </indexterm>
+        <function>pg_restore_relation_stats</function> (
+        <literal>VARIADIC</literal> <parameter>kwargs</parameter> <type>"any"</type> )
+        <returnvalue>boolean</returnvalue>
+        </para>
+        <para>
+         Similar to <function>pg_set_relation_stats()</function>, but intended
+         for bulk restore of relation statistics. The tracked statistics may
+         change from version to version, so the primary purpose of this
+         function is to maintain a consistent function signature to avoid
+         errors when restoring statistics from previous versions.
+        </para>
+        <para>
+         Arguments are passed as pairs of <replaceable>argname</replaceable>
+         and <replaceable>argvalue</replaceable>, where
+         <replaceable>argname</replaceable> corresponds to a named argument in
+         <function>pg_set_relation_stats()</function> and
+         <replaceable>argvalue</replaceable> is of the corresponding type.
+        </para>
+        <para>
+         Additionally, this function supports argument name
+         <literal>version</literal> of type <type>integer</type>, which
+         specifies the version from which the statistics originated, improving
+         intepretation of older statistics.
+        </para>
+        <para>
+         For example, to set the <structname>relpages</structname> and
+         <structname>reltuples</structname> of the table
+         <structname>mytable</structname>:
+<programlisting>
+ SELECT pg_restore_relation_stats(
+    'relation',  'mytable'::regclass,
+    'relpages',  173::integer,
+    'reltuples', 10000::float4);
+</programlisting>
+        </para>
+        <para>
+         Minor errors are reported as a <literal>WARNING</literal> and
+         ignored, and remaining statistics specified will still be restored.
+        </para>
+       </entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry">
+        <para role="func_signature">
+         <indexterm>
+          <primary>pg_set_attribute_stats</primary>
+         </indexterm>
+         <function>pg_set_attribute_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>attname</parameter> <type>name</type>,
+         <parameter>inherited</parameter> <type>boolean</type>
+         <optional>, <parameter>null_frac</parameter> <type>real</type></optional>
+         <optional>, <parameter>avg_width</parameter> <type>integer</type></optional>
+         <optional>, <parameter>n_distinct</parameter> <type>real</type></optional>
+         <optional>, <parameter>most_common_vals</parameter> <type>text</type>, <parameter>most_common_freqs</parameter> <type>real[]</type> </optional>
+         <optional>, <parameter>histogram_bounds</parameter> <type>text</type> </optional>
+         <optional>, <parameter>correlation</parameter> <type>real</type> </optional>
+         <optional>, <parameter>most_common_elems</parameter> <type>text</type>, <parameter>most_common_elem_freqs</parameter> <type>real[]</type> </optional>
+         <optional>, <parameter>elem_count_histogram</parameter> <type>real[]</type> </optional>
+         <optional>, <parameter>range_length_histogram</parameter> <type>text</type> </optional>
+         <optional>, <parameter>range_empty_frac</parameter> <type>real</type> </optional>
+         <optional>, <parameter>range_bounds_histogram</parameter> <type>text</type> </optional> )
+         <returnvalue>booolean</returnvalue>
+        </para>
+        <para>
+         Creates or updates attribute-level statistics for the given relation
+         and attribute name to the specified values. The parameters correspond
+         to to attributes of the same name found in the <link
+         linkend="view-pg-stats"><structname>pg_stats</structname></link>
+         view. Returns <literal>true</literal> if a change was made;
+         <literal>false</literal> otherwise.
+        </para>
+        <para>
+         All parameters default to <literal>NULL</literal>, which leave the
+         corresponding statistic unchanged.
+        </para>
+        <para>
+         Ordinarily, these statistics are collected automatically or updated
+         as a part of <xref linkend="sql-vacuum"/> or <xref
+         linkend="sql-analyze"/>, so it's not necessary to call this
+         function. However, it may be useful when testing the effects of
+         statistics on the planner to understand or anticipate plan changes.
+        </para>
+        <para>
+         The caller must have the <literal>MAINTAIN</literal> privilege on
+         the table or be the owner of the database.
+        </para>
+        <warning>
+         <para>
+          Changes made by this function are likely to be overwritten by <link
+          linkend="autovacuum">autovacuum</link> (or manual
+          <command>VACUUM</command> or <command>ANALYZE</command>) and should
+          be considered temporary.
+         </para>
+         <para>
+          The signature of this function may change in new major releases if
+          there are changes to the statistics being tracked.
+         </para>
+        </warning>
+       </entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_restore_attribute_stats</primary>
+        </indexterm>
+        <function>pg_restore_attribute_stats</function> (
+        <literal>VARIADIC</literal> <parameter>kwargs</parameter> <type>"any"</type> )
+        <returnvalue>boolean</returnvalue>
+        </para>
+        <para>
+         Similar to <function>pg_set_attribute_stats()</function>, but
+         intended for bulk restore of attribute statistics. The tracked
+         statistics may change from version to version, so the primary purpose
+         of this function is to maintain a consistent function signature to
+         avoid errors when restoring statistics from previous versions.
+        </para>
+        <para>
+         Arguments are passed as pairs of <replaceable>argname</replaceable>
+         and <replaceable>argvalue</replaceable>, where
+         <replaceable>argname</replaceable> corresponds to a named argument in
+         <function>pg_set_attribute_stats()</function> and
+         <replaceable>argvalue</replaceable> is of the corresponding type.
+        </para>
+        <para>
+         Additionally, this function supports argument name
+         <literal>version</literal> of type <type>integer</type>, which
+         specifies the version from which the statistics originated, improving
+         intepretation of older statistics.
+        </para>
+        <para>
+         For example, to set the <structname>avg_width</structname> and
+         <structname>null_frac</structname> for the attribute
+         <structname>col1</structname> of the table
+         <structname>mytable</structname>:
+<programlisting>
+ SELECT pg_restore_attribute_stats(
+    'relation',    'mytable'::regclass,
+    'attname',     'col1'::name,
+    'inherited',   false,
+    'avg_width',   125::integer,
+    'null_frac',   0.5::real);
+</programlisting>
+        </para>
+        <para>
+         Minor errors are reported as a <literal>WARNING</literal> and
+         ignored, and remaining statistics specified will still be restored.
+        </para>
+       </entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
-- 
2.46.0

v29-0002-Create-functions-pg_set_relation_stats-pg_clear_.patchtext/x-patch; charset=US-ASCII; name=v29-0002-Create-functions-pg_set_relation_stats-pg_clear_.patchDownload
From 3c4115f665e80e67e4bf56439e95bd3577c0ca42 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Mon, 16 Sep 2024 05:02:32 -0400
Subject: [PATCH v29 2/7] Create functions pg_set_relation_stats,
 pg_clear_relation_stats.

These functions are used to tweak statistics on any relation, provided
that the user has MAINTAIN privilege on the relation, or is the database
owner.

These functions set the following attributes in pg_class: relpages,
reltuples, and relallvisible. Future versions may set any new stats-related
attributes.
---
 src/include/catalog/pg_proc.dat            |  15 ++
 src/backend/statistics/Makefile            |   1 +
 src/backend/statistics/meson.build         |   1 +
 src/backend/statistics/relation_stats.c    | 211 +++++++++++++++++++++
 src/test/regress/expected/stats_import.out | 128 +++++++++++++
 src/test/regress/parallel_schedule         |   2 +-
 src/test/regress/sql/stats_import.sql      |  88 +++++++++
 doc/src/sgml/func.sgml                     | 101 ++++++++++
 8 files changed, 546 insertions(+), 1 deletion(-)
 create mode 100644 src/backend/statistics/relation_stats.c
 create mode 100644 src/test/regress/expected/stats_import.out
 create mode 100644 src/test/regress/sql/stats_import.sql

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 53a081ed88..24279c3078 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12309,5 +12309,20 @@
   proallargtypes => '{int8,pg_lsn,pg_lsn,int4}', proargmodes => '{o,o,o,o}',
   proargnames => '{summarized_tli,summarized_lsn,pending_lsn,summarizer_pid}',
   prosrc => 'pg_get_wal_summarizer_state' },
+  +# Statistics Import
+{ oid => '8048',
+  descr => 'set statistics on relation',
+  proname => 'pg_set_relation_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'regclass int4 float4 int4',
+  proargnames => '{relation,relpages,reltuples,relallvisible}',
+  prosrc => 'pg_set_relation_stats' },
+{ oid => '8049',
+  descr => 'clear statistics on relation',
+  proname => 'pg_clear_relation_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'regclass',
+  proargnames => '{relation}',
+  prosrc => 'pg_clear_relation_stats' },
 
 ]
diff --git a/src/backend/statistics/Makefile b/src/backend/statistics/Makefile
index 3ffc8f38e6..f38c874986 100644
--- a/src/backend/statistics/Makefile
+++ b/src/backend/statistics/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	extended_stats.o \
 	mcv.o \
 	mvdistinct.o \
+	relation_stats.o \
 	stats_utils.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/statistics/meson.build b/src/backend/statistics/meson.build
index 74c5dc6afa..376bd2bee1 100644
--- a/src/backend/statistics/meson.build
+++ b/src/backend/statistics/meson.build
@@ -5,5 +5,6 @@ backend_sources += files(
   'extended_stats.c',
   'mcv.c',
   'mvdistinct.c',
+  'relation_stats.c',
   'stats_utils.c'
 )
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
new file mode 100644
index 0000000000..a6143cc674
--- /dev/null
+++ b/src/backend/statistics/relation_stats.c
@@ -0,0 +1,211 @@
+/*-------------------------------------------------------------------------
+ * relation_stats.c
+ *
+ *	  PostgreSQL relation statistics manipulation
+ *
+ * Code supporting the direct import of relation statistics, similar to
+ * what is done by the ANALYZE command.
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *       src/backend/statistics/relation_stats.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_type_d.h"
+#include "statistics/stats_utils.h"
+#include "utils/fmgrprotos.h"
+#include "utils/syscache.h"
+
+#define DEFAULT_RELPAGES Int32GetDatum(0)
+#define DEFAULT_RELTUPLES Float4GetDatum(-1.0)
+#define DEFAULT_RELALLVISIBLE Int32GetDatum(0)
+
+/*
+ * Positional argument numbers, names, and types for
+ * relation_statistics_update().
+ */
+
+typedef enum
+{
+	RELATION_ARG = 0,
+	RELPAGES_ARG,
+	RELTUPLES_ARG,
+	RELALLVISIBLE_ARG,
+	NUM_RELATION_STATS_ARGS
+} relation_stats_argnum;
+
+static StatsArgInfo relarginfo[] =
+{
+	[RELATION_ARG]			  = {"relation", REGCLASSOID},
+	[RELPAGES_ARG]			  = {"relpages", INT4OID},
+	[RELTUPLES_ARG]			  = {"reltuples", FLOAT4OID},
+	[RELALLVISIBLE_ARG]		  = {"relallvisible", INT4OID},
+	[NUM_RELATION_STATS_ARGS] = {0}
+};
+
+static bool relation_statistics_update(FunctionCallInfo fcinfo, int elevel);
+
+/*
+ * Internal function for modifying statistics for a relation.
+ */
+static bool
+relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
+{
+	Oid				reloid;
+	Relation		crel;
+	HeapTuple		ctup;
+	Form_pg_class	pgcform;
+	int				replaces[3]	= {0};
+	Datum			values[3]	= {0};
+	bool			nulls[3]	= {false, false, false};
+	int				ncols		= 0;
+	TupleDesc		tupdesc;
+	HeapTuple		newtup;
+
+
+	stats_check_required_arg(fcinfo, relarginfo, RELATION_ARG);
+	reloid = PG_GETARG_OID(RELATION_ARG);
+
+	stats_lock_check_privileges(reloid);
+
+	/*
+	 * Take RowExclusiveLock on pg_class, consistent with
+	 * vac_update_relstats().
+	 */
+	crel = table_open(RelationRelationId, RowExclusiveLock);
+
+	tupdesc = RelationGetDescr(crel);
+	ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(reloid));
+	if (!HeapTupleIsValid(ctup))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_OBJECT_IN_USE),
+				 errmsg("pg_class entry for relid %u not found", reloid)));
+		table_close(crel, RowExclusiveLock);
+		return false;
+	}
+
+	pgcform = (Form_pg_class) GETSTRUCT(ctup);
+
+	/* relpages */
+	if (!PG_ARGISNULL(RELPAGES_ARG))
+	{
+		int32 relpages = PG_GETARG_INT32(RELPAGES_ARG);
+
+		if (relpages < -1)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("relpages cannot be < -1")));
+			table_close(crel, RowExclusiveLock);
+			return false;
+		}
+
+		if (relpages != pgcform->relpages)
+		{
+			replaces[ncols] = Anum_pg_class_relpages;
+			values[ncols] = Int32GetDatum(relpages);
+			ncols++;
+		}
+	}
+
+	if (!PG_ARGISNULL(RELTUPLES_ARG))
+	{
+		float reltuples = PG_GETARG_FLOAT4(RELTUPLES_ARG);
+
+		if (reltuples < -1.0)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("reltuples cannot be < -1.0")));
+			table_close(crel, RowExclusiveLock);
+			return false;
+		}
+
+		if (reltuples != pgcform->reltuples)
+		{
+			replaces[ncols] = Anum_pg_class_reltuples;
+			values[ncols] = Float4GetDatum(reltuples);
+			ncols++;
+		}
+	}
+
+	if (!PG_ARGISNULL(RELALLVISIBLE_ARG))
+	{
+		int32 relallvisible = PG_GETARG_INT32(RELALLVISIBLE_ARG);
+
+		if (relallvisible < 0)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("relallvisible cannot be < 0")));
+			table_close(crel, RowExclusiveLock);
+			return false;
+		}
+
+		if (relallvisible != pgcform->relallvisible)
+		{
+			replaces[ncols] = Anum_pg_class_relallvisible;
+			values[ncols] = Int32GetDatum(relallvisible);
+			ncols++;
+		}
+	}
+
+	/* only update pg_class if there is a meaningful change */
+	if (ncols == 0)
+	{
+		table_close(crel, RowExclusiveLock);
+		return false;
+	}
+
+	newtup = heap_modify_tuple_by_cols(ctup, tupdesc, ncols, replaces, values,
+									   nulls);
+
+	CatalogTupleUpdate(crel, &newtup->t_self, newtup);
+	heap_freetuple(newtup);
+
+	/* release the lock, consistent with vac_update_relstats() */
+	table_close(crel, RowExclusiveLock);
+
+	return true;
+}
+
+/*
+ * Set statistics for a given pg_class entry.
+ */
+Datum
+pg_set_relation_stats(PG_FUNCTION_ARGS)
+{
+	PG_RETURN_BOOL(relation_statistics_update(fcinfo, ERROR));
+}
+
+/*
+ * Clear statistics for a given pg_class entry; that is, set back to initial
+ * stats for a newly-created table.
+ */
+Datum
+pg_clear_relation_stats(PG_FUNCTION_ARGS)
+{
+	LOCAL_FCINFO(newfcinfo, 4);
+
+	InitFunctionCallInfoData(*newfcinfo, NULL, 4, InvalidOid, NULL, NULL);
+
+	newfcinfo->args[0].value = PG_GETARG_OID(0);
+	newfcinfo->args[0].isnull = PG_ARGISNULL(0);
+	newfcinfo->args[1].value = DEFAULT_RELPAGES;
+	newfcinfo->args[1].isnull = false;
+	newfcinfo->args[2].value = DEFAULT_RELTUPLES;
+	newfcinfo->args[2].isnull = false;
+	newfcinfo->args[3].value = DEFAULT_RELALLVISIBLE;
+	newfcinfo->args[3].isnull = false;
+
+	PG_RETURN_BOOL(relation_statistics_update(newfcinfo, ERROR));
+}
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
new file mode 100644
index 0000000000..951dd6a10e
--- /dev/null
+++ b/src/test/regress/expected/stats_import.out
@@ -0,0 +1,128 @@
+CREATE SCHEMA stats_import;
+CREATE TYPE stats_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+CREATE TABLE stats_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+-- starting stats
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+        0 |        -1 |             0
+(1 row)
+
+-- error: regclass not found
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 0::Oid,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+ERROR:  could not open relation with OID 0
+-- relpages default
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => NULL::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+ pg_set_relation_stats 
+-----------------------
+ t
+(1 row)
+
+-- reltuples default
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => NULL::real,
+        relallvisible => 4::integer);
+ pg_set_relation_stats 
+-----------------------
+ t
+(1 row)
+
+-- relallvisible default
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => NULL::integer);
+ pg_set_relation_stats 
+-----------------------
+ f
+(1 row)
+
+-- named arguments
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+ pg_set_relation_stats 
+-----------------------
+ f
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       17 |       400 |             4
+(1 row)
+
+-- positional arguments
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        'stats_import.test'::regclass,
+        18::integer,
+        401.0::real,
+        5::integer);
+ pg_set_relation_stats 
+-----------------------
+ t
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       18 |       401 |             5
+(1 row)
+
+-- clear
+SELECT
+    pg_catalog.pg_clear_relation_stats(
+        'stats_import.test'::regclass);
+ pg_clear_relation_stats 
+-------------------------
+ t
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+        0 |        -1 |             0
+(1 row)
+
+DROP SCHEMA stats_import CASCADE;
+NOTICE:  drop cascades to 2 other objects
+DETAIL:  drop cascades to type stats_import.complex_type
+drop cascades to table stats_import.test
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 7a5a910562..0d0428518f 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,7 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import
 
 # ----------
 # Load huge amounts of data
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
new file mode 100644
index 0000000000..40199dd453
--- /dev/null
+++ b/src/test/regress/sql/stats_import.sql
@@ -0,0 +1,88 @@
+CREATE SCHEMA stats_import;
+
+CREATE TYPE stats_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+
+CREATE TABLE stats_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+
+-- starting stats
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+-- error: regclass not found
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 0::Oid,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+
+-- relpages default
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => NULL::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+
+-- reltuples default
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => NULL::real,
+        relallvisible => 4::integer);
+
+-- relallvisible default
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => NULL::integer);
+
+-- named arguments
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => 17::integer,
+        reltuples => 400.0::real,
+        relallvisible => 4::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+-- positional arguments
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        'stats_import.test'::regclass,
+        18::integer,
+        401.0::real,
+        5::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+-- clear
+SELECT
+    pg_catalog.pg_clear_relation_stats(
+        'stats_import.test'::regclass);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+DROP SCHEMA stats_import CASCADE;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 84eb3a45ee..8a715934fe 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30129,6 +30129,107 @@ DETAIL:  Make sure pg_wal_replay_wait() isn't called within a transaction with a
     </tgroup>
    </table>
 
+   <table id="functions-admin-statsmods">
+    <title>Database Object Statistics Modification Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry">
+        <para role="func_signature">
+         <indexterm>
+          <primary>pg_set_relation_stats</primary>
+         </indexterm>
+         <function>pg_set_relation_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>
+         <optional>, <parameter>relpages</parameter> <type>integer</type></optional>
+         <optional>, <parameter>reltuples</parameter> <type>real</type></optional>
+         <optional>, <parameter>relallvisible</parameter> <type>integer</type></optional> )
+         <returnvalue>boolean</returnvalue>
+        </para>
+        <para>
+         Updates relation-level statistics for the given relation to the
+         specified values. The parameters correspond to columns in <link
+         linkend="catalog-pg-class"><structname>pg_class</structname></link>. Unspecified
+         or <literal>NULL</literal> values leave the setting
+         unchanged. Returns <literal>true</literal> if a change was made;
+         <literal>false</literal> otherwise.
+        </para>
+        <para>
+         Ordinarily, these statistics are collected automatically or updated
+         as a part of <xref linkend="sql-vacuum"/> or <xref
+         linkend="sql-analyze"/>, so it's not necessary to call this
+         function. However, it may be useful when testing the effects of
+         statistics on the planner to understand or anticipate plan changes.
+        </para>
+        <para>
+         The caller must have the <literal>MAINTAIN</literal> privilege on
+         the table or be the owner of the database.
+        </para>
+        <para>
+         The value of <structfield>relpages</structfield> must be greater than
+         or equal to <literal>0</literal>,
+         <structfield>reltuples</structfield> must be greater than or equal to
+         <literal>-1.0</literal>, and <structfield>relallvisible</structfield>
+         must be greater than or equal to <literal>0</literal>.
+        </para>
+        <warning>
+         <para>
+          Changes made by this function are likely to be overwritten by <link
+          linkend="autovacuum">autovacuum</link> (or manual
+          <command>VACUUM</command> or <command>ANALYZE</command>) and should
+          be considered temporary.
+         </para>
+         <para>
+          The signature of this function may change in new major releases if
+          there are changes to the statistics being tracked.
+         </para>
+        </warning>
+       </entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry">
+        <para role="func_signature">
+         <indexterm>
+          <primary>pg_clear_relation_stats</primary>
+         </indexterm>
+         <function>pg_clear_relation_stats</function> ( <parameter>relation</parameter> <type>regclass</type> )
+         <returnvalue>boolean</returnvalue>
+        </para>
+        <para>
+         Clears table-level statistics for the given relation, as though the
+         table was newly created. Returns <literal>true</literal> if a change
+         was made; <literal>false</literal> otherwise.
+        </para>
+        <para>
+         The caller must have the <literal>MAINTAIN</literal> privilege on
+         the table or be the owner of the database.
+        </para>
+        <warning>
+         <para>
+          Changes made by this function are likely to be overwritten by <link
+          linkend="autovacuum">autovacuum</link> (or manual
+          <command>VACUUM</command> or <command>ANALYZE</command>) and should
+          be considered temporary.
+         </para>
+        </warning>
+       </entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
    <para>
     <xref linkend="functions-info-partition"/> lists functions that provide
     information about the structure of partitioned tables.
-- 
2.46.0

v29-0004-Create-functions-pg_set_attribute_stats-pg_clear.patchtext/x-patch; charset=US-ASCII; name=v29-0004-Create-functions-pg_set_attribute_stats-pg_clear.patchDownload
From 76a56d16f88dff5462f3a38f001024699a9c40cb Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Mon, 16 Sep 2024 15:42:48 -0400
Subject: [PATCH v29 4/7] Create functions pg_set_attribute_stats,
 pg_clear_attribute_stats.

The function pg_set_attribute_stats is used modify attribute statistics,
allowing the user to inflate chose favorable histograms, inflate the
frequency of certain values, etc, to see what those changes will evoke
from the query planner.

A call to pg_set_attribute_stats will create or modify a pg_statistic
row for the given relation/attribute/inherited combination specified.

While pg_set_attribute_stats does not attempt to validate the statistics
given, certain data errors make rendering those statistics impossible,
and thus those data errors will cause the function to error.

Examples:

- Some statistics kinds come in pairs. For example, the mcv stat
  consists of two parameters: most_common_vals and most_common_freqs,
  they must both be present in order to complete the mcv stat. If one
  is given but not the other, the operation will raise an error.

- Multi-value statistics such as most_common_elems do not allow for any
  elements within the array provided to be NULL, and any array given
  with NULLs in it will be rejected, which would also cause the
  corresponding -freqs parameter to be rejected, thus rejecting the
  whole stat-kind, but not otherwise affecting other parameters
  provided.

The function pg_clear_attribute_stats will delete the pg_statistic row
for the given relation/attribute/inherited combination specified.
---
 src/include/catalog/pg_proc.dat            |  14 +
 src/backend/catalog/system_functions.sql   |  22 +
 src/backend/statistics/Makefile            |   1 +
 src/backend/statistics/attribute_stats.c   | 833 +++++++++++++++++++++
 src/backend/statistics/meson.build         |   1 +
 src/test/regress/expected/stats_import.out | 657 +++++++++++++++-
 src/test/regress/sql/stats_import.sql      | 544 ++++++++++++++
 doc/src/sgml/func.sgml                     |  98 +++
 8 files changed, 2169 insertions(+), 1 deletion(-)
 create mode 100644 src/backend/statistics/attribute_stats.c

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 24279c3078..4b7236bbc9 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12324,5 +12324,19 @@
   proargtypes => 'regclass',
   proargnames => '{relation}',
   prosrc => 'pg_clear_relation_stats' },
+{ oid => '8050',
+  descr => 'set statistics on attribute',
+  proname => 'pg_set_attribute_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'regclass name bool float4 int4 float4 text _float4 text float4 text _float4 _float4 text float4 text',
+  proargnames => '{relation,attname,inherited,null_frac,avg_width,n_distinct,most_common_vals,most_common_freqs,histogram_bounds,correlation,most_common_elems,most_common_elem_freqs,elem_count_histogram,range_length_histogram,range_empty_frac,range_bounds_histogram}',
+  prosrc => 'pg_set_attribute_stats' },
+{ oid => '8051',
+  descr => 'clear statistics on attribute',
+  proname => 'pg_clear_attribute_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'regclass name bool',
+  proargnames => '{relation,attname,bool}',
+  prosrc => 'pg_clear_attribute_stats' },
 
 ]
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 623b9539b1..c9029c5fb5 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -639,6 +639,28 @@ LANGUAGE INTERNAL
 CALLED ON NULL INPUT VOLATILE PARALLEL SAFE
 AS 'pg_stat_reset_slru';
 
+CREATE OR REPLACE FUNCTION
+  pg_set_attribute_stats(relation regclass,
+                         attname name,
+                         inherited bool,
+                         null_frac real DEFAULT NULL,
+                         avg_width integer DEFAULT NULL,
+                         n_distinct real DEFAULT NULL,
+                         most_common_vals text DEFAULT NULL,
+                         most_common_freqs real[] DEFAULT NULL,
+                         histogram_bounds text DEFAULT NULL,
+                         correlation real DEFAULT NULL,
+                         most_common_elems text DEFAULT NULL,
+                         most_common_elem_freqs real[] DEFAULT NULL,
+                         elem_count_histogram real[] DEFAULT NULL,
+                         range_length_histogram text DEFAULT NULL,
+                         range_empty_frac real DEFAULT NULL,
+                         range_bounds_histogram text DEFAULT NULL)
+RETURNS bool
+LANGUAGE INTERNAL
+CALLED ON NULL INPUT VOLATILE
+AS 'pg_set_attribute_stats';
+
 --
 -- The default permissions for functions mean that anyone can execute them.
 -- A number of functions shouldn't be executable by just anyone, but rather
diff --git a/src/backend/statistics/Makefile b/src/backend/statistics/Makefile
index f38c874986..4a620b22f1 100644
--- a/src/backend/statistics/Makefile
+++ b/src/backend/statistics/Makefile
@@ -13,6 +13,7 @@ top_builddir = ../../..
 include $(top_builddir)/src/Makefile.global
 
 OBJS = \
+	attribute_stats.o \
 	dependencies.o \
 	extended_stats.o \
 	mcv.o \
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
new file mode 100644
index 0000000000..0f95a1e46b
--- /dev/null
+++ b/src/backend/statistics/attribute_stats.c
@@ -0,0 +1,833 @@
+/*-------------------------------------------------------------------------
+ * attribute_stats.c
+ *
+ *	  PostgreSQL relation attribute statistics manipulation
+ *
+ * Code supporting the direct import of relation attribute statistics, similar
+ * to what is done by the ANALYZE command.
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *       src/backend/statistics/attribute_stats.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_collation.h"
+#include "catalog/pg_operator.h"
+#include "catalog/pg_type.h"
+#include "funcapi.h"
+#include "nodes/nodeFuncs.h"
+#include "statistics/statistics.h"
+#include "statistics/stats_utils.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/syscache.h"
+#include "utils/typcache.h"
+
+#define DEFAULT_NULL_FRAC      Float4GetDatum(0.0)
+#define DEFAULT_AVG_WIDTH      Int32GetDatum(0)        /* unknown */
+#define DEFAULT_N_DISTINCT     Float4GetDatum(0.0)     /* unknown */
+
+typedef enum
+{
+	ATTRELATION_ARG = 0,
+	ATTNAME_ARG,
+	INHERITED_ARG,
+	NULL_FRAC_ARG,
+	AVG_WIDTH_ARG,
+	N_DISTINCT_ARG,
+	MOST_COMMON_VALS_ARG,
+	MOST_COMMON_FREQS_ARG,
+	HISTOGRAM_BOUNDS_ARG,
+	CORRELATION_ARG,
+	MOST_COMMON_ELEMS_ARG,
+	MOST_COMMON_ELEM_FREQS_ARG,
+	ELEM_COUNT_HISTOGRAM_ARG,
+	RANGE_LENGTH_HISTOGRAM_ARG,
+	RANGE_EMPTY_FRAC_ARG,
+	RANGE_BOUNDS_HISTOGRAM_ARG,
+	NUM_ATTRIBUTE_STATS_ARGS
+} attribute_stats_argnum;
+
+static struct StatsArgInfo attarginfo[] =
+{
+	[ATTRELATION_ARG]			 = {"relation", REGCLASSOID},
+	[ATTNAME_ARG]				 = {"attname", NAMEOID},
+	[INHERITED_ARG]				 = {"inherited", BOOLOID},
+	[NULL_FRAC_ARG]				 = {"null_frac", FLOAT4OID},
+	[AVG_WIDTH_ARG]				 = {"avg_width", INT4OID},
+	[N_DISTINCT_ARG]			 = {"n_distinct", FLOAT4OID},
+	[MOST_COMMON_VALS_ARG]		 = {"most_common_vals", TEXTOID},
+	[MOST_COMMON_FREQS_ARG]		 = {"most_common_freqs", FLOAT4ARRAYOID},
+	[HISTOGRAM_BOUNDS_ARG]		 = {"histogram_bounds", TEXTOID},
+	[CORRELATION_ARG]			 = {"correlation", FLOAT4OID},
+	[MOST_COMMON_ELEMS_ARG]		 = {"most_common_elems", TEXTOID},
+	[MOST_COMMON_ELEM_FREQS_ARG] = {"most_common_elems_freq", FLOAT4ARRAYOID},
+	[ELEM_COUNT_HISTOGRAM_ARG]	 = {"elem_count_histogram", FLOAT4ARRAYOID},
+	[RANGE_LENGTH_HISTOGRAM_ARG] = {"range_length_histogram", TEXTOID},
+	[RANGE_EMPTY_FRAC_ARG]		 = {"range_empty_frac", FLOAT4OID},
+	[RANGE_BOUNDS_HISTOGRAM_ARG] = {"range_bounds_histogram", TEXTOID},
+	[NUM_ATTRIBUTE_STATS_ARGS]	 = {0}
+};
+
+static bool attribute_statistics_update(FunctionCallInfo fcinfo, int elevel);
+static Node * get_attr_expr(Relation rel, int attnum);
+static void get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
+							   Oid *atttypid, int32 *atttypmod,
+							   char *atttyptype, Oid *atttypcoll,
+							   Oid *eq_opr, Oid *lt_opr);
+static bool get_elem_stat_type(Oid atttypid, char atttyptype, int elevel,
+							   Oid *elemtypid, Oid *elem_eq_opr);
+static Datum text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d,
+							   Oid typid, int32 typmod, int elevel, bool *ok);
+static void set_stats_slot(Datum *values, bool *nulls,
+						   int16 stakind, Oid staop, Oid stacoll,
+						   Datum stanumbers, bool stanumbers_isnull,
+						   Datum stavalues, bool stavalues_isnull);
+static void upsert_pg_statistic(Relation starel, HeapTuple oldtup,
+								Datum values[], bool nulls[]);
+static bool delete_pg_statistic(Oid reloid, AttrNumber attnum, bool stainherit);
+
+/*
+ * Insert or Update Attribute Statistics
+ *
+ * Major errors, such as the table not existing, the attribute not existing,
+ * or a permissions failure are always reported at ERROR. Other errors, such
+ * as a conversion failure, are reported at 'elevel', and a partial update
+ * will result.
+ *
+ * See pg_statistic.h for an explanation of how each statistic kind is
+ * stored. Custom statistics kinds are not supported.
+ *
+ * Depending on the statistics kind, we need to derive information from the
+ * attribute for which we're storing the stats. For instance, the MCVs are
+ * stored as an anyarray, and the representation of the array needs to store
+ * the correct element type, which must be derived from the attribute.
+ */
+static bool
+attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
+{
+	Oid			reloid;
+	Name	    attname;
+	bool		inherited;
+	AttrNumber	attnum;
+
+	Relation	starel;
+	HeapTuple	statup;
+
+	Oid			atttypid;
+	int32		atttypmod;
+	char		atttyptype;
+	Oid			atttypcoll;
+	Oid			eq_opr;
+	Oid			lt_opr;
+
+	Oid			elemtypid = InvalidOid;
+	Oid			elem_eq_opr = InvalidOid;
+
+	FmgrInfo	array_in_fn;
+
+	Datum		values[Natts_pg_statistic];
+	bool		nulls[Natts_pg_statistic];
+
+	stats_check_required_arg(fcinfo, attarginfo, ATTRELATION_ARG);
+	reloid = PG_GETARG_OID(ATTRELATION_ARG);
+
+	stats_check_required_arg(fcinfo, attarginfo, ATTNAME_ARG);
+	attname = PG_GETARG_NAME(ATTNAME_ARG);
+	attnum = get_attnum(reloid, NameStr(*attname));
+
+	stats_check_required_arg(fcinfo, attarginfo, INHERITED_ARG);
+	inherited = PG_GETARG_BOOL(INHERITED_ARG);
+
+	/*
+	 * Check argument sanity. If some arguments are unusable, emit at elevel
+	 * and set the corresponding argument to NULL in fcinfo.
+	 *
+	 * NB: may modify fcinfo
+	 */
+
+	stats_check_arg_array(fcinfo, attarginfo, MOST_COMMON_FREQS_ARG, elevel);
+	stats_check_arg_array(fcinfo, attarginfo, MOST_COMMON_ELEM_FREQS_ARG, elevel);
+	stats_check_arg_array(fcinfo, attarginfo, ELEM_COUNT_HISTOGRAM_ARG, elevel);
+
+	/* STATISTIC_KIND_MCV */
+	stats_check_arg_pair(fcinfo, attarginfo,
+						 MOST_COMMON_VALS_ARG, MOST_COMMON_FREQS_ARG,
+						 elevel);
+
+	/* STATISTIC_KIND_MCELEM */
+	stats_check_arg_pair(fcinfo, attarginfo,
+						 MOST_COMMON_ELEMS_ARG, MOST_COMMON_ELEM_FREQS_ARG,
+						 elevel);
+
+	/* STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM */
+	stats_check_arg_pair(fcinfo, attarginfo,
+						 RANGE_LENGTH_HISTOGRAM_ARG, RANGE_EMPTY_FRAC_ARG,
+						 elevel);
+
+	stats_lock_check_privileges(reloid);
+
+	/* derive information from attribute */
+	get_attr_stat_type(reloid, attnum, elevel,
+					   &atttypid, &atttypmod,
+					   &atttyptype, &atttypcoll,
+					   &eq_opr, &lt_opr);
+
+	/* if needed, derive element type */
+	if (!PG_ARGISNULL(MOST_COMMON_ELEMS_ARG) ||
+		!PG_ARGISNULL(ELEM_COUNT_HISTOGRAM_ARG))
+	{
+		if (!get_elem_stat_type(atttypid, atttyptype, elevel,
+								&elemtypid, &elem_eq_opr))
+		{
+			ereport(elevel,
+					(errmsg("unable to determine element type of attribute \"%s\"", NameStr(*attname)),
+					 errdetail("Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.")));
+			elemtypid = InvalidOid;
+			elem_eq_opr = InvalidOid;
+			fcinfo->args[MOST_COMMON_ELEMS_ARG].isnull = true;
+			fcinfo->args[ELEM_COUNT_HISTOGRAM_ARG].isnull = true;
+		}
+	}
+
+	/* histogram and correlation require less-than operator */
+	if ((!PG_ARGISNULL(HISTOGRAM_BOUNDS_ARG) || !PG_ARGISNULL(CORRELATION_ARG)) &&
+		!OidIsValid(lt_opr))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("could not determine less-than operator for attribute \"%s\"", NameStr(*attname)),
+				 errdetail("Cannot set STATISTIC_KIND_HISTOGRAM or STATISTIC_KIND_CORRELATION.")));
+		fcinfo->args[HISTOGRAM_BOUNDS_ARG].isnull = true;
+		fcinfo->args[CORRELATION_ARG].isnull = true;
+	}
+
+	/* only range types can have range stats */
+	if ((!PG_ARGISNULL(RANGE_LENGTH_HISTOGRAM_ARG) || !PG_ARGISNULL(RANGE_BOUNDS_HISTOGRAM_ARG)) &&
+		!(atttyptype == TYPTYPE_RANGE || atttyptype == TYPTYPE_MULTIRANGE))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("attribute \"%s\" is not a range type", NameStr(*attname)),
+				 errdetail("Cannot set STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM or STATISTIC_KIND_BOUNDS_HISTOGRAM.")));
+
+		fcinfo->args[RANGE_LENGTH_HISTOGRAM_ARG].isnull = true;
+		fcinfo->args[RANGE_EMPTY_FRAC_ARG].isnull = true;
+	}
+
+	fmgr_info(F_ARRAY_IN, &array_in_fn);
+
+	starel = table_open(StatisticRelationId, RowExclusiveLock);
+
+	statup = SearchSysCache3(STATRELATTINH, reloid, attnum, inherited);
+	if (HeapTupleIsValid(statup))
+	{
+		/* initialize from existing tuple */
+		heap_deform_tuple(statup, RelationGetDescr(starel), values, nulls);
+	}
+	else
+	{
+		/*
+		 * Initialize nulls array to be false for all non-NULL attributes, and
+		 * true for all nullable attributes.
+		 */
+		for (int i = 0; i < Natts_pg_statistic; i++)
+		{
+			values[i] = (Datum) 0;
+			if (i < Anum_pg_statistic_stanumbers1 - 1)
+				nulls[i] = false;
+			else
+				nulls[i] = true;
+		}
+
+		values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(reloid);
+		values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(attnum);
+		values[Anum_pg_statistic_stainherit - 1] = BoolGetDatum(inherited);
+		values[Anum_pg_statistic_stanullfrac - 1] = DEFAULT_NULL_FRAC;
+		values[Anum_pg_statistic_stawidth - 1] = DEFAULT_AVG_WIDTH;
+		values[Anum_pg_statistic_stadistinct - 1] = DEFAULT_N_DISTINCT;
+	}
+
+	if (!PG_ARGISNULL(NULL_FRAC_ARG))
+		values[Anum_pg_statistic_stanullfrac - 1] = PG_GETARG_DATUM(NULL_FRAC_ARG);
+	if (!PG_ARGISNULL(AVG_WIDTH_ARG))
+		values[Anum_pg_statistic_stawidth - 1] = PG_GETARG_DATUM(AVG_WIDTH_ARG);
+	if (!PG_ARGISNULL(N_DISTINCT_ARG))
+		values[Anum_pg_statistic_stadistinct - 1] = PG_GETARG_DATUM(N_DISTINCT_ARG);
+
+	/*
+	 * STATISTIC_KIND_MCV
+	 * 
+	 * Convert most_common_vals from text to anyarray, where the element type
+	 * is the attribute type, and store in stavalues. Store most_common_freqs
+	 * in stanumbers.
+	 */
+	if (!PG_ARGISNULL(MOST_COMMON_VALS_ARG))
+	{
+		bool		converted;
+		Datum		stanumbers = PG_GETARG_DATUM(MOST_COMMON_FREQS_ARG);
+		Datum		stavalues = text_to_stavalues("most_common_vals",
+												  &array_in_fn,
+												  PG_GETARG_DATUM(MOST_COMMON_VALS_ARG),
+												  atttypid, atttypmod,
+												  elevel, &converted);
+
+		if (converted)
+		{
+			set_stats_slot(values, nulls,
+						   STATISTIC_KIND_MCV,
+						   eq_opr, atttypcoll,
+						   stanumbers, false, stavalues, false);
+		}
+		else
+		{
+			fcinfo->args[MOST_COMMON_VALS_ARG].isnull = true;
+			fcinfo->args[MOST_COMMON_FREQS_ARG].isnull = true;
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_HISTOGRAM
+	 *
+	 * histogram_bounds: ANYARRAY::text
+	 */
+	if (!PG_ARGISNULL(HISTOGRAM_BOUNDS_ARG))
+	{
+		Datum		stavalues;
+		bool		converted = false;
+
+		stavalues = text_to_stavalues("histogram_bounds",
+									  &array_in_fn,
+									  PG_GETARG_DATUM(HISTOGRAM_BOUNDS_ARG),
+									  atttypid, atttypmod, elevel,
+									  &converted);
+
+		if (converted)
+		{
+			set_stats_slot(values, nulls,
+						   STATISTIC_KIND_HISTOGRAM,
+						   lt_opr, atttypcoll,
+						   0, true, stavalues, false);
+		}
+		else
+			fcinfo->args[HISTOGRAM_BOUNDS_ARG].isnull = true;
+	}
+
+	/*
+	 * STATISTIC_KIND_CORRELATION
+	 *
+	 * correlation: real
+	 */
+	if (!PG_ARGISNULL(CORRELATION_ARG))
+	{
+		Datum		elems[] = {PG_GETARG_DATUM(CORRELATION_ARG)};
+		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+		Datum		stanumbers = PointerGetDatum(arry);
+
+		set_stats_slot(values, nulls,
+					   STATISTIC_KIND_CORRELATION,
+					   lt_opr, atttypcoll,
+					   stanumbers, false, 0, true);
+	}
+
+	/*
+	 * STATISTIC_KIND_MCELEM
+	 *
+	 * most_common_elem_freqs: real[]
+	 *
+	 * most_common_elems     : ANYARRAY::text
+	 */
+	if (!PG_ARGISNULL(MOST_COMMON_ELEMS_ARG))
+	{
+		Datum		stanumbers = PG_GETARG_DATUM(MOST_COMMON_ELEM_FREQS_ARG);
+		bool		converted = false;
+		Datum		stavalues;
+
+		stavalues = text_to_stavalues("most_common_elems",
+									  &array_in_fn,
+									  PG_GETARG_DATUM(MOST_COMMON_ELEMS_ARG),
+									  elemtypid, atttypmod,
+									  elevel, &converted);
+
+		if (converted)
+		{
+			set_stats_slot(values, nulls,
+						   STATISTIC_KIND_MCELEM,
+						   elem_eq_opr, atttypcoll,
+						   stanumbers, false, stavalues, false);
+		}
+		else
+		{
+			/* the mc_elem stat did not write */
+			fcinfo->args[MOST_COMMON_ELEMS_ARG].isnull = true;
+			fcinfo->args[MOST_COMMON_ELEM_FREQS_ARG].isnull = true;
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_DECHIST
+	 *
+	 * elem_count_histogram:	real[]
+	 */
+	if (!PG_ARGISNULL(ELEM_COUNT_HISTOGRAM_ARG))
+	{
+		Datum		stanumbers = PG_GETARG_DATUM(ELEM_COUNT_HISTOGRAM_ARG);
+
+		set_stats_slot(values, nulls,
+					   STATISTIC_KIND_DECHIST,
+					   elem_eq_opr, atttypcoll,
+					   stanumbers, false, 0, true);
+	}
+
+	/*
+	 * STATISTIC_KIND_BOUNDS_HISTOGRAM
+	 *
+	 * range_bounds_histogram: ANYARRAY::text
+	 *
+	 * This stakind appears before STATISTIC_KIND_BOUNDS_HISTOGRAM even though
+	 * it is numerically greater, and all other stakinds appear in numerical
+	 * order. We duplicate this quirk to make before/after tests of
+	 * pg_statistic records easier.
+	 */
+	if (!PG_ARGISNULL(RANGE_BOUNDS_HISTOGRAM_ARG))
+	{
+		bool		converted = false;
+		Datum		stavalues;
+
+		stavalues = text_to_stavalues("range_bounds_histogram",
+									  &array_in_fn,
+									  PG_GETARG_DATUM(RANGE_BOUNDS_HISTOGRAM_ARG),
+									  atttypid, atttypmod,
+									  elevel, &converted);
+
+		if (converted)
+		{
+			set_stats_slot(values, nulls,
+						   STATISTIC_KIND_BOUNDS_HISTOGRAM,
+						   InvalidOid, InvalidOid,
+						   0, true, stavalues, false);
+		}
+		else
+			fcinfo->args[RANGE_BOUNDS_HISTOGRAM_ARG].isnull = true;
+	}
+
+	/*
+	 * STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM
+	 *
+	 * range_empty_frac: real
+	 *
+	 * range_length_histogram:  double precision[]::text
+	 */
+	if (!PG_ARGISNULL(RANGE_LENGTH_HISTOGRAM_ARG))
+	{
+		/* The anyarray is always a float8[] for this stakind */
+		Datum		elems[] = {PG_GETARG_DATUM(RANGE_EMPTY_FRAC_ARG)};
+		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+		Datum		stanumbers = PointerGetDatum(arry);
+
+		bool		converted = false;
+		Datum		stavalues;
+
+		stavalues = text_to_stavalues("range_length_histogram",
+									  &array_in_fn,
+									  PG_GETARG_DATUM(RANGE_LENGTH_HISTOGRAM_ARG),
+									  FLOAT8OID, 0, elevel, &converted);
+
+		if (converted)
+		{
+			set_stats_slot(values, nulls,
+						   STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM,
+						   Float8LessOperator, InvalidOid,
+						   stanumbers, false, stavalues, false);
+		}
+		else
+		{
+			fcinfo->args[RANGE_EMPTY_FRAC_ARG].isnull = true;
+			fcinfo->args[RANGE_LENGTH_HISTOGRAM_ARG].isnull = true;
+		}
+	}
+
+	upsert_pg_statistic(starel, statup, values, nulls);
+
+	if (HeapTupleIsValid(statup))
+		ReleaseSysCache(statup);
+	table_close(starel, RowExclusiveLock);
+
+	return true;
+}
+
+/*
+ * If this relation is an index and that index has expressions in it, and
+ * the attnum specified is known to be an expression, then we must walk
+ * the list attributes up to the specified attnum to get the right
+ * expression.
+ */
+static Node *
+get_attr_expr(Relation rel, int attnum)
+{
+	if ((rel->rd_rel->relkind == RELKIND_INDEX
+		 || (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+		&& (rel->rd_indexprs != NIL)
+		&& (rel->rd_index->indkey.values[attnum - 1] == 0))
+	{
+		ListCell   *indexpr_item = list_head(rel->rd_indexprs);
+
+		for (int i = 0; i < attnum - 1; i++)
+			if (rel->rd_index->indkey.values[i] == 0)
+				indexpr_item = lnext(rel->rd_indexprs, indexpr_item);
+
+		if (indexpr_item == NULL)	/* shouldn't happen */
+			elog(ERROR, "too few entries in indexprs list");
+
+		return (Node *) lfirst(indexpr_item);
+	}
+	return NULL;
+}
+
+/*
+ * Derive type information from the attribute.
+ */
+static void
+get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
+				   Oid *atttypid, int32 *atttypmod,
+				   char *atttyptype, Oid *atttypcoll,
+				   Oid *eq_opr, Oid *lt_opr)
+{
+	Relation rel = relation_open(reloid, AccessShareLock);
+	Form_pg_attribute attr;
+	HeapTuple	atup;
+	Node	   *expr;
+	TypeCacheEntry *typcache;
+
+	atup = SearchSysCache2(ATTNUM, ObjectIdGetDatum(reloid),
+						   Int16GetDatum(attnum));
+
+	/* Attribute not found */
+	if (!HeapTupleIsValid(atup))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_COLUMN),
+				 errmsg("attribute %d of relation \"%s\" does not exist",
+						attnum, RelationGetRelationName(rel))));
+
+	attr = (Form_pg_attribute) GETSTRUCT(atup);
+
+	if (attr->attisdropped)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_COLUMN),
+				 errmsg("attribute %d of relation \"%s\" does not exist",
+						attnum, RelationGetRelationName(rel))));
+
+	expr = get_attr_expr(rel, attr->attnum);
+
+	/*
+	 * 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.
+	 * This mimics the behvior of examine_attribute().
+	 */
+	if (expr == NULL)
+	{
+		*atttypid = attr->atttypid;
+		*atttypmod = attr->atttypmod;
+		*atttypcoll = attr->attcollation;
+	}
+	else
+	{
+		*atttypid = exprType(expr);
+		*atttypmod = exprTypmod(expr);
+
+		if (OidIsValid(attr->attcollation))
+			*atttypcoll = attr->attcollation;
+		else
+			*atttypcoll = exprCollation(expr);
+	}
+	ReleaseSysCache(atup);
+
+	/*
+	 * If it's a multirange, step down to the range type, as is done
+	 * by multirange_typanalyze().
+	 */
+	if (type_is_multirange(*atttypid))
+		*atttypid = get_multirange_range(*atttypid);
+
+	typcache = lookup_type_cache(*atttypid, TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR);
+	*atttyptype = typcache->typtype;
+	*eq_opr = typcache->eq_opr;
+	*lt_opr = typcache->lt_opr;
+
+	/*
+	 * compute_tsvector_stats() sets the collation to the default,
+	 * Duplicate that behavior.
+	 */
+	if (*atttypid == TSVECTOROID)
+		*atttypcoll = DEFAULT_COLLATION_OID;
+
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Derive element type information from the attribute type.
+ */
+static bool
+get_elem_stat_type(Oid atttypid, char atttyptype, int elevel,
+				   Oid *elemtypid, Oid *elem_eq_opr)
+{
+	TypeCacheEntry *elemtypcache;
+
+	/*
+	 * Handle special case for tsvectors as done in compute_tsvector_stats().
+	 */
+	if (atttypid == TSVECTOROID)
+		*elemtypid = TEXTOID;
+	else if (atttyptype == TYPTYPE_RANGE)
+		*elemtypid = get_range_subtype(atttypid);
+	else
+		*elemtypid = get_base_element_type(atttypid);
+
+	if (!OidIsValid(*elemtypid))
+		return false;
+
+	elemtypcache = lookup_type_cache(*elemtypid, TYPECACHE_EQ_OPR);
+	if (!OidIsValid(elemtypcache->eq_opr))
+		return false;
+
+	*elem_eq_opr = elemtypcache->eq_opr;
+
+	return true;
+}
+
+/*
+ * Cast a text datum into an array with element type elemtypid.
+ *
+ * If an error is encountered, capture it and re-throw at elevel, and set ok
+ * to false. If the resulting array contains NULLs, raise an error at elevel
+ * and set ok to false. Otherwise, set ok to true.
+ */
+static Datum
+text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid typid,
+				  int32 typmod, int elevel, bool *ok)
+{
+	LOCAL_FCINFO(fcinfo, 8);
+	char	   *s;
+	Datum		result;
+	ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+	escontext.details_wanted = true;
+
+	s = TextDatumGetCString(d);
+
+	InitFunctionCallInfoData(*fcinfo, array_in, 3, InvalidOid,
+							 (Node *) &escontext, NULL);
+
+	fcinfo->args[0].value = CStringGetDatum(s);
+	fcinfo->args[0].isnull = false;
+	fcinfo->args[1].value = ObjectIdGetDatum(typid);
+	fcinfo->args[1].isnull = false;
+	fcinfo->args[2].value = Int32GetDatum(typmod);
+	fcinfo->args[2].isnull = false;
+
+	result = FunctionCallInvoke(fcinfo);
+
+	pfree(s);
+
+	if (SOFT_ERROR_OCCURRED(&escontext))
+	{
+		if (elevel != ERROR)
+			escontext.error_data->elevel = elevel;
+		ThrowErrorData(escontext.error_data);
+		*ok = false;
+		return (Datum)0;
+	}
+
+	if (array_contains_nulls(DatumGetArrayTypeP(result)))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("\"%s\" array cannot contain NULL values", staname)));
+		*ok = false;
+		return (Datum)0;
+	}
+
+	*ok = true;
+
+	return result;
+}
+
+/*
+ * Find and update the slot with the given stakind, or use the first empty
+ * slot.
+ */
+static void
+set_stats_slot(Datum *values, bool *nulls,
+			   int16 stakind, Oid staop, Oid stacoll,
+			   Datum stanumbers, bool stanumbers_isnull,
+			   Datum stavalues, bool stavalues_isnull)
+{
+	int slotidx;
+	int first_empty = -1;
+
+	/* find existing slot with given stakind */
+	for (slotidx = 0; slotidx < STATISTIC_NUM_SLOTS; slotidx++)
+	{
+		AttrNumber stakind_attnum = Anum_pg_statistic_stakind1 - 1 + slotidx;
+		if (first_empty < 0 &&
+			DatumGetInt16(values[stakind_attnum]) == 0)
+			first_empty = slotidx;
+		if (DatumGetInt16(values[stakind_attnum]) == stakind)
+			break;
+	}
+
+	if (slotidx >= STATISTIC_NUM_SLOTS && first_empty >= 0)
+		slotidx = first_empty;
+
+	if (slotidx >= STATISTIC_NUM_SLOTS)
+		ereport(ERROR,
+				(errmsg("maximum number of statistics slots exceeded: %d",
+						slotidx + 1)));
+
+	values[Anum_pg_statistic_stakind1 - 1 + slotidx] = stakind;
+	values[Anum_pg_statistic_staop1 - 1 + slotidx] = staop;
+	values[Anum_pg_statistic_stacoll1 - 1 + slotidx] = stacoll;
+
+	if (!stanumbers_isnull)
+	{
+		values[Anum_pg_statistic_stanumbers1 - 1 + slotidx] = stanumbers;
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + slotidx] = false;
+	}
+	if (!stavalues_isnull)
+	{
+		values[Anum_pg_statistic_stavalues1 - 1 + slotidx] = stavalues;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + slotidx] = false;
+	}
+}
+
+/*
+ * Upsert the pg_statistic record.
+ */
+static void
+upsert_pg_statistic(Relation starel, HeapTuple oldtup,
+					Datum values[], bool nulls[])
+{
+	HeapTuple newtup;
+
+	if (HeapTupleIsValid(oldtup))
+	{
+		bool		replaces[Natts_pg_statistic];
+
+		for (int i = 0; i < Natts_pg_statistic; i++)
+			replaces[i] = true;
+
+		newtup = heap_modify_tuple(oldtup, RelationGetDescr(starel),
+								   values, nulls, replaces);
+		CatalogTupleUpdate(starel, &newtup->t_self, newtup);
+	}
+	else
+	{
+		newtup = heap_form_tuple(RelationGetDescr(starel), values, nulls);
+		CatalogTupleInsert(starel, newtup);
+	}
+
+	heap_freetuple(newtup);
+}
+
+/*
+ * Delete pg_statistic record.
+ */
+static bool
+delete_pg_statistic(Oid reloid, AttrNumber attnum, bool stainherit)
+{
+	Relation	sd = table_open(StatisticRelationId, RowExclusiveLock);
+	HeapTuple	oldtup;
+
+	/* Is there already a pg_statistic tuple for this attribute? */
+	oldtup = SearchSysCache3(STATRELATTINH,
+							 ObjectIdGetDatum(reloid),
+							 Int16GetDatum(attnum),
+							 BoolGetDatum(stainherit));
+
+	if (HeapTupleIsValid(oldtup))
+	{
+		CatalogTupleDelete(sd, &oldtup->t_self);
+		ReleaseSysCache(oldtup);
+		table_close(sd, RowExclusiveLock);
+		return true;
+	}
+
+	table_close(sd, RowExclusiveLock);
+	return false;
+}
+
+/*
+ * Import statistics for a given relation attribute.
+ *
+ * This will insert/replace a row in pg_statistic for the given relation and
+ * attribute name.
+ *
+ * The function takes input parameters that correspond to columns in the view
+ * pg_stats.
+ *
+ * Of those, the columns attname, inherited, null_frac, avg_width, and
+ * n_distinct all correspond to NOT NULL columns in pg_statistic. These
+ * parameters have no default value and passing NULL to them will result
+ * in an error.
+ *
+ * If there is no attribute with a matching attname in the relation, the
+ * function will raise an error. Likewise for setting inherited statistics
+ * on a table that is not partitioned.
+ *
+ * The remaining parameters all belong to a specific stakind. Some stakinds
+ * have multiple parameters, and in those cases both parameters must be
+ * NOT NULL or both NULL, otherwise an error will be raised.
+ *
+ * Omitting a parameter or explicitly passing NULL means that that particular
+ * stakind is not associated with the attribute.
+ *
+ * Parameters that are NOT NULL will be inspected for consistency checks,
+ * any of which can raise an error.
+ *
+ * Parameters corresponding to ANYARRAY columns are instead passed in as text
+ * values, which is a valid input string for an array of the type or element
+ * type of the attribute. Any error generated by the array_in() function will
+ * in turn fail the function.
+ */
+Datum
+pg_set_attribute_stats(PG_FUNCTION_ARGS)
+{
+	PG_RETURN_BOOL(attribute_statistics_update(fcinfo, ERROR));
+}
+
+/*
+ * Delete statistics for the given attribute.
+ */
+Datum
+pg_clear_attribute_stats(PG_FUNCTION_ARGS)
+{
+	Oid			reloid;
+	Name		attname;
+	AttrNumber	attnum;
+	bool		inherited;
+
+	stats_check_required_arg(fcinfo, attarginfo, ATTRELATION_ARG);
+	reloid = PG_GETARG_OID(ATTRELATION_ARG);
+
+	stats_check_required_arg(fcinfo, attarginfo, ATTNAME_ARG);
+	attname = PG_GETARG_NAME(ATTNAME_ARG);
+	attnum = get_attnum(reloid, NameStr(*attname));
+
+	stats_check_required_arg(fcinfo, attarginfo, INHERITED_ARG);
+	inherited = PG_GETARG_BOOL(INHERITED_ARG);
+
+	stats_lock_check_privileges(reloid);
+
+	PG_RETURN_BOOL(delete_pg_statistic(reloid, attnum, inherited));
+}
diff --git a/src/backend/statistics/meson.build b/src/backend/statistics/meson.build
index 376bd2bee1..8ba8c39de8 100644
--- a/src/backend/statistics/meson.build
+++ b/src/backend/statistics/meson.build
@@ -1,6 +1,7 @@
 # Copyright (c) 2022-2024, PostgreSQL Global Development Group
 
 backend_sources += files(
+	'attribute_stats.c',
   'dependencies.c',
   'extended_stats.c',
   'mcv.c',
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index 61223949f6..1a1a6fc6ff 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -133,9 +133,664 @@ SELECT
     pg_catalog.pg_clear_relation_stats(
         'stats_import.testview'::regclass);
 ERROR:  cannot modify statistics for relations of kind 'v'
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => '0'::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  could not open relation with OID 0
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => NULL::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  "relation" cannot be NULL
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => NULL::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  "attname" cannot be NULL
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => NULL::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  "inherited" cannot be NULL
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => NULL::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => NULL::integer,
+    n_distinct => 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => NULL::real);
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT stanullfrac, stawidth, stadistinct
+FROM pg_statistic
+WHERE starelid = 'stats_import.test'::regclass;
+ stanullfrac | stawidth | stadistinct 
+-------------+----------+-------------
+         0.1 |        2 |         0.3
+(1 row)
+
+-- warn: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_freqs => '{0.1,0.2,0.3}'::real[]
+    );
+ERROR:  "most_common_vals" must be specified when "most_common_freqs" is specified
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{1,2,3}'::text
+    );
+ERROR:  "most_common_freqs" must be specified when "most_common_vals" is specified
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2023-09-30,2024-10-31,3}'::text,
+    most_common_freqs => '{0.2,0.1}'::real[]
+    );
+ERROR:  invalid input syntax for type integer: "2023-09-30"
+-- warning: mcv cast failure
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,four,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+ERROR:  invalid input syntax for type integer: "four"
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: histogram elements null value
+-- this generates no warnings, but perhaps it should
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,NULL,3,4}'::text
+    );
+ERROR:  "histogram_bounds" array cannot contain NULL values
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,2,3,4}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 0.5::real);
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |         0.5 |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have mcelem
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{1,3}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+ERROR:  unable to determine element type of attribute "id"
+DETAIL:  Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,two}'::text
+    );
+ERROR:  "most_common_elems_freq" must be specified when "most_common_elems" is specified
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
+    );
+ERROR:  "most_common_elems" must be specified when "most_common_elems_freq" is specified
+-- ok: mcelem
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             | {one,three}       | {0.3,0.2,0.2,0.3,0}    |                      |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ERROR:  unable to determine element type of attribute "id"
+DETAIL:  Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ERROR:  "elem_count_histogram" array cannot contain NULL values
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
+ stats_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             | {one,three}       | {0.3,0.2,0.2,0.3,0}    | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+(1 row)
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ERROR:  attribute "id" is not a range type
+DETAIL:  Cannot set STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM or STATISTIC_KIND_BOUNDS_HISTOGRAM.
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ERROR:  "range_empty_frac" must be specified when "range_length_histogram" is specified
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real
+    );
+ERROR:  "range_length_histogram" must be specified when "range_empty_frac" is specified
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
+ stats_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
+(1 row)
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ERROR:  attribute "id" is not a range type
+DETAIL:  Cannot set STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM or STATISTIC_KIND_BOUNDS_HISTOGRAM.
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
+ stats_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+(1 row)
+
+-- warn: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{"[2,3)","[1,2)","[3,4)"}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[],
+    histogram_bounds => '{"[1,2)","[2,3)","[3,4)","[4,5)"}'::text,
+    correlation => 1.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    range_empty_frac => -0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ERROR:  maximum number of statistics slots exceeded: 6
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
+-- Generate statistics on table with data
+ANALYZE stats_import.test;
+CREATE TABLE stats_import.test_clone ( LIKE stats_import.test );
+CREATE INDEX is_odd_clone ON stats_import.test_clone(((comp).a % 2 = 1));
+--
+-- Copy stats from test to test_clone, and is_odd to is_odd_clone
+--
+SELECT s.schemaname, s.tablename, s.attname, s.inherited
+FROM pg_catalog.pg_stats AS s
+CROSS JOIN LATERAL
+    pg_catalog.pg_set_attribute_stats(
+        relation => ('stats_import.' || s.tablename || '_clone')::regclass::oid,
+        attname => s.attname,
+        inherited => s.inherited,
+        null_frac => s.null_frac,
+        avg_width => s.avg_width,
+        n_distinct => s.n_distinct,
+        most_common_vals => s.most_common_vals::text,
+        most_common_freqs => s.most_common_freqs,
+        histogram_bounds => s.histogram_bounds::text,
+        correlation => s.correlation,
+        most_common_elems => s.most_common_elems::text,
+        most_common_elem_freqs => s.most_common_elem_freqs,
+        elem_count_histogram => s.elem_count_histogram,
+        range_bounds_histogram => s.range_bounds_histogram::text,
+        range_empty_frac => s.range_empty_frac,
+        range_length_histogram => s.range_length_histogram::text) AS r
+WHERE s.schemaname = 'stats_import'
+AND s.tablename IN ('test', 'is_odd')
+ORDER BY s.tablename, s.attname, s.inherited;
+  schemaname  | tablename | attname | inherited 
+--------------+-----------+---------+-----------
+ stats_import | is_odd    | expr    | f
+ stats_import | test      | arange  | f
+ stats_import | test      | comp    | f
+ stats_import | test      | id      | f
+ stats_import | test      | name    | f
+ stats_import | test      | tags    | f
+(6 rows)
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+   relname    | num_stats 
+--------------+-----------
+ is_odd       |         1
+ is_odd_clone |         1
+ test         |         5
+ test_clone   |         5
+(4 rows)
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
 DROP SCHEMA stats_import CASCADE;
-NOTICE:  drop cascades to 4 other objects
+NOTICE:  drop cascades to 5 other objects
 DETAIL:  drop cascades to type stats_import.complex_type
 drop cascades to table stats_import.test
 drop cascades to sequence stats_import.testseq
 drop cascades to view stats_import.testview
+drop cascades to table stats_import.test_clone
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index 3e9f6d9124..5651f133c0 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -95,4 +95,548 @@ SELECT
     pg_catalog.pg_clear_relation_stats(
         'stats_import.testview'::regclass);
 
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => '0'::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => NULL::oid,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => NULL::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => NULL::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => NULL::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => NULL::integer,
+    n_distinct => 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => NULL::real);
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+SELECT stanullfrac, stawidth, stadistinct
+FROM pg_statistic
+WHERE starelid = 'stats_import.test'::regclass;
+
+-- warn: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_freqs => '{0.1,0.2,0.3}'::real[]
+    );
+
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{1,2,3}'::text
+    );
+
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2023-09-30,2024-10-31,3}'::text,
+    most_common_freqs => '{0.2,0.1}'::real[]
+    );
+
+-- warning: mcv cast failure
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,four,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{2,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: histogram elements null value
+-- this generates no warnings, but perhaps it should
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,NULL,3,4}'::text
+    );
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    histogram_bounds => '{1,2,3,4}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 0.5::real);
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: scalars can't have mcelem
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{1,3}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,two}'::text
+    );
+
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
+    );
+
+-- ok: mcelem
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_elems => '{one,three}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- warn: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'tags'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real
+    );
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_empty_frac => 0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'arange'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    most_common_vals => '{"[2,3)","[1,2)","[3,4)"}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[],
+    histogram_bounds => '{"[1,2)","[2,3)","[3,4)","[4,5)"}'::text,
+    correlation => 1.1::real,
+    most_common_elems => '{3,1}'::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[],
+    range_empty_frac => -0.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text,
+    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+
+CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
+
+-- Generate statistics on table with data
+ANALYZE stats_import.test;
+
+CREATE TABLE stats_import.test_clone ( LIKE stats_import.test );
+
+CREATE INDEX is_odd_clone ON stats_import.test_clone(((comp).a % 2 = 1));
+
+--
+-- Copy stats from test to test_clone, and is_odd to is_odd_clone
+--
+SELECT s.schemaname, s.tablename, s.attname, s.inherited
+FROM pg_catalog.pg_stats AS s
+CROSS JOIN LATERAL
+    pg_catalog.pg_set_attribute_stats(
+        relation => ('stats_import.' || s.tablename || '_clone')::regclass::oid,
+        attname => s.attname,
+        inherited => s.inherited,
+        null_frac => s.null_frac,
+        avg_width => s.avg_width,
+        n_distinct => s.n_distinct,
+        most_common_vals => s.most_common_vals::text,
+        most_common_freqs => s.most_common_freqs,
+        histogram_bounds => s.histogram_bounds::text,
+        correlation => s.correlation,
+        most_common_elems => s.most_common_elems::text,
+        most_common_elem_freqs => s.most_common_elem_freqs,
+        elem_count_histogram => s.elem_count_histogram,
+        range_bounds_histogram => s.range_bounds_histogram::text,
+        range_empty_frac => s.range_empty_frac,
+        range_length_histogram => s.range_length_histogram::text) AS r
+WHERE s.schemaname = 'stats_import'
+AND s.tablename IN ('test', 'is_odd')
+ORDER BY s.tablename, s.attname, s.inherited;
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- check test minus test_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test_clone'::regclass;
+
+-- check test_clone minus test
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test'::regclass;
+
+-- check is_odd minus is_odd_clone
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd_clone'::regclass;
+
+-- check is_odd_clone minus is_odd
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd'::regclass;
+
 DROP SCHEMA stats_import CASCADE;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 8a715934fe..672d478a90 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30226,6 +30226,104 @@ DETAIL:  Make sure pg_wal_replay_wait() isn't called within a transaction with a
         </warning>
        </entry>
       </row>
+
+      <row>
+       <entry role="func_table_entry">
+        <para role="func_signature">
+         <indexterm>
+          <primary>pg_set_attribute_stats</primary>
+         </indexterm>
+         <function>pg_set_attribute_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>attname</parameter> <type>name</type>,
+         <parameter>inherited</parameter> <type>boolean</type>
+         <optional>, <parameter>null_frac</parameter> <type>real</type></optional>
+         <optional>, <parameter>avg_width</parameter> <type>integer</type></optional>
+         <optional>, <parameter>n_distinct</parameter> <type>real</type></optional>
+         <optional>, <parameter>most_common_vals</parameter> <type>text</type>, <parameter>most_common_freqs</parameter> <type>real[]</type> </optional>
+         <optional>, <parameter>histogram_bounds</parameter> <type>text</type> </optional>
+         <optional>, <parameter>correlation</parameter> <type>real</type> </optional>
+         <optional>, <parameter>most_common_elems</parameter> <type>text</type>, <parameter>most_common_elem_freqs</parameter> <type>real[]</type> </optional>
+         <optional>, <parameter>elem_count_histogram</parameter> <type>real[]</type> </optional>
+         <optional>, <parameter>range_length_histogram</parameter> <type>text</type> </optional>
+         <optional>, <parameter>range_empty_frac</parameter> <type>real</type> </optional>
+         <optional>, <parameter>range_bounds_histogram</parameter> <type>text</type> </optional> )
+         <returnvalue>booolean</returnvalue>
+        </para>
+        <para>
+         Creates or updates attribute-level statistics for the given relation
+         and attribute name to the specified values. The parameters correspond
+         to to attributes of the same name found in the <link
+         linkend="view-pg-stats"><structname>pg_stats</structname></link>
+         view. Returns <literal>true</literal> if a change was made;
+         <literal>false</literal> otherwise.
+        </para>
+        <para>
+         All parameters default to <literal>NULL</literal>, which leave the
+         corresponding statistic unchanged.
+        </para>
+        <para>
+         Ordinarily, these statistics are collected automatically or updated
+         as a part of <xref linkend="sql-vacuum"/> or <xref
+         linkend="sql-analyze"/>, so it's not necessary to call this
+         function. However, it may be useful when testing the effects of
+         statistics on the planner to understand or anticipate plan changes.
+        </para>
+        <para>
+         The caller must have the <literal>MAINTAIN</literal> privilege on
+         the table or be the owner of the database.
+        </para>
+        <warning>
+         <para>
+          Changes made by this function are likely to be overwritten by <link
+          linkend="autovacuum">autovacuum</link> (or manual
+          <command>VACUUM</command> or <command>ANALYZE</command>) and should
+          be considered temporary.
+         </para>
+         <para>
+          The signature of this function may change in new major releases if
+          there are changes to the statistics being tracked.
+         </para>
+        </warning>
+       </entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry">
+        <para role="func_signature">
+         <indexterm>
+          <primary>pg_clear_attribute_stats</primary>
+         </indexterm>
+         <function>pg_clear_attribute_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>attname</parameter> <type>name</type>,
+         <parameter>inherited</parameter> <type>boolean</type> )
+         <returnvalue>boolean</returnvalue>
+        </para>
+        <para>
+         Clears table-level statistics for the given relation attribute, as
+         though the table was newly created. Returns <literal>true</literal>
+         if a change was made; <literal>false</literal> otherwise.
+        </para>
+        <para>
+         The caller must have the <literal>MAINTAIN</literal> privilege on
+         the table or be the owner of the database.
+        </para>
+        <warning>
+         <para>
+          Changes made by this function are likely to be overwritten by <link
+          linkend="autovacuum">autovacuum</link> (or manual
+          <command>VACUUM</command> or <command>ANALYZE</command>) and should
+          be considered temporary.
+         </para>
+         <para>
+          The signature of this function may change in new major releases if
+          there are changes to the statistics being tracked.
+         </para>
+        </warning>
+       </entry>
+      </row>
+
      </tbody>
     </tgroup>
    </table>
-- 
2.46.0

v29-0007-Enable-dumping-of-table-index-stats-in-pg_dump.patchtext/x-patch; charset=US-ASCII; name=v29-0007-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From 4361758beec8b98249a5b8e7e480fc7fa50bb532 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH v29 7/7] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will generate a statement that
calls pg_set_relation_stats(), and it will generate a series of
statements that call pg_set_attribute_stats(), one per attribute. These
statements will restore the statistics of the current system onto the
destination system.

Adds the command-line options -X / --statistics-only, which are mutually
exclusive to --schema-only and --data-only.

Statistics are not dumped when --schema-only is specified, except during
a binary upgrade.

As is the pattern with pg_dump options, staistics can be disabled using
--no-statistics.

Table statistics are dumped in the data section. This is true even if
dumping stats in a binary upgrade. Index and Materialized View
statistics are dumped in the post-data section.
---
 src/bin/pg_dump/pg_backup.h          |   6 +
 src/bin/pg_dump/pg_backup_archiver.c |   5 +
 src/bin/pg_dump/pg_dump.c            | 397 ++++++++++++++++++++++++++-
 src/bin/pg_dump/pg_dump.h            |   8 +
 src/bin/pg_dump/pg_dump_sort.c       |   5 +
 src/bin/pg_dump/pg_dumpall.c         |   5 +
 src/bin/pg_dump/pg_restore.c         |   8 +
 src/bin/pg_dump/t/001_basic.pl       |  10 +-
 doc/src/sgml/ref/pg_dump.sgml        |  36 ++-
 doc/src/sgml/ref/pg_restore.sgml     |  31 ++-
 10 files changed, 493 insertions(+), 18 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index d942a6a256..25d386f5bb 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -112,11 +112,13 @@ typedef struct _restoreOptions
 	int			no_publications;	/* Skip publication entries */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
 	int			dataOnly;
 	int			schemaOnly;
+	int			statisticsOnly;
 	int			dumpSections;
 	int			verbose;
 	int			aclsSkip;
@@ -161,6 +163,7 @@ typedef struct _restoreOptions
 	/* flags derived entirely from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } RestoreOptions;
 
 typedef struct _dumpOptions
@@ -172,6 +175,7 @@ typedef struct _dumpOptions
 	/* various user-settable parameters */
 	bool		schemaOnly;
 	bool		dataOnly;
+	bool		statisticsOnly;
 	int			dumpSections;	/* bitmask of chosen sections */
 	bool		aclsSkip;
 	const char *lockWaitTimeout;
@@ -185,6 +189,7 @@ typedef struct _dumpOptions
 	int			no_security_labels;
 	int			no_publications;
 	int			no_subscriptions;
+	int			no_statistics;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
@@ -211,6 +216,7 @@ typedef struct _dumpOptions
 	/* flags derived entirely from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } DumpOptions;
 
 /*
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 8c20c263c4..9926e5dad4 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2958,6 +2958,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's a stats dump, maybe ignore it */
+	if (ropt->no_statistics && strcmp(te->desc, "STATISTICS") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2987,6 +2991,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 2d0a3c1e9f..eaf63242d4 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -462,6 +462,7 @@ main(int argc, char **argv)
 		{"encoding", required_argument, NULL, 'E'},
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
+		{"statistics-only", no_argument, NULL, 'X'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -490,6 +491,7 @@ main(int argc, char **argv)
 		{"no-comments", no_argument, &dopt.no_comments, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -535,7 +537,7 @@ main(int argc, char **argv)
 
 	InitDumpOptions(&dopt);
 
-	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:RsS:t:T:U:vwWxZ:",
+	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:PRsS:t:T:U:vwWxXZ:",
 							long_options, &optindex)) != -1)
 	{
 		switch (c)
@@ -609,6 +611,10 @@ main(int argc, char **argv)
 				dopt.cparams.pgport = pg_strdup(optarg);
 				break;
 
+			case 'X':			/* Dump statistics only */
+				dopt.statisticsOnly = true;
+				break;
+
 			case 'R':
 				/* no-op, still accepted for backwards compatibility */
 				break;
@@ -778,8 +784,11 @@ main(int argc, char **argv)
 	if (dopt.binary_upgrade)
 		dopt.sequence_data = 1;
 
-	if (dopt.dataOnly && dopt.schemaOnly)
-		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (((int)dopt.dataOnly + (int) dopt.schemaOnly + (int) dopt.statisticsOnly) > 1)
+		pg_fatal("options -s/--schema-only, -a/--data-only, and -X/--statistics-only cannot be used together");
+
+	if (dopt.statisticsOnly && dopt.no_statistics)
+		pg_fatal("options -X/--statistics-only and --no-statistics cannot be used together");
 
 	if (dopt.schemaOnly && foreign_servers_include_patterns.head != NULL)
 		pg_fatal("options -s/--schema-only and --include-foreign-data cannot be used together");
@@ -794,8 +803,23 @@ main(int argc, char **argv)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
 	/* set derivative flags */
-	dopt.dumpSchema = (!dopt.dataOnly);
-	dopt.dumpData = (!dopt.schemaOnly);
+	dopt.dumpSchema = dopt.schemaOnly ||
+		!(dopt.dataOnly || dopt.statisticsOnly);
+
+	dopt.dumpData = dopt.dataOnly ||
+		!(dopt.schemaOnly || dopt.statisticsOnly);
+
+	if (dopt.statisticsOnly)
+		/* stats are only thing wanted */
+		dopt.dumpStatistics = true;
+	else if (dopt.no_statistics)
+		/* stats specifically excluded */
+		dopt.dumpStatistics = false;
+	else if (dopt.binary_upgrade)
+		/* binary upgrade and not specifically excluded */
+		dopt.dumpStatistics = true;
+	else
+		dopt.dumpStatistics = !(dopt.schemaOnly || dopt.dataOnly);
 
 	/*
 	 * --inserts are already implied above if --column-inserts or
@@ -1095,6 +1119,7 @@ main(int argc, char **argv)
 	ropt->dropSchema = dopt.outputClean;
 	ropt->dataOnly = dopt.dataOnly;
 	ropt->schemaOnly = dopt.schemaOnly;
+	ropt->statisticsOnly = dopt.statisticsOnly;
 	ropt->if_exists = dopt.if_exists;
 	ropt->column_inserts = dopt.column_inserts;
 	ropt->dumpSections = dopt.dumpSections;
@@ -1173,7 +1198,7 @@ help(const char *progname)
 	printf(_("  -?, --help                   show this help, then exit\n"));
 
 	printf(_("\nOptions controlling the output content:\n"));
-	printf(_("  -a, --data-only              dump only the data, not the schema\n"));
+	printf(_("  -a, --data-only              dump only the data, not the schema or statistics\n"));
 	printf(_("  -b, --large-objects          include large objects in dump\n"));
 	printf(_("  --blobs                      (same as --large-objects, deprecated)\n"));
 	printf(_("  -B, --no-large-objects       exclude large objects in dump\n"));
@@ -1186,11 +1211,12 @@ help(const char *progname)
 	printf(_("  -N, --exclude-schema=PATTERN do NOT dump the specified schema(s)\n"));
 	printf(_("  -O, --no-owner               skip restoration of object ownership in\n"
 			 "                               plain-text format\n"));
-	printf(_("  -s, --schema-only            dump only the schema, no data\n"));
+	printf(_("  -s, --schema-only            dump only the schema, no data or statistics\n"));
 	printf(_("  -S, --superuser=NAME         superuser user name to use in plain-text format\n"));
 	printf(_("  -t, --table=PATTERN          dump only the specified table(s)\n"));
 	printf(_("  -T, --exclude-table=PATTERN  do NOT dump the specified table(s)\n"));
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
+	printf(_("  -X, --statistics-only        dump only the statistics, not schema or data\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
@@ -1217,6 +1243,7 @@ help(const char *progname)
 	printf(_("  --no-comments                do not dump comments\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -6759,6 +6786,42 @@ getFuncs(Archive *fout)
 	destroyPQExpBuffer(query);
 }
 
+/*
+ * getRelationStatistics
+ *    register the statistics object as a dependent of the relation.
+ *
+ */
+static RelStatsInfo *
+getRelationStatistics(Archive *fout, DumpableObject *rel, char relkind)
+{
+	if ((relkind == RELKIND_RELATION) ||
+		(relkind == RELKIND_PARTITIONED_TABLE) ||
+		(relkind == RELKIND_INDEX) ||
+		(relkind == RELKIND_PARTITIONED_INDEX) ||
+		(relkind == RELKIND_MATVIEW))
+	{
+		RelStatsInfo	   *info = pg_malloc0(sizeof(RelStatsInfo));
+		DumpableObject	   *dobj = &info->dobj;
+
+		dobj->objType = DO_REL_STATS;
+		dobj->catId.tableoid = 0;
+		dobj->catId.oid = 0;
+		AssignDumpId(dobj);
+		dobj->dependencies = (DumpId *) pg_malloc(sizeof(DumpId));
+		dobj->dependencies[0] = rel->dumpId;
+		dobj->nDeps = 1;
+		dobj->allocDeps = 1;
+		dobj->components |= DUMP_COMPONENT_STATISTICS;
+		dobj->dump = rel->dump;
+		dobj->name = pg_strdup(rel->name);
+		dobj->namespace = rel->namespace;
+		info->relkind = relkind;
+
+		return info;
+	}
+	return NULL;
+}
+
 /*
  * getTables
  *	  read all the tables (no indexes) in the system catalogs,
@@ -7136,6 +7199,7 @@ getTables(Archive *fout, int *numTables)
 
 		/* Tables have data */
 		tblinfo[i].dobj.components |= DUMP_COMPONENT_DATA;
+		tblinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
 
 		/* Mark whether table has an ACL */
 		if (!PQgetisnull(res, i, i_relacl))
@@ -7184,6 +7248,8 @@ getTables(Archive *fout, int *numTables)
 				}
 			}
 		}
+		if (tblinfo[i].interesting)
+			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relkind);
 	}
 
 	if (query->len != 0)
@@ -7619,11 +7685,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 		for (int c = 0; c < numinds; c++, j++)
 		{
 			char		contype;
+			char		indexkind;
+			RelStatsInfo   *relstats;
 
 			indxinfo[j].dobj.objType = DO_INDEX;
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -7646,7 +7715,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			{
 				NULL, NULL
 			};
+
+			if (indxinfo[j].parentidx == 0)
+				indexkind = RELKIND_INDEX;
+			else
+				indexkind = RELKIND_PARTITIONED_INDEX;
+
 			contype = *(PQgetvalue(res, j, i_contype));
+			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, indexkind);
 
 			if (contype == 'p' || contype == 'u' || contype == 'x')
 			{
@@ -7679,6 +7755,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				constrinfo->separate = true;
 
 				indxinfo[j].indexconstraint = constrinfo->dobj.dumpId;
+				if (relstats != NULL)
+					addObjectDependency(&relstats->dobj, constrinfo->dobj.dumpId);
 			}
 			else
 			{
@@ -10116,6 +10194,296 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * Tabular description of the parameters to pg_restore_relation_stats()
+ * required, param_name, param_type
+ */
+static const char *rel_stats_arginfo[][3] = {
+	{"f", "relation", "regclass"},
+	{"f", "version", "integer"},
+	{"f", "relpages", "integer"},
+	{"f", "reltuples", "real"},
+	{"f", "relallvisible", "integer"},
+};
+
+/*
+ * Tabular description of the parameters to pg_restore_attribute_stats()
+ * required, param_name, param_type
+ */
+static const char *att_stats_arginfo[][3] = {
+	{"f", "relation", "regclass"},
+	{"f", "attname", "name"},
+	{"f", "inherited", "boolean"},
+	{"f", "version", "integer"},
+	{"f", "null_frac", "float4"},
+	{"f", "avg_width", "integer"},
+	{"f", "n_distinct", "float4"},
+	{"f", "most_common_vals", "text"},
+	{"f", "most_common_freqs", "float4[]"},
+	{"f", "histogram_bounds", "text"},
+	{"f", "correlation", "float4"},
+	{"f", "most_common_elems", "text"},
+	{"f", "most_common_elem_freqs", "float4[]"},
+	{"f", "elem_count_histogram", "float4[]"},
+	{"f", "range_length_histogram", "text"},
+	{"f", "range_empty_frac", "float4"},
+	{"f", "range_bounds_histogram", "text"},
+};
+
+/*
+ * getRelStatsExportQuery --
+ *
+ * Generate a query that will fetch all relation (e.g. pg_class)
+ * stats for a given relation.
+ */
+static void
+getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "current_setting('server_version_num') AS version, "
+						 "c.relpages, c.reltuples, c.relallvisible "
+						 "FROM pg_class c "
+						 "JOIN pg_namespace n "
+						 "ON n.oid = c.relnamespace "
+						 "WHERE n.nspname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND c.relname = ");
+	appendStringLiteralAH(query, relname, fout);
+}
+
+/*
+ * getAttStatsExportQuery --
+ *
+ * Generate a query that will fetch all attribute (e.g. pg_statistic)
+ * stats for a given relation.
+ */
+static void
+getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "s.attname,"
+						 "s.inherited,"
+						 "current_setting('server_version_num') AS version, "
+						 "s.null_frac,"
+						 "s.avg_width,"
+						 "s.n_distinct,"
+						 "s.most_common_vals,"
+						 "s.most_common_freqs,"
+						 "s.histogram_bounds,"
+						 "s.correlation,"
+						 "s.most_common_elems,"
+						 "s.most_common_elem_freqs,"
+						 "s.elem_count_histogram,");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 "s.range_length_histogram,"
+							 "s.range_empty_frac,"
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(query,
+							 "NULL AS range_length_histogram,"
+							 "NULL AS range_empty_frac,"
+							 "NULL AS range_bounds_histogram ");
+
+	appendPQExpBufferStr(query,
+						 "FROM pg_stats s "
+						 "JOIN pg_namespace n "
+						 "ON n.nspname = s.schemaname "
+						 "JOIN pg_class c "
+						 "ON c.relname = s.tablename "
+						 "AND c.relnamespace = n.oid "
+						 "WHERE s.schemaname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND s.tablename = ");
+	appendStringLiteralAH(query, relname, fout);
+	appendPQExpBufferStr(query, " ORDER BY s.attname, s.inherited");
+}
+
+
+/*
+ * appendNamedArgument --
+ *
+ * Convenience routine for constructing parameters of the form:
+ * 'paraname', 'value'::type
+ */
+static void
+appendNamedArgument(PQExpBuffer out, Archive *fout,
+					const char *argname, bool positional,
+					const char *argval, const char *argtype)
+{
+	appendPQExpBufferStr(out, "\t");
+
+	if (!positional)
+	{
+		appendStringLiteralAH(out, argname, fout);
+		appendPQExpBufferStr(out, ", ");
+	}
+
+	appendStringLiteralAH(out, argval, fout);
+	appendPQExpBuffer(out, "::%s", argtype);
+}
+
+/*
+ * appendRelStatsImport --
+ *
+ * Append a formatted pg_restore_relation_stats statement.
+ */
+static void
+appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	const char *sep = "";
+
+	if (PQntuples(res) == 0)
+		return;
+
+	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
+
+	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
+	{
+		bool		positional = (rel_stats_arginfo[argno][0][0] == 't');
+		const char *argname = rel_stats_arginfo[argno][1];
+		const char *argtype = rel_stats_arginfo[argno][2];
+		int			fieldno = PQfnumber(res, argname);
+
+		if (fieldno < 0)
+			pg_fatal("relation stats export query missing field '%s'",
+					 argname);
+
+		if (PQgetisnull(res, 0, fieldno))
+		{
+			if (!positional)
+				pg_fatal("relation stats export query unexpected NULL in '%s'",
+						 argname);
+			else
+				continue;
+		}
+
+		appendPQExpBufferStr(out, sep);
+		appendNamedArgument(out, fout, argname, positional,
+							PQgetvalue(res, 0, fieldno), argtype);
+
+		sep = ",\n";
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+/*
+ * appendAttStatsImport --
+ *
+ * Append a series of formatted pg_restore_attribute_stats statements.
+ */
+static void
+appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	{
+		const char *sep = "";
+
+		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+		for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
+		{
+			bool		positional = (att_stats_arginfo[argno][0][0] == 't');
+			const char *argname = att_stats_arginfo[argno][1];
+			const char *argtype = att_stats_arginfo[argno][2];
+			int			fieldno = PQfnumber(res, argname);
+
+			if (fieldno < 0)
+				pg_fatal("attribute stats export query missing field '%s'",
+						 argname);
+
+			if (PQgetisnull(res, rownum, fieldno))
+			{
+				if (positional)
+					pg_fatal("attribute stats export query unexpected NULL in '%s'",
+							 argname);
+				else
+					continue;
+			}
+
+			appendPQExpBufferStr(out, sep);
+			appendNamedArgument(out, fout, argname, positional,
+								PQgetvalue(res, rownum, fieldno), argtype);
+			sep = ",\n";
+		}
+		appendPQExpBufferStr(out, "\n);\n");
+	}
+}
+
+/*
+ * get statistics dump section, which depends on the parent object type
+ *
+ * objects created in SECTION_PRE_DATA have stats in SECTION_DATA
+ * objects created in SECTION_POST_DATA have stats in SECTION_POST_DATA
+ */
+static teSection
+statisticsDumpSection(const RelStatsInfo *rsinfo)
+{
+
+	if ((rsinfo->relkind == RELKIND_MATVIEW) ||
+		(rsinfo->relkind == RELKIND_INDEX) ||
+		(rsinfo->relkind == RELKIND_PARTITIONED_INDEX))
+			return SECTION_POST_DATA;
+
+	return SECTION_DATA;
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	PGresult	   *res;
+	PQExpBuffer		query;
+	PQExpBuffer		out;
+	PQExpBuffer		tag;
+	DumpableObject *dobj = (DumpableObject *) &rsinfo->dobj;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", "STATISTICS DATA", fmtId(dobj->name));
+
+	query = createPQExpBuffer();
+	out = createPQExpBuffer();
+
+	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendRelStatsImport(out, fout, res);
+	PQclear(res);
+
+	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendAttStatsImport(out, fout, res);
+	PQclear(res);
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATISTICS DATA",
+							  .section = statisticsDumpSection(rsinfo),
+							  .createStmt = out->data,
+							  .deps = dobj->dependencies,
+							  .nDeps = dobj->nDeps));
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(out);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -10564,6 +10932,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_SUBSCRIPTION_REL:
 			dumpSubscriptionTable(fout, (const SubRelInfo *) dobj);
 			break;
+		case DO_REL_STATS:
+			dumpRelationStats(fout, (const RelStatsInfo *) dobj);
+			break;
 		case DO_PRE_DATA_BOUNDARY:
 		case DO_POST_DATA_BOUNDARY:
 			/* never dumped, nothing to do */
@@ -16896,6 +17267,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 		free(indstatvalsarray);
 	}
 
+	/* Comments and stats share same .dep */
+
 	/* Dump Index Comments */
 	if (indxinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
 		dumpComment(fout, "INDEX", qindxname,
@@ -18680,6 +19053,16 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 				/* must come after the pre-data boundary */
 				addObjectDependency(dobj, preDataBound->dumpId);
 				break;
+			case DO_REL_STATS:
+				/* stats section varies by parent object type, DATA or POST */
+				if (statisticsDumpSection((RelStatsInfo *) dobj) == SECTION_DATA)
+				{
+					addObjectDependency(dobj, preDataBound->dumpId);
+					addObjectDependency(postDataBound, dobj->dumpId);
+				}
+				else
+					addObjectDependency(dobj, postDataBound->dumpId);
+				break;
 		}
 	}
 }
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 0b7d21b2e9..094a7d8b80 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -84,6 +84,7 @@ typedef enum
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION,
 	DO_SUBSCRIPTION_REL,		/* see note for SubRelInfo */
+	DO_REL_STATS,
 } DumpableObjectType;
 
 /*
@@ -101,6 +102,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
@@ -417,6 +419,12 @@ typedef struct _indexAttachInfo
 	IndxInfo   *partitionIdx;	/* link to index on partition */
 } IndexAttachInfo;
 
+typedef struct _relStatsInfo
+{
+	DumpableObject	dobj;
+	char			relkind;		/* 'r', 'v', 'c', etc */
+} RelStatsInfo;
+
 typedef struct _statsExtInfo
 {
 	DumpableObject dobj;
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 4cb754caa5..cfe277f5ce 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -1490,6 +1490,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "POST-DATA BOUNDARY  (ID %d)",
 					 obj->dumpId);
 			return;
+		case DO_REL_STATS:
+			snprintf(buf, bufsize,
+					 "RELATION STATISTICS FOR %s  (ID %d OID %u)",
+					 obj->name, obj->dumpId, obj->catId.oid);
+			return;
 	}
 	/* shouldn't get here */
 	snprintf(buf, bufsize,
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index e3ad8fb295..1799a03ff1 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -103,6 +103,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -172,6 +173,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -451,6 +453,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -666,6 +670,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 3475168a64..e285b2828f 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -75,6 +75,7 @@ main(int argc, char **argv)
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
 	static int	no_subscriptions = 0;
+	static int  no_statistics = 0;
 	static int	strict_names = 0;
 
 	struct option cmdopts[] = {
@@ -107,6 +108,7 @@ main(int argc, char **argv)
 		{"username", 1, NULL, 'U'},
 		{"verbose", 0, NULL, 'v'},
 		{"single-transaction", 0, NULL, '1'},
+		{"statistics-only", no_argument, NULL, 'P'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -127,6 +129,7 @@ main(int argc, char **argv)
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
+		{"no-statistics", no_argument, &no_statistics, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -270,6 +273,10 @@ main(int argc, char **argv)
 				opts->aclsSkip = 1;
 				break;
 
+			case 'X':			/* Restore statistics only */
+				opts->statisticsOnly = 1;
+				break;
+
 			case '1':			/* Restore data in a single transaction */
 				opts->single_txn = true;
 				opts->exit_on_error = true;
@@ -374,6 +381,7 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
diff --git a/src/bin/pg_dump/t/001_basic.pl b/src/bin/pg_dump/t/001_basic.pl
index b9d13a0e1d..b1c7a10919 100644
--- a/src/bin/pg_dump/t/001_basic.pl
+++ b/src/bin/pg_dump/t/001_basic.pl
@@ -46,8 +46,8 @@ command_fails_like(
 
 command_fails_like(
 	[ 'pg_dump', '-s', '-a' ],
-	qr/\Qpg_dump: error: options -s\/--schema-only and -a\/--data-only cannot be used together\E/,
-	'pg_dump: options -s/--schema-only and -a/--data-only cannot be used together'
+	qr/\Qpg_dump: error: options -s\/--schema-only, -a\/--data-only, and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: options -s/--schema-only, -a/--data-only, and -X/--statistics-only cannot be used together'
 );
 
 command_fails_like(
@@ -56,6 +56,12 @@ command_fails_like(
 	'pg_dump: options -s/--schema-only and --include-foreign-data cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '--statistics-only', '--no-statistics' ],
+	qr/\Qpg_dump: error: options -X\/--statistics-only and --no-statistics cannot be used together\E/,
+	'pg_dump: options -X\/--statistics-only and --no-statistics cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-j2', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: option --include-foreign-data is not supported with parallel backup\E/,
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index ffc29b04fb..99f25e40c0 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -123,7 +123,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Dump only the data, not the schema (data definitions).
+        Dump only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are dumped.
        </para>
 
@@ -141,8 +141,9 @@ PostgreSQL documentation
       <listitem>
        <para>
         Include large objects in the dump.  This is the default behavior
-        except when <option>--schema</option>, <option>--table</option>, or
-        <option>--schema-only</option> is specified.  The <option>-b</option>
+        except when <option>--schema</option>, <option>--table</option>,
+        <option>--schema-only</option>, or
+        <option>--statistics-only</option> is specified.  The <option>-b</option>
         switch is therefore only useful to add large objects to dumps
         where a specific schema or table has been requested.  Note that
         large objects are considered data and therefore will be included when
@@ -516,10 +517,11 @@ PostgreSQL documentation
       <term><option>--schema-only</option></term>
       <listitem>
        <para>
-        Dump only the object definitions (schema), not data.
+        Dump only the object definitions (schema), not data or statistics.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive to <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
         <option>--section=pre-data --section=post-data</option>.
@@ -652,6 +654,18 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Dump only the statistics, not the schema (data definitions) or data.
+        Statistics for tables, materialized views, and indexes are dumped.
+       </para>
+
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-Z <replaceable class="parameter">level</replaceable></option></term>
       <term><option>-Z <replaceable class="parameter">method</replaceable></option>[:<replaceable>detail</replaceable>]</term>
@@ -833,7 +847,8 @@ PostgreSQL documentation
         though you do not need the data in it.
        </para>
        <para>
-        To exclude data for all tables in the database, see <option>--schema-only</option>.
+        To exclude data for all tables in the database, see <option>--schema-only</option>
+        or <option>--statistics-only</option>.
        </para>
       </listitem>
      </varlistentry>
@@ -1098,6 +1113,15 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index b8b27e1719..ff1441a243 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -94,7 +94,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Restore only the data, not the schema (data definitions).
+        Restore only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are restored,
         if present in the archive.
        </para>
@@ -483,10 +483,11 @@ PostgreSQL documentation
         to the extent that schema entries are present in the archive.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive of <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
-        <option>--section=pre-data --section=post-data</option>.
+        <option>--section=pre-data --section=post-data --no-statistics</option>.
        </para>
        <para>
         (Do not confuse this with the <option>--schema</option> option, which
@@ -599,6 +600,20 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Restore only the statistics, not schema or data.
+       </para>
+       <para>
+        (Do not confuse this with the <option>--schema</option> option, which
+        uses the word <quote>schema</quote> in a different meaning.)
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-1</option></term>
       <term><option>--single-transaction</option></term>
@@ -723,6 +738,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore statistics, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
-- 
2.46.0

v29-0006-Add-derivative-flags-dumpSchema-dumpData.patchtext/x-patch; charset=US-ASCII; name=v29-0006-Add-derivative-flags-dumpSchema-dumpData.patchDownload
From b90a8a43c36ec8791876b631ad6471b9849f7c5f Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 4 May 2024 04:52:38 -0400
Subject: [PATCH v29 6/7] Add derivative flags dumpSchema, dumpData.

User-set flags --schema-only and --data-only are often consulted by
various operations to determine if they should be skipped or not. While
this logic works when there are only two mutually-exclusive -only
options, it will get progressively more confusing when more are added.

In anticipation of this, create the flags dumpSchema and dumpData which
are derivative of the existing options schemaOnly and dataOnly. This
allows us to restate current skip-this-section tests in terms of what is
enabled, rather than checking if the other -only mode is turned off.
---
 src/bin/pg_dump/pg_backup.h  |   8 ++
 src/bin/pg_dump/pg_dump.c    | 186 ++++++++++++++++++-----------------
 src/bin/pg_dump/pg_restore.c |   4 +
 3 files changed, 107 insertions(+), 91 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index fbf5f1c515..d942a6a256 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -157,6 +157,10 @@ typedef struct _restoreOptions
 	int			enable_row_security;
 	int			sequence_data;	/* dump sequence data even in schema-only mode */
 	int			binary_upgrade;
+
+	/* flags derived entirely from the user-settable flags */
+	bool		dumpSchema;
+	bool		dumpData;
 } RestoreOptions;
 
 typedef struct _dumpOptions
@@ -203,6 +207,10 @@ typedef struct _dumpOptions
 
 	int			sequence_data;	/* dump sequence data even in schema-only mode */
 	int			do_nothing;
+
+	/* flags derived entirely from the user-settable flags */
+	bool		dumpSchema;
+	bool		dumpData;
 } DumpOptions;
 
 /*
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 546e7e4ce1..2d0a3c1e9f 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -793,6 +793,10 @@ main(int argc, char **argv)
 	if (dopt.if_exists && !dopt.outputClean)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
+	/* set derivative flags */
+	dopt.dumpSchema = (!dopt.dataOnly);
+	dopt.dumpData = (!dopt.schemaOnly);
+
 	/*
 	 * --inserts are already implied above if --column-inserts or
 	 * --rows-per-insert were specified.
@@ -975,7 +979,7 @@ main(int argc, char **argv)
 	 * -s means "schema only" and LOs are data, not schema, so we never
 	 * include LOs when -s is used.
 	 */
-	if (dopt.include_everything && !dopt.schemaOnly && !dopt.dontOutputLOs)
+	if (dopt.include_everything && dopt.dumpData && !dopt.dontOutputLOs)
 		dopt.outputLOs = true;
 
 	/*
@@ -989,15 +993,15 @@ main(int argc, char **argv)
 	 */
 	tblinfo = getSchemaData(fout, &numTables);
 
-	if (!dopt.schemaOnly)
+	if (dopt.dumpData)
 	{
 		getTableData(&dopt, tblinfo, numTables, 0);
 		buildMatViewRefreshDependencies(fout);
-		if (dopt.dataOnly)
+		if (!dopt.dumpSchema)
 			getTableDataFKConstraints();
 	}
 
-	if (dopt.schemaOnly && dopt.sequence_data)
+	if (!dopt.dumpData && dopt.sequence_data)
 		getTableData(&dopt, tblinfo, numTables, RELKIND_SEQUENCE);
 
 	/*
@@ -4159,8 +4163,8 @@ dumpPolicy(Archive *fout, const PolicyInfo *polinfo)
 	const char *cmd;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
@@ -4372,8 +4376,8 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 	char	   *qpubname;
 	bool		first = true;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	delq = createPQExpBuffer();
@@ -4687,8 +4691,8 @@ dumpPublicationNamespace(Archive *fout, const PublicationSchemaInfo *pubsinfo)
 	PQExpBuffer query;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	tag = psprintf("%s %s", pubinfo->dobj.name, schemainfo->dobj.name);
@@ -4730,8 +4734,8 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 	PQExpBuffer query;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	tag = psprintf("%s %s", pubinfo->dobj.name, tbinfo->dobj.name);
@@ -5116,8 +5120,8 @@ dumpSubscriptionTable(Archive *fout, const SubRelInfo *subrinfo)
 	PQExpBuffer query;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	Assert(fout->dopt->binary_upgrade && fout->remoteVersion >= 170000);
@@ -5190,8 +5194,8 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo)
 	int			i;
 	char		two_phase_disabled[] = {LOGICALREP_TWOPHASE_STATE_DISABLED, '\0'};
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	delq = createPQExpBuffer();
@@ -7337,8 +7341,8 @@ getPartitioningInfo(Archive *fout)
 	/* hash partitioning didn't exist before v11 */
 	if (fout->remoteVersion < 110000)
 		return;
-	/* needn't bother if schema-only dump */
-	if (fout->dopt->schemaOnly)
+	/* needn't bother if not dumping data */
+	if (!fout->dopt->dumpData)
 		return;
 
 	query = createPQExpBuffer();
@@ -8980,7 +8984,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * Now get info about column defaults.  This is skipped for a data-only
 	 * dump, as it is only needed for table schemas.
 	 */
-	if (!dopt->dataOnly && tbloids->len > 1)
+	if (dopt->dumpSchema && tbloids->len > 1)
 	{
 		AttrDefInfo *attrdefs;
 		int			numDefaults;
@@ -9110,7 +9114,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * Get info about table CHECK constraints.  This is skipped for a
 	 * data-only dump, as it is only needed for table schemas.
 	 */
-	if (!dopt->dataOnly && checkoids->len > 2)
+	if (dopt->dumpSchema && checkoids->len > 2)
 	{
 		ConstraintInfo *constrs;
 		int			numConstrs;
@@ -10020,13 +10024,13 @@ dumpCommentExtended(Archive *fout, const char *type,
 	/* Comments are schema not data ... except LO comments are data */
 	if (strcmp(type, "LARGE OBJECT") != 0)
 	{
-		if (dopt->dataOnly)
+		if (!dopt->dumpSchema)
 			return;
 	}
 	else
 	{
 		/* We do dump LO comments in binary-upgrade mode */
-		if (dopt->schemaOnly && !dopt->binary_upgrade)
+		if (!dopt->dumpData && !dopt->binary_upgrade)
 			return;
 	}
 
@@ -10133,7 +10137,7 @@ dumpTableComment(Archive *fout, const TableInfo *tbinfo,
 		return;
 
 	/* Comments are SCHEMA not data */
-	if (dopt->dataOnly)
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Search for comments associated with relation, using table */
@@ -10579,8 +10583,8 @@ dumpNamespace(Archive *fout, const NamespaceInfo *nspinfo)
 	PQExpBuffer delq;
 	char	   *qnspname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -10656,8 +10660,8 @@ dumpExtension(Archive *fout, const ExtensionInfo *extinfo)
 	PQExpBuffer delq;
 	char	   *qextname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -10781,8 +10785,8 @@ dumpType(Archive *fout, const TypeInfo *tyinfo)
 {
 	DumpOptions *dopt = fout->dopt;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Dump out in proper style */
@@ -11892,8 +11896,8 @@ dumpShellType(Archive *fout, const ShellTypeInfo *stinfo)
 	DumpOptions *dopt = fout->dopt;
 	PQExpBuffer q;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -11944,8 +11948,8 @@ dumpProcLang(Archive *fout, const ProcLangInfo *plang)
 	FuncInfo   *inlineInfo = NULL;
 	FuncInfo   *validatorInfo = NULL;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
@@ -12152,8 +12156,8 @@ dumpFunc(Archive *fout, const FuncInfo *finfo)
 	int			nconfigitems = 0;
 	const char *keyword;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -12544,8 +12548,8 @@ dumpCast(Archive *fout, const CastInfo *cast)
 	const char *sourceType;
 	const char *targetType;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Cannot dump if we don't have the cast function's info */
@@ -12650,8 +12654,8 @@ dumpTransform(Archive *fout, const TransformInfo *transform)
 	char	   *lanname;
 	const char *transformType;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Cannot dump if we don't have the transform functions' info */
@@ -12799,8 +12803,8 @@ dumpOpr(Archive *fout, const OprInfo *oprinfo)
 	char	   *oprregproc;
 	char	   *oprref;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
@@ -13086,8 +13090,8 @@ dumpAccessMethod(Archive *fout, const AccessMethodInfo *aminfo)
 	PQExpBuffer delq;
 	char	   *qamname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -13189,8 +13193,8 @@ dumpOpclass(Archive *fout, const OpclassInfo *opcinfo)
 	bool		needComma;
 	int			i;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -13460,8 +13464,8 @@ dumpOpfamily(Archive *fout, const OpfamilyInfo *opfinfo)
 	bool		needComma;
 	int			i;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -13667,8 +13671,8 @@ dumpCollation(Archive *fout, const CollInfo *collinfo)
 	const char *colllocale;
 	const char *collicurules;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -13921,8 +13925,8 @@ dumpConversion(Archive *fout, const ConvInfo *convinfo)
 	const char *conproc;
 	bool		condefault;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -14069,8 +14073,8 @@ dumpAgg(Archive *fout, const AggInfo *agginfo)
 	const char *proparallel;
 	char		defaultfinalmodify;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -14399,8 +14403,8 @@ dumpTSParser(Archive *fout, const TSParserInfo *prsinfo)
 	PQExpBuffer delq;
 	char	   *qprsname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14467,8 +14471,8 @@ dumpTSDictionary(Archive *fout, const TSDictInfo *dictinfo)
 	char	   *nspname;
 	char	   *tmplname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14543,8 +14547,8 @@ dumpTSTemplate(Archive *fout, const TSTemplateInfo *tmplinfo)
 	PQExpBuffer delq;
 	char	   *qtmplname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14609,8 +14613,8 @@ dumpTSConfig(Archive *fout, const TSConfigInfo *cfginfo)
 	int			i_tokenname;
 	int			i_dictname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14721,8 +14725,8 @@ dumpForeignDataWrapper(Archive *fout, const FdwInfo *fdwinfo)
 	PQExpBuffer delq;
 	char	   *qfdwname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14794,8 +14798,8 @@ dumpForeignServer(Archive *fout, const ForeignServerInfo *srvinfo)
 	char	   *qsrvname;
 	char	   *fdwname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14985,8 +14989,8 @@ dumpDefaultACL(Archive *fout, const DefaultACLInfo *daclinfo)
 	PQExpBuffer tag;
 	const char *type;
 
-	/* Do nothing in data-only dump, or if we're skipping ACLs */
-	if (dopt->dataOnly || dopt->aclsSkip)
+	/* Do nothing if not dumping schema, or if we're skipping ACLs */
+	if (!dopt->dumpSchema || dopt->aclsSkip)
 		return;
 
 	q = createPQExpBuffer();
@@ -15086,7 +15090,7 @@ dumpACL(Archive *fout, DumpId objDumpId, DumpId altDumpId,
 		return InvalidDumpId;
 
 	/* --data-only skips ACLs *except* large object ACLs */
-	if (dopt->dataOnly && strcmp(type, "LARGE OBJECT") != 0)
+	if (!dopt->dumpSchema && strcmp(type, "LARGE OBJECT") != 0)
 		return InvalidDumpId;
 
 	sql = createPQExpBuffer();
@@ -15215,13 +15219,13 @@ dumpSecLabel(Archive *fout, const char *type, const char *name,
 	 */
 	if (strcmp(type, "LARGE OBJECT") != 0)
 	{
-		if (dopt->dataOnly)
+		if (!dopt->dumpSchema)
 			return;
 	}
 	else
 	{
 		/* We do dump large object security labels in binary-upgrade mode */
-		if (dopt->schemaOnly && !dopt->binary_upgrade)
+		if (!dopt->dumpData && !dopt->binary_upgrade)
 			return;
 	}
 
@@ -15289,7 +15293,7 @@ dumpTableSecLabel(Archive *fout, const TableInfo *tbinfo, const char *reltypenam
 		return;
 
 	/* SecLabel are SCHEMA not data */
-	if (dopt->dataOnly)
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Search for comments associated with relation, using table */
@@ -15528,8 +15532,8 @@ dumpTable(Archive *fout, const TableInfo *tbinfo)
 	DumpId		tableAclDumpId = InvalidDumpId;
 	char	   *namecopy;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	if (tbinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
@@ -16620,8 +16624,8 @@ dumpTableAttach(Archive *fout, const TableAttachInfo *attachinfo)
 	PGresult   *res;
 	char	   *partbound;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -16692,8 +16696,8 @@ dumpAttrDef(Archive *fout, const AttrDefInfo *adinfo)
 	char	   *tag;
 	char	   *foreign;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Skip if not "separate"; it was dumped in the table's definition */
@@ -16781,8 +16785,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 	char	   *qindxname;
 	char	   *qqindxname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -16914,8 +16918,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 static void
 dumpIndexAttach(Archive *fout, const IndexAttachInfo *attachinfo)
 {
-	/* Do nothing in data-only dump */
-	if (fout->dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!fout->dopt->dumpSchema)
 		return;
 
 	if (attachinfo->partitionIdx->dobj.dump & DUMP_COMPONENT_DEFINITION)
@@ -16961,8 +16965,8 @@ dumpStatisticsExt(Archive *fout, const StatsExtInfo *statsextinfo)
 	PGresult   *res;
 	char	   *stxdef;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -17037,8 +17041,8 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
 	char	   *tag = NULL;
 	char	   *foreign;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -17771,8 +17775,8 @@ dumpTrigger(Archive *fout, const TriggerInfo *tginfo)
 	char	   *qtabname;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -17893,8 +17897,8 @@ dumpEventTrigger(Archive *fout, const EventTriggerInfo *evtinfo)
 	PQExpBuffer delqry;
 	char	   *qevtname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -17984,8 +17988,8 @@ dumpRule(Archive *fout, const RuleInfo *rinfo)
 	PGresult   *res;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index df119591cc..3475168a64 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -360,6 +360,10 @@ main(int argc, char **argv)
 	if (opts->single_txn && numWorkers > 1)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
+	/* set derivative flags */
+	opts->dumpSchema = (opts->dataOnly != 1);
+	opts->dumpData = (opts->schemaOnly != 1);
+
 	opts->disable_triggers = disable_triggers;
 	opts->enable_row_security = enable_row_security;
 	opts->noDataForFailedTables = no_data_for_failed_tables;
-- 
2.46.0

#192jian he
jian.universality@gmail.com
In reply to: Corey Huinker (#191)
Re: Statistics Import and Export

On Tue, Sep 17, 2024 at 5:03 PM Corey Huinker <corey.huinker@gmail.com> wrote:

1. make sure these three functions: 'pg_set_relation_stats',
'pg_restore_relation_stats','pg_clear_relation_stats' proisstrict to true.
because in
pg_class catalog, these three attributes (relpages, reltuples, relallvisible) is
marked as not null. updating it to null will violate these constraints.
tom also mention this at [

Things have changed a bit since then, and the purpose of the functions has changed, so the considerations are now different. The function signature could change in the future as new pg_class stats are added, and it might not still be strict.

if you add more arguments to relation_statistics_update,
but the first 3 arguments (relpages, reltuples, relallvisible) still not null.
and, we are unlikely to add 3 or more (nullable=null) arguments?

we have code like:
if (!PG_ARGISNULL(RELPAGES_ARG))
{
values[ncols] = Int32GetDatum(relpages);
ncols++;
}
if (!PG_ARGISNULL(RELTUPLES_ARG))
{
replaces[ncols] = Anum_pg_class_reltuples;
values[ncols] = Float4GetDatum(reltuples);
}
if (!PG_ARGISNULL(RELALLVISIBLE_ARG))
{
values[ncols] = Int32GetDatum(relallvisible);
ncols++;
}
newtup = heap_modify_tuple_by_cols(ctup, tupdesc, ncols, replaces, nulls);

you just directly declared "bool nulls[3] = {false, false, false};"
if any of (RELPAGES_ARG, RELTUPLES_ARG, RELALLVISIBLE_ARG)
is null, should you set that null[position] to true?
otherwise, i am confused with the variable nulls.

Looking at other usage of heap_modify_tuple_by_cols, "ncols" cannot be
dynamic, it should be a fixed value?
The current implementation works, because the (bool[3] nulls) is
always false, never changed.
if nulls becomes {false, false, true} then "ncols" must be 3, cannot be 2.

8. lock_check_privileges function issue.
------------------------------------------------
--asume there is a superuser jian
create role alice NOSUPERUSER LOGIN;
create role bob NOSUPERUSER LOGIN;
create role carol NOSUPERUSER LOGIN;
alter database test owner to alice
GRANT CONNECT, CREATE on database test to bob;
\c test bob
create schema one;
create table one.t(a int);
create table one.t1(a int);
set session AUTHORIZATION; --switch to superuser.
alter table one.t1 owner to carol;
\c test alice
--now current database owner alice cannot do ANYTHING WITH table one.t1,
like ANALYZE, SELECT, INSERT, MAINTAIN etc.

Interesting.

database owners do not necessarily have schema USAGE privilege.
-------------<<<>>>------------------
create role alice NOSUPERUSER LOGIN;
create role bob NOSUPERUSER LOGIN;
create database test;
alter database test owner to alice;
GRANT CONNECT, CREATE on database test to bob;
\c test bob
create schema one;
create table one.t(a int);
\c test alice

analyze one.t;

with cte as (
select oid as the_t
from pg_class
where relname = any('{t}') and relnamespace = 'one'::regnamespace)
SELECT
pg_catalog.pg_set_relation_stats(
relation => the_t,
relpages => 17::integer,
reltuples => 400.0::real,
relallvisible => 4::integer)
from cte;

In the above case, alice cannot do "analyze one.t;",
but can do pg_set_relation_stats, which seems not ok?
-------------<<<>>>------------------

src/include/statistics/stats_utils.h
comment
* Portions Copyright (c) 1994, Regents of the University of California
*
* src/include/statistics/statistics.h

should be "src/include/statistics/stats_utils.h"

comment src/backend/statistics/stats_utils.c
* IDENTIFICATION
* src/backend/statistics/stats_privs.c
should be
* IDENTIFICATION
* src/backend/statistics/stats_utils.c

#193jian he
jian.universality@gmail.com
In reply to: jian he (#192)
Re: Statistics Import and Export

On Mon, Sep 23, 2024 at 8:57 AM jian he <jian.universality@gmail.com> wrote:

database owners do not necessarily have schema USAGE privilege.
-------------<<<>>>------------------
create role alice NOSUPERUSER LOGIN;
create role bob NOSUPERUSER LOGIN;
create database test;
alter database test owner to alice;
GRANT CONNECT, CREATE on database test to bob;
\c test bob
create schema one;
create table one.t(a int);
\c test alice

analyze one.t;

with cte as (
select oid as the_t
from pg_class
where relname = any('{t}') and relnamespace = 'one'::regnamespace)
SELECT
pg_catalog.pg_set_relation_stats(
relation => the_t,
relpages => 17::integer,
reltuples => 400.0::real,
relallvisible => 4::integer)
from cte;

In the above case, alice cannot do "analyze one.t;",
but can do pg_set_relation_stats, which seems not ok?

sorry for the noise.
what you stats_lock_check_privileges about privilege is right.

database owner cannot do
"ANALYZE one.t;"
but it can do "ANALYZE;" to indirect analyzing one.t

which seems to be the expected behavior per
https://www.postgresql.org/docs/17/sql-analyze.html
<<
To analyze a table, one must ordinarily have the MAINTAIN privilege on
the table.
However, database owners are allowed to analyze all tables in their
databases, except shared catalogs.
<<

#194Nathan Bossart
nathandbossart@gmail.com
In reply to: Corey Huinker (#191)
Re: Statistics Import and Export

I took a look at v29-0006.

On Tue, Sep 17, 2024 at 05:02:49AM -0400, Corey Huinker wrote:

From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 4 May 2024 04:52:38 -0400
Subject: [PATCH v29 6/7] Add derivative flags dumpSchema, dumpData.

User-set flags --schema-only and --data-only are often consulted by
various operations to determine if they should be skipped or not. While
this logic works when there are only two mutually-exclusive -only
options, it will get progressively more confusing when more are added.

After glancing at v29-0007, I see what you mean.

In anticipation of this, create the flags dumpSchema and dumpData which
are derivative of the existing options schemaOnly and dataOnly. This
allows us to restate current skip-this-section tests in terms of what is
enabled, rather than checking if the other -only mode is turned off.

This seems like a reasonable refactoring exercise that we could take care
of before the rest of the patch set goes in. I added one new reference to
dopt.schemaOnly in commit bd15b7d, so that should probably be revised to
!dumpData, too. I also noticed a few references to dataOnly/schemaOnly in
comments that should likely be adjusted.

One other question I had when looking at this patch is whether we could
remove dataOnly/schemaOnly from DumpOptions and RestoreOptions. Once 0007
is applied, those variables become particularly hazardous, so we really
want to prevent folks from using them in new code.

--
nathan

#195Corey Huinker
corey.huinker@gmail.com
In reply to: Nathan Bossart (#194)
Re: Statistics Import and Export

This seems like a reasonable refactoring exercise that we could take care
of before the rest of the patch set goes in. I added one new reference to
dopt.schemaOnly in commit bd15b7d, so that should probably be revised to
!dumpData, too. I also noticed a few references to dataOnly/schemaOnly in
comments that should likely be adjusted.

I'll be on the lookout for the new usage with the next rebase, and will
fix the comments as well.

One other question I had when looking at this patch is whether we could
remove dataOnly/schemaOnly from DumpOptions and RestoreOptions. Once 0007
is applied, those variables become particularly hazardous, so we really
want to prevent folks from using them in new code.

Well, the very next patch in the series adds --statistics-only, so I don't
think we're getting rid of user-facing command switches. However, I could
see us taking away the dataOnly/schemaOnly internal variables, thus
preventing coders from playing with those sharp objects.

#196Nathan Bossart
nathandbossart@gmail.com
In reply to: Corey Huinker (#195)
Re: Statistics Import and Export

On Thu, Oct 10, 2024 at 03:49:16PM -0400, Corey Huinker wrote:

One other question I had when looking at this patch is whether we could
remove dataOnly/schemaOnly from DumpOptions and RestoreOptions. Once 0007
is applied, those variables become particularly hazardous, so we really
want to prevent folks from using them in new code.

Well, the very next patch in the series adds --statistics-only, so I don't
think we're getting rid of user-facing command switches. However, I could
see us taking away the dataOnly/schemaOnly internal variables, thus
preventing coders from playing with those sharp objects.

That's what I meant. The user-facing options would stay the same, but the
internal variables would be local to main() so that other functions would
be forced to use dumpData, dumpSchema, etc.

--
nathan

#197Jeff Davis
pgsql@j-davis.com
In reply to: jian he (#192)
Re: Statistics Import and Export

On Mon, 2024-09-23 at 08:57 +0800, jian he wrote:

    newtup = heap_modify_tuple_by_cols(ctup, tupdesc, ncols,
replaces, nulls);

you just directly declared "bool nulls[3]    = {false, false,
false};"

Those must be false (not NULL), because in pg_class those are non-NULL
attributes. They must be set to something whenever we update.

if any of (RELPAGES_ARG, RELTUPLES_ARG, RELALLVISIBLE_ARG)
is null, should you set that null[position] to true?

If the corresponding SQL argument is NULL, we leave the existing value
unchanged, we don't set it to NULL.

otherwise, i am confused with the variable nulls.

Looking at other usage of heap_modify_tuple_by_cols, "ncols" cannot
be
dynamic, it should be a fixed value?
The current implementation works, because the (bool[3] nulls) is
always false, never changed.
if nulls becomes {false, false, true} then "ncols" must be 3, cannot
be 2.

heap_modify_tuple_by_cols() uses ncols to specify the length of the
values/isnull arrays. The "replaces" is an array of attribute numbers
to replace (in contrast to plain heap_modify_tuple(), which uses an
array of booleans).

We are going to replace a maximum of 3 attributes, so the arrays have a
maximum size of 3. Predeclaring the arrays to be 3 elements is just
fine even if we only use the first 1-2 elements -- it avoids a needless
heap allocation/free.

Regards,
Jeff Davis

In reply to: Jeff Davis (#197)
1 attachment(s)
RE: Statistics Import and Export

Hi,
Thank you for developing this great feature. I have tested the committed feature.
The manual for the pg_set_relation_stats function says the following:
"The value of relpages must be greater than or equal to 0"

However, this function seems to accept -1 for the relpages parameter. Below is an example of execution:
---
postgres=> CREATE TABLE data1(c1 INT PRIMARY KEY, c2 VARCHAR(10));
CREATE TABLE
postgres=> SELECT pg_set_relation_stats('data1', relpages=>-1);
pg_set_relation_stats
-----------------------
t
(1 row)
postgres=> SELECT relname, relpages FROM pg_class WHERE relname='data1';
relname | relpages
---------+----------
data1 | -1
(1 row)
---

The attached patch modifies the pg_set_relation_stats function to work as described in the manual.

Regards,
Noriyoshi Shinoda

-----Original Message-----
From: Jeff Davis <pgsql@j-davis.com>
Sent: Saturday, October 12, 2024 8:11 AM
To: jian he <jian.universality@gmail.com>; Corey Huinker <corey.huinker@gmail.com>
Cc: Matthias van de Meent <boekewurm+postgres@gmail.com>; Bruce Momjian <bruce@momjian.us>; Tom Lane <tgl@sss.pgh.pa.us>; Nathan Bossart <nathandbossart@gmail.com>; Magnus Hagander <magnus@hagander.net>; Stephen Frost <sfrost@snowman.net>; Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; Peter Smith <smithpb2250@gmail.com>; PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; Tomas Vondra <tomas.vondra@enterprisedb.com>; alvherre@alvh.no-ip.org
Subject: Re: Statistics Import and Export

On Mon, 2024-09-23 at 08:57 +0800, jian he wrote:

    newtup = heap_modify_tuple_by_cols(ctup, tupdesc, ncols, replaces,
nulls);

you just directly declared "bool nulls[3]    = {false, false, false};"

Those must be false (not NULL), because in pg_class those are non-NULL attributes. They must be set to something whenever we update.

if any of (RELPAGES_ARG, RELTUPLES_ARG, RELALLVISIBLE_ARG) is null,
should you set that null[position] to true?

If the corresponding SQL argument is NULL, we leave the existing value unchanged, we don't set it to NULL.

otherwise, i am confused with the variable nulls.

Looking at other usage of heap_modify_tuple_by_cols, "ncols" cannot be
dynamic, it should be a fixed value?
The current implementation works, because the (bool[3] nulls) is
always false, never changed.
if nulls becomes {false, false, true} then "ncols" must be 3, cannot
be 2.

heap_modify_tuple_by_cols() uses ncols to specify the length of the values/isnull arrays. The "replaces" is an array of attribute numbers to replace (in contrast to plain heap_modify_tuple(), which uses an array of booleans).

We are going to replace a maximum of 3 attributes, so the arrays have a maximum size of 3. Predeclaring the arrays to be 3 elements is just fine even if we only use the first 1-2 elements -- it avoids a needless heap allocation/free.

Regards,
Jeff Davis

Attachments:

relpages_stat_update_v1.diffapplication/octet-stream; name=relpages_stat_update_v1.diffDownload
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index ffa3d83a87..26f15061e8 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -99,11 +99,11 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	{
 		int32		relpages = PG_GETARG_INT32(RELPAGES_ARG);
 
-		if (relpages < -1)
+		if (relpages < 0)
 		{
 			ereport(elevel,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("relpages cannot be < -1")));
+					 errmsg("relpages cannot be < 0")));
 			table_close(crel, RowExclusiveLock);
 			return false;
 		}
#199Corey Huinker
corey.huinker@gmail.com
In reply to: Shinoda, Noriyoshi (SXD Japan FSIP) (#198)
Re: Statistics Import and Export

However, this function seems to accept -1 for the relpages parameter.
Below is an example of execution:
---
postgres=> CREATE TABLE data1(c1 INT PRIMARY KEY, c2 VARCHAR(10));
CREATE TABLE
postgres=> SELECT pg_set_relation_stats('data1', relpages=>-1);
pg_set_relation_stats
-----------------------
t
(1 row)
postgres=> SELECT relname, relpages FROM pg_class WHERE relname='data1';
relname | relpages
---------+----------
data1 | -1
(1 row)
---

The attached patch modifies the pg_set_relation_stats function to work as
described in the manual.

Regards,
Noriyoshi Shinoda

Accepting -1 is correct. I thought I had fixed that in a recent patch.
Perhaps signals got crossed somewhere along the way.

#200Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#199)
Re: Statistics Import and Export

On Mon, Oct 14, 2024 at 3:40 PM Corey Huinker <corey.huinker@gmail.com>
wrote:

However, this function seems to accept -1 for the relpages parameter.

Below is an example of execution:
---
postgres=> CREATE TABLE data1(c1 INT PRIMARY KEY, c2 VARCHAR(10));
CREATE TABLE
postgres=> SELECT pg_set_relation_stats('data1', relpages=>-1);
pg_set_relation_stats
-----------------------
t
(1 row)
postgres=> SELECT relname, relpages FROM pg_class WHERE relname='data1';
relname | relpages
---------+----------
data1 | -1
(1 row)
---

The attached patch modifies the pg_set_relation_stats function to work as
described in the manual.

Regards,
Noriyoshi Shinoda

Accepting -1 is correct. I thought I had fixed that in a recent patch.
Perhaps signals got crossed somewhere along the way.

Just to be sure, I went back to v29, fixed a typo and some whitespace
issues in stats_import.out, confirmed that it passed regression tests, then
changed the relpages lower bound from -1 back to 0, and sure enough, the
regression test for pg_upgrade failed again.

It seems that partitioned tables have a relpages of -1, so regression tests
involving tables alpha_neg and alpha_pos (and 35 others, all seemingly
partitioned) fail. So it was the docs that were wrong, not the code.

e839c8ecc9352b7754e74f19ace013c0c0d18613 doesn't include the stuff that
modified pg_dump/pg_upgrade, so it wouldn't have turned up this problem.

#201Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#200)
Re: Statistics Import and Export

On Mon, 2024-10-14 at 21:46 -0400, Corey Huinker wrote:

It seems that partitioned tables have a relpages of -1

Oh, I see. It appears that there's a special case for partitioned
tables that sets relpages=-1 in do_analyze_rel() around line 680. It's
a bit inconsistent, though, because even partitioned indexes have
relpages=0. Furthermore, the parameter is of type BlockNumber, so
InvalidBlockNumber would make more sense.

Not the cleanest code, but if the value exists, we need to be able to
import it.

Regards,
Jeff Davis

#202Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#201)
Re: Statistics Import and Export

Oh, I see. It appears that there's a special case for partitioned
tables that sets relpages=-1 in do_analyze_rel() around line 680. It's
a bit inconsistent, though, because even partitioned indexes have
relpages=0. Furthermore, the parameter is of type BlockNumber, so
InvalidBlockNumber would make more sense.

Not the cleanest code, but if the value exists, we need to be able to
import it.

Thanks for tracking that down. I'll have a patch ready shortly.

#203Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#202)
1 attachment(s)
Re: Statistics Import and Export

On Tue, Oct 15, 2024 at 2:50 PM Corey Huinker <corey.huinker@gmail.com>
wrote:

Oh, I see. It appears that there's a special case for partitioned

tables that sets relpages=-1 in do_analyze_rel() around line 680. It's
a bit inconsistent, though, because even partitioned indexes have
relpages=0. Furthermore, the parameter is of type BlockNumber, so
InvalidBlockNumber would make more sense.

Not the cleanest code, but if the value exists, we need to be able to
import it.

Thanks for tracking that down. I'll have a patch ready shortly.

Code fix with comment on why nobody expects a relpages -1. Test case to
demonstrate that relpages -1 can happen, and updated doc to reflect the new
lower bound.

Attachments:

v1-0001-Allow-pg_set_relation_stats-to-set-relpages-to-1.patchtext/x-patch; charset=US-ASCII; name=v1-0001-Allow-pg_set_relation_stats-to-set-relpages-to-1.patchDownload
From c0efe3fb2adac102e186e086bf94fb29fabba28b Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Tue, 15 Oct 2024 19:09:59 -0400
Subject: [PATCH v1] Allow pg_set_relation_stats() to set relpages to -1.

While the default value for relpages is 0, if a partitioned table with
at least one child has been analyzed, then the partititoned table will
have a relpages value of -1. pg_set_relation_stats() should therefore be
able to set relpages to -1.

Add test case to demonstrate the behavior of ANALYZE on a partitioned
table with child table(s), and to demonstrate that
pg_set_relation_stats() now accepts -1.
---
 src/backend/statistics/relation_stats.c    |  8 +++--
 src/test/regress/expected/stats_import.out | 38 +++++++++++++++++++++-
 src/test/regress/sql/stats_import.sql      | 25 ++++++++++++++
 doc/src/sgml/func.sgml                     |  2 +-
 4 files changed, 69 insertions(+), 4 deletions(-)

diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index 26f15061e8..b90f70b190 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -99,11 +99,15 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	{
 		int32		relpages = PG_GETARG_INT32(RELPAGES_ARG);
 
-		if (relpages < 0)
+		/*
+		 * While the default value for relpages is 0, a partitioned table with at
+		 * least one child partition can have a relpages of -1.
+		 */
+		if (relpages < -1)
 		{
 			ereport(elevel,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("relpages cannot be < 0")));
+					 errmsg("relpages cannot be < -1")));
 			table_close(crel, RowExclusiveLock);
 			return false;
 		}
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index cd1b80aa43..46e45a5fa3 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -135,9 +135,45 @@ SELECT
         'stats_import.testview'::regclass);
 ERROR:  cannot modify statistics for relation "testview"
 DETAIL:  This operation is not supported for views.
+-- Partitioned tables with at least 1 child partition will, when analyzed,
+-- have a relpages of -1
+CREATE TABLE stats_import.part_parent ( i integer ) PARTITION BY RANGE(i);
+CREATE TABLE stats_import.part_child_1
+  PARTITION OF stats_import.part_parent
+  FOR VALUES FROM (0) TO (10);
+ANALYZE stats_import.part_parent;
+SELECT relpages
+FROM pg_class
+WHERE oid = 'stats_import.part_parent'::regclass;
+ relpages 
+----------
+       -1
+(1 row)
+
+-- nothing stops us from setting it to not -1
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.part_parent'::regclass,
+        relpages => 2::integer);
+ pg_set_relation_stats 
+-----------------------
+ t
+(1 row)
+
+-- nothing stops us from setting it to -1
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.part_parent'::regclass,
+        relpages => -1::integer);
+ pg_set_relation_stats 
+-----------------------
+ t
+(1 row)
+
 DROP SCHEMA stats_import CASCADE;
-NOTICE:  drop cascades to 4 other objects
+NOTICE:  drop cascades to 5 other objects
 DETAIL:  drop cascades to type stats_import.complex_type
 drop cascades to table stats_import.test
 drop cascades to sequence stats_import.testseq
 drop cascades to view stats_import.testview
+drop cascades to table stats_import.part_parent
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index 3e9f6d9124..ad129b1ab9 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -95,4 +95,29 @@ SELECT
     pg_catalog.pg_clear_relation_stats(
         'stats_import.testview'::regclass);
 
+-- Partitioned tables with at least 1 child partition will, when analyzed,
+-- have a relpages of -1
+CREATE TABLE stats_import.part_parent ( i integer ) PARTITION BY RANGE(i);
+CREATE TABLE stats_import.part_child_1
+  PARTITION OF stats_import.part_parent
+  FOR VALUES FROM (0) TO (10);
+
+ANALYZE stats_import.part_parent;
+
+SELECT relpages
+FROM pg_class
+WHERE oid = 'stats_import.part_parent'::regclass;
+
+-- nothing stops us from setting it to not -1
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.part_parent'::regclass,
+        relpages => 2::integer);
+
+-- nothing stops us from setting it to -1
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.part_parent'::regclass,
+        relpages => -1::integer);
+
 DROP SCHEMA stats_import CASCADE;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index f8a0d76d12..ad663c94d7 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30197,7 +30197,7 @@ DETAIL:  Make sure pg_wal_replay_wait() isn't called within a transaction with a
         </para>
         <para>
          The value of <structfield>relpages</structfield> must be greater than
-         or equal to <literal>0</literal>,
+         or equal to <literal>-1</literal>,
          <structfield>reltuples</structfield> must be greater than or equal to
          <literal>-1.0</literal>, and <structfield>relallvisible</structfield>
          must be greater than or equal to <literal>0</literal>.
-- 
2.46.2

#204Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#203)
3 attachment(s)
Re: Statistics Import and Export

Code fix with comment on why nobody expects a relpages -1. Test case to
demonstrate that relpages -1 can happen, and updated doc to reflect the new
lower bound.

Additional fixes, now in a patch-set:

1. Allow relpages to be set to -1 (partitioned tables with partitions have
this value after ANALYZE).
2. Turn off autovacuum on tables (where possible) if they are going to be
the target of pg_set_relation_stats().
3. Allow pg_set_relation_stats to continue past an out-of-range detection
on one attribute, rather than immediately returning false.

Attachments:

v2-0001-Allow-pg_set_relation_stats-to-set-relpages-to-1.patchtext/x-patch; charset=US-ASCII; name=v2-0001-Allow-pg_set_relation_stats-to-set-relpages-to-1.patchDownload
From c0efe3fb2adac102e186e086bf94fb29fabba28b Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Tue, 15 Oct 2024 19:09:59 -0400
Subject: [PATCH v2 1/3] Allow pg_set_relation_stats() to set relpages to -1.

While the default value for relpages is 0, if a partitioned table with
at least one child has been analyzed, then the partititoned table will
have a relpages value of -1. pg_set_relation_stats() should therefore be
able to set relpages to -1.

Add test case to demonstrate the behavior of ANALYZE on a partitioned
table with child table(s), and to demonstrate that
pg_set_relation_stats() now accepts -1.
---
 src/backend/statistics/relation_stats.c    |  8 +++--
 src/test/regress/expected/stats_import.out | 38 +++++++++++++++++++++-
 src/test/regress/sql/stats_import.sql      | 25 ++++++++++++++
 doc/src/sgml/func.sgml                     |  2 +-
 4 files changed, 69 insertions(+), 4 deletions(-)

diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index 26f15061e8..b90f70b190 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -99,11 +99,15 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	{
 		int32		relpages = PG_GETARG_INT32(RELPAGES_ARG);
 
-		if (relpages < 0)
+		/*
+		 * While the default value for relpages is 0, a partitioned table with at
+		 * least one child partition can have a relpages of -1.
+		 */
+		if (relpages < -1)
 		{
 			ereport(elevel,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("relpages cannot be < 0")));
+					 errmsg("relpages cannot be < -1")));
 			table_close(crel, RowExclusiveLock);
 			return false;
 		}
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index cd1b80aa43..46e45a5fa3 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -135,9 +135,45 @@ SELECT
         'stats_import.testview'::regclass);
 ERROR:  cannot modify statistics for relation "testview"
 DETAIL:  This operation is not supported for views.
+-- Partitioned tables with at least 1 child partition will, when analyzed,
+-- have a relpages of -1
+CREATE TABLE stats_import.part_parent ( i integer ) PARTITION BY RANGE(i);
+CREATE TABLE stats_import.part_child_1
+  PARTITION OF stats_import.part_parent
+  FOR VALUES FROM (0) TO (10);
+ANALYZE stats_import.part_parent;
+SELECT relpages
+FROM pg_class
+WHERE oid = 'stats_import.part_parent'::regclass;
+ relpages 
+----------
+       -1
+(1 row)
+
+-- nothing stops us from setting it to not -1
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.part_parent'::regclass,
+        relpages => 2::integer);
+ pg_set_relation_stats 
+-----------------------
+ t
+(1 row)
+
+-- nothing stops us from setting it to -1
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.part_parent'::regclass,
+        relpages => -1::integer);
+ pg_set_relation_stats 
+-----------------------
+ t
+(1 row)
+
 DROP SCHEMA stats_import CASCADE;
-NOTICE:  drop cascades to 4 other objects
+NOTICE:  drop cascades to 5 other objects
 DETAIL:  drop cascades to type stats_import.complex_type
 drop cascades to table stats_import.test
 drop cascades to sequence stats_import.testseq
 drop cascades to view stats_import.testview
+drop cascades to table stats_import.part_parent
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index 3e9f6d9124..ad129b1ab9 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -95,4 +95,29 @@ SELECT
     pg_catalog.pg_clear_relation_stats(
         'stats_import.testview'::regclass);
 
+-- Partitioned tables with at least 1 child partition will, when analyzed,
+-- have a relpages of -1
+CREATE TABLE stats_import.part_parent ( i integer ) PARTITION BY RANGE(i);
+CREATE TABLE stats_import.part_child_1
+  PARTITION OF stats_import.part_parent
+  FOR VALUES FROM (0) TO (10);
+
+ANALYZE stats_import.part_parent;
+
+SELECT relpages
+FROM pg_class
+WHERE oid = 'stats_import.part_parent'::regclass;
+
+-- nothing stops us from setting it to not -1
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.part_parent'::regclass,
+        relpages => 2::integer);
+
+-- nothing stops us from setting it to -1
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.part_parent'::regclass,
+        relpages => -1::integer);
+
 DROP SCHEMA stats_import CASCADE;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index f8a0d76d12..ad663c94d7 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30197,7 +30197,7 @@ DETAIL:  Make sure pg_wal_replay_wait() isn't called within a transaction with a
         </para>
         <para>
          The value of <structfield>relpages</structfield> must be greater than
-         or equal to <literal>0</literal>,
+         or equal to <literal>-1</literal>,
          <structfield>reltuples</structfield> must be greater than or equal to
          <literal>-1.0</literal>, and <structfield>relallvisible</structfield>
          must be greater than or equal to <literal>0</literal>.
-- 
2.46.2

v2-0002-Turn-off-autovacuum-for-pg_set_relation_stats-tes.patchtext/x-patch; charset=US-ASCII; name=v2-0002-Turn-off-autovacuum-for-pg_set_relation_stats-tes.patchDownload
From f3a3df68763892f966547c2705b0cf9198bd55ec Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 16 Oct 2024 18:45:57 -0400
Subject: [PATCH v2 2/3] Turn off autovacuum for pg_set_relation_stats() tests.

Unlikely as it may be, it is theoretically possible that autovacuum
could act upon one of the tables that have had test stats applied and
set the stats to reflect the empty table that it is. To avoid the
possibility of an intermittent (and very confusing) regression test
failure, turn off autovacuum for all tables that will have stats applied
during the tests.
---
 src/test/regress/expected/stats_import.out | 9 ++++++---
 src/test/regress/sql/stats_import.sql      | 9 ++++++---
 2 files changed, 12 insertions(+), 6 deletions(-)

diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index 46e45a5fa3..de37d890c4 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -11,7 +11,7 @@ CREATE TABLE stats_import.test(
     comp stats_import.complex_type,
     arange int4range,
     tags text[]
-);
+) WITH (autovacuum_enabled = false);
 -- starting stats
 SELECT relpages, reltuples, relallvisible
 FROM pg_class
@@ -136,11 +136,14 @@ SELECT
 ERROR:  cannot modify statistics for relation "testview"
 DETAIL:  This operation is not supported for views.
 -- Partitioned tables with at least 1 child partition will, when analyzed,
--- have a relpages of -1
+-- have a relpages of -1.
+-- Note: can't set storage params on partitioned tables, so just set for
+-- the child table(s).
 CREATE TABLE stats_import.part_parent ( i integer ) PARTITION BY RANGE(i);
 CREATE TABLE stats_import.part_child_1
   PARTITION OF stats_import.part_parent
-  FOR VALUES FROM (0) TO (10);
+  FOR VALUES FROM (0) TO (10)
+  WITH (autovacuum_enabled = false);
 ANALYZE stats_import.part_parent;
 SELECT relpages
 FROM pg_class
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index ad129b1ab9..b8d1ef795f 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -13,7 +13,7 @@ CREATE TABLE stats_import.test(
     comp stats_import.complex_type,
     arange int4range,
     tags text[]
-);
+) WITH (autovacuum_enabled = false);
 
 -- starting stats
 SELECT relpages, reltuples, relallvisible
@@ -96,11 +96,14 @@ SELECT
         'stats_import.testview'::regclass);
 
 -- Partitioned tables with at least 1 child partition will, when analyzed,
--- have a relpages of -1
+-- have a relpages of -1.
+-- Note: can't set storage params on partitioned tables, so just set for
+-- the child table(s).
 CREATE TABLE stats_import.part_parent ( i integer ) PARTITION BY RANGE(i);
 CREATE TABLE stats_import.part_child_1
   PARTITION OF stats_import.part_parent
-  FOR VALUES FROM (0) TO (10);
+  FOR VALUES FROM (0) TO (10)
+  WITH (autovacuum_enabled = false);
 
 ANALYZE stats_import.part_parent;
 
-- 
2.46.2

v2-0003-Allow-pg_set_relation_stats-to-continue-after-non.patchtext/x-patch; charset=US-ASCII; name=v2-0003-Allow-pg_set_relation_stats-to-continue-after-non.patchDownload
From cd66d8c4d3874cffe27f63ef7eaedbbce70142af Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 16 Oct 2024 19:13:01 -0400
Subject: [PATCH v2 3/3] Allow pg_set_relation_stats to continue after
 non-ERROR validation.

Some validation checks would, when elevel was set to WARNING or lower,
immediately return false after the failed check. That isn't the correct
behavior, and we should instead let the function continue in case there
were other valid values that could be set.
---
 src/backend/statistics/relation_stats.c | 31 +++++++++----------------
 1 file changed, 11 insertions(+), 20 deletions(-)

diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index b90f70b190..ffc8d4cebd 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -67,7 +67,6 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	bool		nulls[3] = {0};
 	int			ncols = 0;
 	TupleDesc	tupdesc;
-	HeapTuple	newtup;
 
 
 	stats_check_required_arg(fcinfo, relarginfo, RELATION_ARG);
@@ -109,10 +108,8 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("relpages cannot be < -1")));
 			table_close(crel, RowExclusiveLock);
-			return false;
 		}
-
-		if (relpages != pgcform->relpages)
+		else if (relpages != pgcform->relpages)
 		{
 			replaces[ncols] = Anum_pg_class_relpages;
 			values[ncols] = Int32GetDatum(relpages);
@@ -130,10 +127,8 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("reltuples cannot be < -1.0")));
 			table_close(crel, RowExclusiveLock);
-			return false;
 		}
-
-		if (reltuples != pgcform->reltuples)
+		else if (reltuples != pgcform->reltuples)
 		{
 			replaces[ncols] = Anum_pg_class_reltuples;
 			values[ncols] = Float4GetDatum(reltuples);
@@ -151,10 +146,8 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("relallvisible cannot be < 0")));
 			table_close(crel, RowExclusiveLock);
-			return false;
 		}
-
-		if (relallvisible != pgcform->relallvisible)
+		else if (relallvisible != pgcform->relallvisible)
 		{
 			replaces[ncols] = Anum_pg_class_relallvisible;
 			values[ncols] = Int32GetDatum(relallvisible);
@@ -163,22 +156,20 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	}
 
 	/* only update pg_class if there is a meaningful change */
-	if (ncols == 0)
+	if (ncols > 0)
 	{
-		table_close(crel, RowExclusiveLock);
-		return false;
+		HeapTuple	newtup;
+
+		newtup = heap_modify_tuple_by_cols(ctup, tupdesc, ncols, replaces, values,
+										nulls);
+		CatalogTupleUpdate(crel, &newtup->t_self, newtup);
+		heap_freetuple(newtup);
 	}
 
-	newtup = heap_modify_tuple_by_cols(ctup, tupdesc, ncols, replaces, values,
-									   nulls);
-
-	CatalogTupleUpdate(crel, &newtup->t_self, newtup);
-	heap_freetuple(newtup);
-
 	/* release the lock, consistent with vac_update_relstats() */
 	table_close(crel, RowExclusiveLock);
 
-	return true;
+	return (ncols > 0);
 }
 
 /*
-- 
2.46.2

#205Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#204)
Re: Statistics Import and Export

On Wed, Oct 16, 2024 at 7:20 PM Corey Huinker <corey.huinker@gmail.com>
wrote:

Code fix with comment on why nobody expects a relpages -1. Test case to

demonstrate that relpages -1 can happen, and updated doc to reflect the new
lower bound.

Additional fixes, now in a patch-set:

1. Allow relpages to be set to -1 (partitioned tables with partitions have
this value after ANALYZE).
2. Turn off autovacuum on tables (where possible) if they are going to be
the target of pg_set_relation_stats().
3. Allow pg_set_relation_stats to continue past an out-of-range detection
on one attribute, rather than immediately returning false.

There is some uncertainty on what, if anything, should be returned by
pg_set_relation_stats() and pg_set_attribute_stats().

Initially the function (and there was just one) returned void, but it had a
bool return added around the time it split into relation/attribute stats.

Returning a boolean seems like good instrumentation and a helper for
allowing other tooling to use the functions. However, it's rather limited
in what it can convey.

Currently, a return of true means "a record was written", and false means
that a record was not written. Cases where a record was not written for
pg_set_relation_stats amount to the following:

1. No attributes were specified, therefore there is nothing to change.
2. The attributes were set to the values that the record already has, thus
no change was necessary.

#2 can be confusing, because false may look like a failure, but it means
"the pg_class values were already set to what you wanted".

An alternate use of boolean, suggested by Jeff was the following:

1. Return true if all of the fields specified were applied to the record.
2. Return false if any field that was specified was NOT set, even if the
other ones were.

#2 is also confusing in that the user has received a false value, but the
operation did modify the record, just not as fully as the caller had hoped.

These show that a boolean isn't really up to conveying the nuances of
potential outcomes. Complex return types have met with considerable
resistance, enumerations are similarly undesirable, no other scalar value
seems up to the task, and while an INFO or LOG message could convey
considerable complexity, it wouldn't be readily handled programmatically.
This re-raises the question of whether the pg_set_*_stats functions should
return anything at all.

Any feedback on what users would expect from these functions in terms of
return value is appreciated. Bear in mind that these functions will NOT be
integrated into pg_upgrade/pg_dump, as that functionality will be handled
by functions that are less user friendly but more flexible and forgiving of
bad data. We're talking purely about functions meant for tweaking stats to
look for changes in planner behavior.

#206Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#205)
Re: Statistics Import and Export

On Thu, 2024-10-17 at 20:54 -0400, Corey Huinker wrote:

There is some uncertainty on what, if anything, should be returned by
pg_set_relation_stats() and pg_set_attribute_stats().

...

This re-raises the question of whether the pg_set_*_stats functions
should return anything at all.

What is the benefit of a return value from the pg_set_*_stats variants?
As far as I can tell, there is none because they throw an ERROR if
anything goes wrong, so they should just return VOID. What am I
missing?

The return value is more interesting for pg_restore_relation_stats()
and pg_restore_attribute_stats(), which will be used by pg_dump and
which are designed to keep going on non-fatal errors. Isn't that what
this discussion should be about?

Magnus, you previously commented that you'd like some visibility for
tooling:

/messages/by-id/CABUevEz1gLOkWSh_Vd9LQh-JM4i=Mu7PVT9ffc77TmH0Zh3TzA@mail.gmail.com

Is a return value what you had in mind? Or some other function that can
help find missing stats later, or something else entirely?

Regards,
Jeff Davis

#207Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#206)
Re: Statistics Import and Export

What is the benefit of a return value from the pg_set_*_stats variants?
As far as I can tell, there is none because they throw an ERROR if
anything goes wrong, so they should just return VOID. What am I
missing?

Probably nothing. The nuances of "this stat didn't get set but these other
two did" are too complex for a scalar to report, and
pg_set_relation_stats() doesn't do nuance, anyway. I've attached a patch
that returns void instead.

The return value is more interesting for pg_restore_relation_stats()
and pg_restore_attribute_stats(), which will be used by pg_dump and
which are designed to keep going on non-fatal errors. Isn't that what
this discussion should be about?

Yes. I was trying to focus the conversation on something that could be
easily resolved first, and then move on to -restore which is trickier.

Patch that allows relation_statistics_update to continue after one failed
stat (0001) attached, along with bool->void change (0002).

#208Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#207)
2 attachment(s)
Re: Statistics Import and Export

Patch that allows relation_statistics_update to continue after one failed
stat (0001) attached, along with bool->void change (0002).

Once more, with feeling:

Attachments:

v3-0001-Allow-relation_statistics_update-to-continue-afte.patchtext/x-patch; charset=US-ASCII; name=v3-0001-Allow-relation_statistics_update-to-continue-afte.patchDownload
From 0cf8a90c37ec95bbbefb274bc3d48a077b4868b8 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 16 Oct 2024 19:13:01 -0400
Subject: [PATCH v3 1/2] Allow relation_statistics_update to continue after
 non-ERROR validation.

Some validation checks would, when elevel was set to WARNING or lower,
immediately return false after the failed check, rather than let the
update continue on to other attributes.

Currently the only caller is pg_set_relation_stats() which sets the
elevel to ERROR, so this incorrect behavior would never surface, but
later callers will user WARNING elevel.
---
 src/backend/statistics/relation_stats.c | 31 +++++++++----------------
 1 file changed, 11 insertions(+), 20 deletions(-)

diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index 1a6d1640c3..126eab49d8 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -67,7 +67,6 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	bool		nulls[3] = {0};
 	int			ncols = 0;
 	TupleDesc	tupdesc;
-	HeapTuple	newtup;
 
 
 	stats_check_required_arg(fcinfo, relarginfo, RELATION_ARG);
@@ -110,10 +109,8 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("relpages cannot be < -1")));
 			table_close(crel, RowExclusiveLock);
-			return false;
 		}
-
-		if (relpages != pgcform->relpages)
+		else if (relpages != pgcform->relpages)
 		{
 			replaces[ncols] = Anum_pg_class_relpages;
 			values[ncols] = Int32GetDatum(relpages);
@@ -131,10 +128,8 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("reltuples cannot be < -1.0")));
 			table_close(crel, RowExclusiveLock);
-			return false;
 		}
-
-		if (reltuples != pgcform->reltuples)
+		else if (reltuples != pgcform->reltuples)
 		{
 			replaces[ncols] = Anum_pg_class_reltuples;
 			values[ncols] = Float4GetDatum(reltuples);
@@ -152,10 +147,8 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("relallvisible cannot be < 0")));
 			table_close(crel, RowExclusiveLock);
-			return false;
 		}
-
-		if (relallvisible != pgcform->relallvisible)
+		else if (relallvisible != pgcform->relallvisible)
 		{
 			replaces[ncols] = Anum_pg_class_relallvisible;
 			values[ncols] = Int32GetDatum(relallvisible);
@@ -164,22 +157,20 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	}
 
 	/* only update pg_class if there is a meaningful change */
-	if (ncols == 0)
+	if (ncols > 0)
 	{
-		table_close(crel, RowExclusiveLock);
-		return false;
+		HeapTuple	newtup;
+
+		newtup = heap_modify_tuple_by_cols(ctup, tupdesc, ncols, replaces, values,
+										nulls);
+		CatalogTupleUpdate(crel, &newtup->t_self, newtup);
+		heap_freetuple(newtup);
 	}
 
-	newtup = heap_modify_tuple_by_cols(ctup, tupdesc, ncols, replaces, values,
-									   nulls);
-
-	CatalogTupleUpdate(crel, &newtup->t_self, newtup);
-	heap_freetuple(newtup);
-
 	/* release the lock, consistent with vac_update_relstats() */
 	table_close(crel, RowExclusiveLock);
 
-	return true;
+	return (ncols > 0);
 }
 
 /*
-- 
2.47.0

v3-0002-Change-pg_set_relation_stats-return-type-to-void.patchtext/x-patch; charset=US-ASCII; name=v3-0002-Change-pg_set_relation_stats-return-type-to-void.patchDownload
From b36976de8d2e5a797eb48859ef597d8c157e8a3e Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Fri, 18 Oct 2024 19:06:13 -0400
Subject: [PATCH v3 2/2] Change pg_set_relation_stats() return type to void.

This function will either raise an ERROR or run to normal completion.
The boolean return value of relation_statistics_update() is preserved,
but that is useful for more nuanced future uses than
pg_set_relation_stats().
---
 src/include/catalog/pg_proc.dat            |  4 ++--
 src/backend/catalog/system_functions.sql   |  2 +-
 src/backend/statistics/relation_stats.c    |  6 ++++--
 src/test/regress/expected/stats_import.out | 16 ++++++++--------
 doc/src/sgml/func.sgml                     | 11 ++++-------
 5 files changed, 19 insertions(+), 20 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 7c0b74fe05..b4430e7c77 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12345,14 +12345,14 @@
 { oid => '9944',
   descr => 'set statistics on relation',
   proname => 'pg_set_relation_stats', provolatile => 'v', proisstrict => 'f',
-  proparallel => 'u', prorettype => 'bool',
+  proparallel => 'u', prorettype => 'void',
   proargtypes => 'regclass int4 float4 int4',
   proargnames => '{relation,relpages,reltuples,relallvisible}',
   prosrc => 'pg_set_relation_stats' },
 { oid => '9945',
   descr => 'clear statistics on relation',
   proname => 'pg_clear_relation_stats', provolatile => 'v', proisstrict => 'f',
-  proparallel => 'u', prorettype => 'bool',
+  proparallel => 'u', prorettype => 'void',
   proargtypes => 'regclass',
   proargnames => '{relation}',
   prosrc => 'pg_clear_relation_stats' },
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 92b6aefe7a..b7e2906f11 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -644,7 +644,7 @@ CREATE OR REPLACE FUNCTION
                         relpages integer DEFAULT NULL,
                         reltuples real DEFAULT NULL,
                         relallvisible integer DEFAULT NULL)
-RETURNS bool
+RETURNS void
 LANGUAGE INTERNAL
 CALLED ON NULL INPUT VOLATILE
 AS 'pg_set_relation_stats';
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index 126eab49d8..7c6ae4ecd8 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -179,7 +179,8 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 Datum
 pg_set_relation_stats(PG_FUNCTION_ARGS)
 {
-	PG_RETURN_BOOL(relation_statistics_update(fcinfo, ERROR));
+	relation_statistics_update(fcinfo, ERROR);
+	PG_RETURN_VOID();
 }
 
 /*
@@ -202,5 +203,6 @@ pg_clear_relation_stats(PG_FUNCTION_ARGS)
 	newfcinfo->args[3].value = DEFAULT_RELALLVISIBLE;
 	newfcinfo->args[3].isnull = false;
 
-	PG_RETURN_BOOL(relation_statistics_update(newfcinfo, ERROR));
+	relation_statistics_update(newfcinfo, ERROR);
+	PG_RETURN_VOID();
 }
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index a4e3a0f3c4..b50af75bb9 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -38,7 +38,7 @@ SELECT
         relallvisible => 4::integer);
  pg_set_relation_stats 
 -----------------------
- t
+ 
 (1 row)
 
 -- reltuples default
@@ -50,7 +50,7 @@ SELECT
         relallvisible => 4::integer);
  pg_set_relation_stats 
 -----------------------
- t
+ 
 (1 row)
 
 -- relallvisible default
@@ -62,7 +62,7 @@ SELECT
         relallvisible => NULL::integer);
  pg_set_relation_stats 
 -----------------------
- f
+ 
 (1 row)
 
 -- named arguments
@@ -74,7 +74,7 @@ SELECT
         relallvisible => 4::integer);
  pg_set_relation_stats 
 -----------------------
- f
+ 
 (1 row)
 
 SELECT relpages, reltuples, relallvisible
@@ -94,7 +94,7 @@ SELECT
         5::integer);
  pg_set_relation_stats 
 -----------------------
- t
+ 
 (1 row)
 
 SELECT relpages, reltuples, relallvisible
@@ -111,7 +111,7 @@ SELECT
         'stats_import.test'::regclass);
  pg_clear_relation_stats 
 -------------------------
- t
+ 
 (1 row)
 
 SELECT relpages, reltuples, relallvisible
@@ -158,7 +158,7 @@ SELECT
         relpages => 2::integer);
  pg_set_relation_stats 
 -----------------------
- t
+ 
 (1 row)
 
 -- nothing stops us from setting it to -1
@@ -168,7 +168,7 @@ SELECT
         relpages => -1::integer);
  pg_set_relation_stats 
 -----------------------
- t
+ 
 (1 row)
 
 DROP SCHEMA stats_import CASCADE;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index ad663c94d7..14dd035134 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30174,15 +30174,13 @@ DETAIL:  Make sure pg_wal_replay_wait() isn't called within a transaction with a
          <optional>, <parameter>relpages</parameter> <type>integer</type></optional>
          <optional>, <parameter>reltuples</parameter> <type>real</type></optional>
          <optional>, <parameter>relallvisible</parameter> <type>integer</type></optional> )
-         <returnvalue>boolean</returnvalue>
+         <returnvalue>void</returnvalue>
         </para>
         <para>
          Updates relation-level statistics for the given relation to the
          specified values. The parameters correspond to columns in <link
          linkend="catalog-pg-class"><structname>pg_class</structname></link>. Unspecified
-         or <literal>NULL</literal> values leave the setting
-         unchanged. Returns <literal>true</literal> if a change was made;
-         <literal>false</literal> otherwise.
+         or <literal>NULL</literal> values leave the setting unchanged.
         </para>
         <para>
          Ordinarily, these statistics are collected automatically or updated
@@ -30212,12 +30210,11 @@ DETAIL:  Make sure pg_wal_replay_wait() isn't called within a transaction with a
           <primary>pg_clear_relation_stats</primary>
          </indexterm>
          <function>pg_clear_relation_stats</function> ( <parameter>relation</parameter> <type>regclass</type> )
-         <returnvalue>boolean</returnvalue>
+         <returnvalue>void</returnvalue>
         </para>
         <para>
          Clears table-level statistics for the given relation, as though the
-         table was newly created. Returns <literal>true</literal> if a change
-         was made; <literal>false</literal> otherwise.
+         table was newly created.
         </para>
         <para>
          The caller must have the <literal>MAINTAIN</literal> privilege on
-- 
2.47.0

In reply to: Corey Huinker (#208)
1 attachment(s)
RE: Statistics Import and Export

1. Allow relpages to be set to -1 (partitioned tables with partitions have this value after ANALYZE).
2. Turn off autovacuum on tables (where possible) if they are going to be the target of pg_set_relation_stats().
3. Allow pg_set_relation_stats to continue past an out-of-range detection on one attribute, rather than immediately returning false

Thank you for developing this feature.
If the relpages option contains -1 only for partitioned tables, shouldn't pg_set_relation_stats restrict the values that can be
specified by table type? The attached patch limits the value to -1 or more if the target
is a partition table, and 0 or more otherwise.
Changing relpages to -1 on a non-partitioned table seems to significantly change the execution plan.

---
postgres=> EXPLAIN (COSTS OFF) SELECT * FROM data1 WHERE c1=1000;
QUERY PLAN
--------------------------------------
Index Scan using data1_pkey on data1
Index Cond: (c1 = 1000)
(2 rows)

postgres=> SELECT pg_set_relation_stats('data1', relpages=>-1);
pg_set_relation_stats
-----------------------
t
(1 row)

postgres=> EXPLAIN (COSTS OFF) SELECT * FROM data1 WHERE c1=1000;
QUERY PLAN
---------------------------------------
Bitmap Heap Scan on data1
Recheck Cond: (c1 = 1000)
-> Bitmap Index Scan on data1_pkey
Index Cond: (c1 = 1000)
(4 rows)
---

Regards,
Noriyoshi Shinoda

From: Corey Huinker <corey.huinker@gmail.com>
Sent: Saturday, October 19, 2024 10:00 AM
To: Jeff Davis <pgsql@j-davis.com>
Cc: Shinoda, Noriyoshi (SXD Japan FSIP) <noriyoshi.shinoda@hpe.com>; jian he <jian.universality@gmail.com>; Matthias van de Meent <boekewurm+postgres@gmail.com>; Bruce Momjian <bruce@momjian.us>; Tom Lane <tgl@sss.pgh.pa.us>; Nathan Bossart <nathandbossart@gmail.com>; Magnus Hagander <magnus@hagander.net>; Stephen Frost <sfrost@snowman.net>; Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; Peter Smith <smithpb2250@gmail.com>; PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; alvherre@alvh.no-ip.org
Subject: Re: Statistics Import and Export

Patch that allows relation_statistics_update to continue after one failed stat (0001) attached, along with bool->void change (0002).

Once more, with feeling:

Attachments:

pg_set_relation_stats_patch_v1.diffapplication/octet-stream; name=pg_set_relation_stats_patch_v1.diffDownload
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index ad663c94d7..bb610357c3 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30197,7 +30197,8 @@ DETAIL:  Make sure pg_wal_replay_wait() isn't called within a transaction with a
         </para>
         <para>
          The value of <structfield>relpages</structfield> must be greater than
-         or equal to <literal>-1</literal>,
+         or equal to <literal>-1</literal> for partitioned table otherwise
+         greater then or equal to <literal>0</literal>,
          <structfield>reltuples</structfield> must be greater than or equal to
          <literal>-1.0</literal>, and <structfield>relallvisible</structfield>
          must be greater than or equal to <literal>0</literal>.
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index 1a6d1640c3..5ad3368463 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -98,17 +98,21 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	if (!PG_ARGISNULL(RELPAGES_ARG))
 	{
 		int32		relpages = PG_GETARG_INT32(RELPAGES_ARG);
+		int32		minpages = 0;
 
 		/*
 		 * Partitioned tables may have relpages=-1. Note: for relations with
 		 * no storage, relpages=-1 is not used consistently, but must be
 		 * supported here.
 		 */
-		if (relpages < -1)
+		if (pgcform->relkind == RELKIND_PARTITIONED_TABLE)
+			minpages = -1;
+
+		if (relpages < minpages)
 		{
 			ereport(elevel,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("relpages cannot be < -1")));
+					 errmsg("relpages cannot be < %d", minpages)));
 			table_close(crel, RowExclusiveLock);
 			return false;
 		}
#210Corey Huinker
corey.huinker@gmail.com
In reply to: Shinoda, Noriyoshi (SXD Japan FSIP) (#209)
Re: Statistics Import and Export

If the relpages option contains -1 only for partitioned tables, shouldn't
pg_set_relation_stats restrict the values that can be

specified by table type? The attached patch limits the value to -1 or more
if the target

is a partition table, and 0 or more otherwise.

Changing relpages to -1 on a non-partitioned table seems to significantly
change the execution plan.

Short answer: It's working as intended. Significantly changing the
execution plan in weird ways is part of the intention of the function, even
if the execution plan changes for the worse. I appreciate

Longer answer:

Enforcing -1 on only partitioned tables is tricky, as it seems to be a
value for any table that has no local storage. So foreign data wrapper
tables could, in theory, also have this value. More importantly, the -1
value seems to be situational, in my experience it only happens on
partitioned tables after they have their first partition added, which means
that the current valid stat range is set according to facts that can
change. Like so :

chuinker=# select version();
version

------------------------------------------------------------------------------------------------------------------------------------
PostgreSQL 16.4 (Postgres.app) on aarch64-apple-darwin21.6.0, compiled by
Apple clang version 14.0.0 (clang-1400.0.29.102), 64-bit
(1 row)
chuinker=# create table part_parent (x integer) partition by range (x);
CREATE TABLE
chuinker=# select relpages from pg_class where oid =
'part_parent'::regclass;
relpages
----------
0
(1 row)

chuinker=# analyze part_parent;
ANALYZE
chuinker=# select relpages from pg_class where oid =
'part_parent'::regclass;
relpages
----------
0
(1 row)

chuinker=# create table part_child partition of part_parent for values from
(0) TO (100);
CREATE TABLE
chuinker=# select relpages from pg_class where oid =
'part_parent'::regclass;
relpages
----------
0
(1 row)

chuinker=# analyze part_parent;
ANALYZE
chuinker=# select relpages from pg_class where oid =
'part_parent'::regclass;
relpages
----------
-1
(1 row)

chuinker=# drop table part_child;
DROP TABLE
chuinker=# select relpages from pg_class where oid =
'part_parent'::regclass;
relpages
----------
-1
(1 row)

chuinker=# analyze part_parent;
ANALYZE
chuinker=# select relpages from pg_class where oid =
'part_parent'::regclass;
relpages
----------
-1
(1 row)

Prior versions (March 2024 and earlier) of this patch and the
pg_set_attribute_stats patch did have many checks to prevent importing stat
values that were "wrong" in some way. Some examples from attribute stats
import were:

* Histograms that were not monotonically nondecreasing.
* Frequency values that were out of bounds specified by other values in the
array.
* Frequency values outside of the [0.0,1.0] or [-1.0,1.0] depending on the
stat type.
* paired arrays of most-common-values and their attendant frequency array
not having the same length

All of these checks were removed based on feedback from reviewers and
committers who saw the pg_set_*_stats() functions as a fuzzing tool, so the
ability to set illogical, wildly implausible, or mathematically impossible
values was a feature, not a bug. I would suspect that they would view your
demonstration that setting impossible values on a table as proof that the
function can be used to experiment with planner scenarios. So, while I
previously would have eagerly accepted this patch as another valued
validation check, such checks don't fit with the new intention of the
functions. Still, I greatly appreciate your helping us discover ways in
which we can use this tool to make the planner do odd things.

One thing that could cause us to enforce a check like the one you submitted
would be if an invalid value caused a query to fail or a session to crash,
even then, that would probably spur a change to make the planner more
defensive rather than more checks on the set_* side.

Show quoted text
#211Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#191)
Re: Statistics Import and Export

I've taken most of Jeff's work, reincorporated it into roughly the
same patch structure as before, and am posting it now.

I committed 0001-0004 with significant revision.

Regards,
Jeff Davis

In reply to: Jeff Davis (#211)
1 attachment(s)
RE: Statistics Import and Export

Hi,

I committed 0001-0004 with significant revision.

Thanks for developing good features. I tried the patch that was committed right away.
It seems that the implementation and documentation differ on the return value of the pg_clear_attribute_stats function.
The attached small patch fixes the documentation.

Regards,
Noriyoshi Shinoda

-----Original Message-----
From: Jeff Davis <pgsql@j-davis.com>
Sent: Wednesday, October 23, 2024 7:28 AM
To: Corey Huinker <corey.huinker@gmail.com>; jian he <jian.universality@gmail.com>
Cc: Matthias van de Meent <boekewurm+postgres@gmail.com>; Bruce Momjian <bruce@momjian.us>; Tom Lane <tgl@sss.pgh.pa.us>; Nathan Bossart <nathandbossart@gmail.com>; Magnus Hagander <magnus@hagander.net>; Stephen Frost <sfrost@snowman.net>; Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>; Peter Smith <smithpb2250@gmail.com>; PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; Tomas Vondra <tomas.vondra@enterprisedb.com>; alvherre@alvh.no-ip.org
Subject: Re: Statistics Import and Export

I've taken most of Jeff's work, reincorporated it into roughly the
same patch structure as before, and am posting it now.

I committed 0001-0004 with significant revision.

Regards,
Jeff Davis

Attachments:

pg_clear_attribute_stats_doc_v1.diffapplication/octet-stream; name=pg_clear_attribute_stats_doc_v1.diffDownload
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index ba5656c86b..3ce70b1397 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30281,7 +30281,7 @@ DETAIL:  Make sure pg_wal_replay_wait() isn't called within a transaction with a
          <parameter>relation</parameter> <type>regclass</type>,
          <parameter>attname</parameter> <type>name</type>,
          <parameter>inherited</parameter> <type>boolean</type> )
-         <returnvalue>boolean</returnvalue>
+         <returnvalue>void</returnvalue>
         </para>
         <para>
          Clears table-level statistics for the given relation attribute, as
#213Jeff Davis
pgsql@j-davis.com
In reply to: Shinoda, Noriyoshi (SXD Japan FSIP) (#212)
Re: Statistics Import and Export

On Tue, 2024-10-22 at 23:58 +0000, Shinoda, Noriyoshi (SXD Japan FSIP)
wrote:

Thanks for developing good features. I tried the patch that was
committed right away.
It seems that the implementation and documentation differ on the
return value of the pg_clear_attribute_stats function.
The attached small patch fixes the documentation.

Thank you again, fixed.

Regards,
Jeff Davis

#214Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#191)
Re: Statistics Import and Export

On Tue, 2024-09-17 at 05:02 -0400, Corey Huinker wrote:

I've taken most of Jeff's work, reincorporated it into roughly the
same patch structure as before, and am posting it now.

I have committed the import side of this patch series; that is, the
function calls that can load stats into an existing cluster without the
need to ANALYZE.

The pg_restore_*_stats() functions are designed such that pg_dump can
emit the calls. Some design choices of the functions worth noting:

(a) a variadic signature of name/value pairs rather than ordinary SQL
arguments, which makes it easier for future versions to interpret what
has been output from a previous version; and
(b) many kinds of errors are demoted to WARNINGs, to allow some
statistics to be set for an attribute even if other statistics are
malformed (also a future-proofing design); and
(c) we are considering whether to use an in-place heap update for the
relation stats, so that a large restore doesn't bloat pg_class -- I'd
like feedback on this idea

The pg_set_*_stats() functions are designed for interactive use, such
as tweaking statistics for planner testing, experimentation, or
reproducing a plan outside of a production system. The aforementioned
design choices don't make a lot of sense in this context, so that's why
the pg_set_*_stats() functions are separate from the
pg_restore_*_stats() functions. But there's a lot of overlap, so it may
be worth discussing again whether we should only have one set of
functions.

Regards,
Jeff Davis

#215Alexander Lakhin
exclusion@gmail.com
In reply to: Jeff Davis (#214)
Re: Statistics Import and Export

Hello Jeff and Corey,

26.10.2024 01:18, Jeff Davis wrote:

On Tue, 2024-09-17 at 05:02 -0400, Corey Huinker wrote:

I've taken most of Jeff's work, reincorporated it into roughly the
same patch structure as before, and am posting it now.

I have committed the import side of this patch series; that is, the
function calls that can load stats into an existing cluster without the
need to ANALYZE.

The pg_restore_*_stats() functions are designed such that pg_dump can
emit the calls. Some design choices of the functions worth noting:

Please look at the following seemingly atypical behavior of the new
functions:
CREATE TABLE test(id int);

SELECT pg_restore_attribute_stats(
  'relation', 'test'::regclass,
  'attname', 'id'::name,
  'inherited', false);

SELECT pg_restore_attribute_stats(
  'relation', 'test'::regclass,
  'attname', 'id'::name,
  'inherited', false
) FROM generate_series(1, 2);
ERROR:  XX000: tuple already updated by self
LOCATION:  simple_heap_update, heapam.c:4353

Or:
SELECT pg_clear_attribute_stats('test'::regclass, 'id'::name, false)
FROM generate_series(1, 2);
ERROR:  XX000: tuple already updated by self
LOCATION:  simple_heap_delete, heapam.c:3108

Best regards,
Alexander

#216Jeff Davis
pgsql@j-davis.com
In reply to: Alexander Lakhin (#215)
1 attachment(s)
Re: Statistics Import and Export

On Sun, 2024-10-27 at 14:00 +0300, Alexander Lakhin wrote:

Please look at the following seemingly atypical behavior of the new
functions:

...

SELECT pg_restore_attribute_stats(
   'relation', 'test'::regclass,
   'attname', 'id'::name,
   'inherited', false
) FROM generate_series(1, 2);
ERROR:  XX000: tuple already updated by self

Thank you for the report!

Attached a patch to add calls to CommandCounterIncrement().

Regards,
Jeff Davis

Attachments:

0001-Add-missing-CommandCounterIncrement-in-stats-import-.patchtext/x-patch; charset=UTF-8; name=0001-Add-missing-CommandCounterIncrement-in-stats-import-.patchDownload
From ccff3df8b2d6f0c139a39e5aef8721b4480bdbd3 Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Mon, 28 Oct 2024 18:16:09 -0700
Subject: [PATCH] Add missing CommandCounterIncrement() in stats import
 functions.

Reported-by: Alexander Lakhin
Discussion: https://postgr.es/m/98b2fcf0-f701-369e-d63d-6be9739ce17c@gmail.com
---
 src/backend/statistics/attribute_stats.c | 11 ++++++++---
 src/backend/statistics/relation_stats.c  |  2 ++
 2 files changed, 10 insertions(+), 3 deletions(-)

diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index af61fd79e4..4ae0722b78 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -752,6 +752,8 @@ upsert_pg_statistic(Relation starel, HeapTuple oldtup,
 	}
 
 	heap_freetuple(newtup);
+
+	CommandCounterIncrement();
 }
 
 /*
@@ -762,6 +764,7 @@ delete_pg_statistic(Oid reloid, AttrNumber attnum, bool stainherit)
 {
 	Relation	sd = table_open(StatisticRelationId, RowExclusiveLock);
 	HeapTuple	oldtup;
+	bool		result = false;
 
 	/* Is there already a pg_statistic tuple for this attribute? */
 	oldtup = SearchSysCache3(STATRELATTINH,
@@ -773,12 +776,14 @@ delete_pg_statistic(Oid reloid, AttrNumber attnum, bool stainherit)
 	{
 		CatalogTupleDelete(sd, &oldtup->t_self);
 		ReleaseSysCache(oldtup);
-		table_close(sd, RowExclusiveLock);
-		return true;
+		result = true;
 	}
 
 	table_close(sd, RowExclusiveLock);
-	return false;
+
+	CommandCounterIncrement();
+
+	return result;
 }
 
 /*
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index 5a2aabc921..ed5dea2e05 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -171,6 +171,8 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	/* release the lock, consistent with vac_update_relstats() */
 	table_close(crel, RowExclusiveLock);
 
+	CommandCounterIncrement();
+
 	return result;
 }
 
-- 
2.34.1

#217Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#214)
Re: Statistics Import and Export

(c) we are considering whether to use an in-place heap update for the
relation stats, so that a large restore doesn't bloat pg_class -- I'd
like feedback on this idea

I'd also like feedback, though I feel very strongly that we should do what
ANALYZE does. In an upgrade situation, nearly all tables will have stats
imported, which would result in an immediate doubling of pg_class - not the
end of the world, but not great either.

Given the recent bugs associated with inplace updates and race conditions,
if we don't want to do in-place here, we should also consider getting rid
of it for ANALYZE. I briefly pondered if it would make sense to vertically
partition pg_class into the stable attributes and the attributes that get
modified in-place, but that list is pretty long: relpages, reltuples,
relallvisible, relhasindex, reltoastrelid, relhasrules, relhastriggers,
relfrozenxid, and reminmxid,

If we don't want to do inplace updates in pg_restore_relation_stats(), then
we could mitigate the bloat with a VACUUM FULL pg_class at the tail end of
the upgrade if stats were enabled.

pg_restore_*_stats() functions. But there's a lot of overlap, so it may
be worth discussing again whether we should only have one set of
functions.

For the reason of in-place updates and error tolerance, I think they have
to remain separate functions, but I'm also interested in hearing other's
opinions.

#218Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#217)
5 attachment(s)
Re: Statistics Import and Export

I'd also like feedback, though I feel very strongly that we should do what
ANALYZE does. In an upgrade situation, nearly all tables will have stats
imported, which would result in an immediate doubling of pg_class - not the
end of the world, but not great either.

Given the recent bugs associated with inplace updates and race conditions,
if we don't want to do in-place here, we should also consider getting rid
of it for ANALYZE. I briefly pondered if it would make sense to vertically
partition pg_class into the stable attributes and the attributes that get
modified in-place, but that list is pretty long: relpages, reltuples,
relallvisible, relhasindex, reltoastrelid, relhasrules, relhastriggers,
relfrozenxid, and reminmxid,

If we don't want to do inplace updates in pg_restore_relation_stats(),
then we could mitigate the bloat with a VACUUM FULL pg_class at the tail
end of the upgrade if stats were enabled.

pg_restore_*_stats() functions. But there's a lot of overlap, so it may
be worth discussing again whether we should only have one set of
functions.

For the reason of in-place updates and error tolerance, I think they have
to remain separate functions, but I'm also interested in hearing other's
opinions.

A lot of stuff has been committed, but a lot still remains, so I'm going to
give a state-of-the-patchset update.

WHAT HAS BEEN COMMITTED

The functions pg_set_relation_stats(), and pg_set_attribute_stats(). These
are new functions allowing the table owner/maintainer to inject arbitrary
statistics into a table/index/matview or attribute thereof. The goal is to
provide a tool to test "what if" experiments on the planner.

The function pg_clear_relation_stats(). It resets pg_class stats variables
back to the new-table defaults. However, the potential change to make the
default -1 for partitioned tables/indexes means that this function would
need to be updated.

The function pg_clear_attribute_stats(). This function deletes the
pg_statistic row associated with a given attribute.

The functions pg_restore_relation_stats() and pg_restore_attribute_stats().
These perform a similar function to their -set counterparts, but in a way
more friendly to use via pg_upgrade, and using a parameter syntax that's
future-tolerant if not future-proof.

NEXT STEPS, PATCHES ATTACHED

0001

Add booleans dumpSchema, dumpData to the dump/restore options structures
without removing schemaOnly and dataOnly. This patch is borne of the
combinatorial complexity that we would have to deal with if we kept all
"should I dump this object?" logic based on schemaOnly, dataOnly, and a new
statisticsOnly flag. Better to just flip these negatives to positives and
resolve them once.

0002

Based on feedback from Nathan Bossart. This removes schemaOnly and dataOnly
from the dump/restore option structure as they are now redundant and could
cause confusion.

0003

This adds statistics import to pg_restore and pg_upgrade. This is what all
of these patches have been leading up to.

0004

Importing statistics in a pg_upgrade would touch all relations (tables,
indexes, matviews) in a database, meaning that each pg_class entry would be
updated. If those updates are not inplace (as is done in VACUUM and
ANALYZE), then we've just added near-100% bloat to pg_class. To avoid that,
we re-introduce inplace updates for pg_restore_relation_stats().

0005

Depending on the outcome of
/messages/by-id/be7951d424b9c16c761c39b9b2677a57fb697b1f.camel@j-davis.com,
we may have to modify pg_clear_relation_stats() to give a different default
relpages depending on the relkind. If that comes to pass, then this patch
will be needed.

WHAT IS NOT DONE - EXTENDED STATISTICS

It is a general consensus in the community that "nobody uses extended
statistics", though I've had difficulty getting actual figures to back this
up, even from my own employer. Surveying several vendors at PgConf.EU, the
highest estimate was that at most 1% of their customers used extended
statistics, though more probably should. This reinforces my belief that a
feature that would eliminate a major pain point in upgrades for 99% of
customers shouldn't be held back by the fact that the other 1% only have a
reduced hassle.

However, having relation and attribute statistics carry over on major
version upgrades presents a slight problem: running vacuumdb
--analyze-in-stages after such an upgrade is completely unnecessary for
those without extended statistics, and would actually result in _worse_
statistics for the database until the last stage is complete. Granted,
we've had great difficulty getting users to know that vacuumdb is a thing
that should be run, but word has slowly spread through our own
documentation and those "This one simple trick will make your postgres go
fast post-upgrade" blog posts. Those posts will continue to lurk in search
results long after this feature goes into release, and it would be a rude
surprise to users to find out that the extra work they put in to learn
about a feature that helped their upgrade in 17 was suddenly detrimental
(albeit temporarily) in 18. We should never punish people for only being a
little-bit current in their knowledge. Moreover, this surprise would
persist even after we add extended statistics import function functionality.

I presented this problem to several people at PgConf.EU, and the consensus
least-bad solution was that vacuumdb should filter out tables that are not
missing any statistics when using options --analyze, --analyze-only, and
--analyze-in-stages, with an additional flag for now called --force-analyze
to restore the un-filtered functionality. This gives the outcome tree:

1. Users who do not have extended statistics and do not use (or not even
know about) vacuumdb will be blissfully unaware, and will get better
post-upgrade performance.
2. Users who do not have extended statistics but use vacuumdb
--analyze-in-stages will be pleasantly surprised that the vacuumdb run is
almost a no-op, and completes quickly. Those who are surprised by this and
re-run vacuumdb --analyze-in-stages will get another no-op.
3. Users who have extended statistics and use vacuumdb --analyze-in-stages
will get a quicker vacuumdb run, as only the tables with extended stats
will pass the filter. Subsequent re-runs of vacuumdb --analyze-in-stages
would be the no-op.
4. Users who have extended statistics and don't use vacuumdb will still get
better performance than they would have without any stats imported.

In case anyone is curious, I'm defining "missing stats" as a table/matview
with any of the following:

1. A table with an attribute that lacks a corresponding pg_statistic row.
2. A table with an index with an expression attribute that lacks a
corresponding pg_statistic row (non-expression attributes just borrow the
pg_statistic row from the table's attribute).
3. A table with at least one extended statistic that does not have a
corresponding pg_statistic_ext_data row.

Note that none of these criteria are concerned with the substance of the
statistics (ex. pg_statistic row should have mcv stats but does not),
merely their row-existence.

Some rejected alternative solutions were:

1. Adding a new option --analyze-missing-stats. While simple, few people
would learn about it, knowledge of it would be drowned out by the
aforementioned sea of existing blog posts.
2. Adding --analyze-missing-stats and making --analyze-in-stages fail with
an error message educating the user about --analyze-missing-stats. Users
might not see the error, existing tooling wouldn't be able to act on the
error, and there are legitimate non-upgrade uses of --analyze-in-stages.

MAIN CONCERN GOING FORWARD

This change to vacuumdb will require some reworking of the
vacuum_one_database() function so that the list of tables analyzed is
preserved across the stages, as subsequent stages runs won't be able to
detect which tables were previously missing stats.

Attachments:

v30-0001-Add-derivative-flags-dumpSchema-dumpData.patchapplication/x-patch; name=v30-0001-Add-derivative-flags-dumpSchema-dumpData.patchDownload
From b329af5d656873938a7fc77cd9244c32e078adf3 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 4 May 2024 04:52:38 -0400
Subject: [PATCH v30 1/4] Add derivative flags dumpSchema, dumpData.

User-set flags --schema-only and --data-only are often consulted by
various operations to determine if they should be skipped or not. While
this logic works when there are only two mutually-exclusive -only
options, it will get progressively more confusing when more are added.

In anticipation of this, create the flags dumpSchema and dumpData which
are derivative of the existing options schemaOnly and dataOnly. This
allows us to restate current skip-this-section tests in terms of what is
enabled, rather than checking if the other -only mode is turned off.
---
 src/bin/pg_dump/pg_backup.h  |   8 ++
 src/bin/pg_dump/pg_dump.c    | 186 ++++++++++++++++++-----------------
 src/bin/pg_dump/pg_restore.c |   4 +
 3 files changed, 107 insertions(+), 91 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 68ae2970ad..9bd3266a63 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -158,6 +158,10 @@ typedef struct _restoreOptions
 	int			enable_row_security;
 	int			sequence_data;	/* dump sequence data even in schema-only mode */
 	int			binary_upgrade;
+
+	/* flags derived entirely from the user-settable flags */
+	bool		dumpSchema;
+	bool		dumpData;
 } RestoreOptions;
 
 typedef struct _dumpOptions
@@ -204,6 +208,10 @@ typedef struct _dumpOptions
 
 	int			sequence_data;	/* dump sequence data even in schema-only mode */
 	int			do_nothing;
+
+	/* flags derived entirely from the user-settable flags */
+	bool		dumpSchema;
+	bool		dumpData;
 } DumpOptions;
 
 /*
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index d8c6330732..3eb8523fd7 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -793,6 +793,10 @@ main(int argc, char **argv)
 	if (dopt.if_exists && !dopt.outputClean)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
+	/* set derivative flags */
+	dopt.dumpSchema = (!dopt.dataOnly);
+	dopt.dumpData = (!dopt.schemaOnly);
+
 	/*
 	 * --inserts are already implied above if --column-inserts or
 	 * --rows-per-insert were specified.
@@ -975,7 +979,7 @@ main(int argc, char **argv)
 	 * -s means "schema only" and LOs are data, not schema, so we never
 	 * include LOs when -s is used.
 	 */
-	if (dopt.include_everything && !dopt.schemaOnly && !dopt.dontOutputLOs)
+	if (dopt.include_everything && dopt.dumpData && !dopt.dontOutputLOs)
 		dopt.outputLOs = true;
 
 	/*
@@ -989,15 +993,15 @@ main(int argc, char **argv)
 	 */
 	tblinfo = getSchemaData(fout, &numTables);
 
-	if (!dopt.schemaOnly)
+	if (dopt.dumpData)
 	{
 		getTableData(&dopt, tblinfo, numTables, 0);
 		buildMatViewRefreshDependencies(fout);
-		if (dopt.dataOnly)
+		if (!dopt.dumpSchema)
 			getTableDataFKConstraints();
 	}
 
-	if (dopt.schemaOnly && dopt.sequence_data)
+	if (!dopt.dumpData && dopt.sequence_data)
 		getTableData(&dopt, tblinfo, numTables, RELKIND_SEQUENCE);
 
 	/*
@@ -4159,8 +4163,8 @@ dumpPolicy(Archive *fout, const PolicyInfo *polinfo)
 	const char *cmd;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
@@ -4375,8 +4379,8 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 	char	   *qpubname;
 	bool		first = true;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	delq = createPQExpBuffer();
@@ -4690,8 +4694,8 @@ dumpPublicationNamespace(Archive *fout, const PublicationSchemaInfo *pubsinfo)
 	PQExpBuffer query;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	tag = psprintf("%s %s", pubinfo->dobj.name, schemainfo->dobj.name);
@@ -4733,8 +4737,8 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 	PQExpBuffer query;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	tag = psprintf("%s %s", pubinfo->dobj.name, tbinfo->dobj.name);
@@ -5119,8 +5123,8 @@ dumpSubscriptionTable(Archive *fout, const SubRelInfo *subrinfo)
 	PQExpBuffer query;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	Assert(fout->dopt->binary_upgrade && fout->remoteVersion >= 170000);
@@ -5193,8 +5197,8 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo)
 	int			i;
 	char		two_phase_disabled[] = {LOGICALREP_TWOPHASE_STATE_DISABLED, '\0'};
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	delq = createPQExpBuffer();
@@ -7345,8 +7349,8 @@ getPartitioningInfo(Archive *fout)
 	/* hash partitioning didn't exist before v11 */
 	if (fout->remoteVersion < 110000)
 		return;
-	/* needn't bother if schema-only dump */
-	if (fout->dopt->schemaOnly)
+	/* needn't bother if not dumping data */
+	if (!fout->dopt->dumpData)
 		return;
 
 	query = createPQExpBuffer();
@@ -8998,7 +9002,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * Now get info about column defaults.  This is skipped for a data-only
 	 * dump, as it is only needed for table schemas.
 	 */
-	if (!dopt->dataOnly && tbloids->len > 1)
+	if (dopt->dumpSchema && tbloids->len > 1)
 	{
 		AttrDefInfo *attrdefs;
 		int			numDefaults;
@@ -9128,7 +9132,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * Get info about table CHECK constraints.  This is skipped for a
 	 * data-only dump, as it is only needed for table schemas.
 	 */
-	if (!dopt->dataOnly && checkoids->len > 2)
+	if (dopt->dumpSchema && checkoids->len > 2)
 	{
 		ConstraintInfo *constrs;
 		int			numConstrs;
@@ -10038,13 +10042,13 @@ dumpCommentExtended(Archive *fout, const char *type,
 	/* Comments are schema not data ... except LO comments are data */
 	if (strcmp(type, "LARGE OBJECT") != 0)
 	{
-		if (dopt->dataOnly)
+		if (!dopt->dumpSchema)
 			return;
 	}
 	else
 	{
 		/* We do dump LO comments in binary-upgrade mode */
-		if (dopt->schemaOnly && !dopt->binary_upgrade)
+		if (!dopt->dumpData && !dopt->binary_upgrade)
 			return;
 	}
 
@@ -10151,7 +10155,7 @@ dumpTableComment(Archive *fout, const TableInfo *tbinfo,
 		return;
 
 	/* Comments are SCHEMA not data */
-	if (dopt->dataOnly)
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Search for comments associated with relation, using table */
@@ -10597,8 +10601,8 @@ dumpNamespace(Archive *fout, const NamespaceInfo *nspinfo)
 	PQExpBuffer delq;
 	char	   *qnspname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -10674,8 +10678,8 @@ dumpExtension(Archive *fout, const ExtensionInfo *extinfo)
 	PQExpBuffer delq;
 	char	   *qextname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -10799,8 +10803,8 @@ dumpType(Archive *fout, const TypeInfo *tyinfo)
 {
 	DumpOptions *dopt = fout->dopt;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Dump out in proper style */
@@ -11910,8 +11914,8 @@ dumpShellType(Archive *fout, const ShellTypeInfo *stinfo)
 	DumpOptions *dopt = fout->dopt;
 	PQExpBuffer q;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -11962,8 +11966,8 @@ dumpProcLang(Archive *fout, const ProcLangInfo *plang)
 	FuncInfo   *inlineInfo = NULL;
 	FuncInfo   *validatorInfo = NULL;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
@@ -12170,8 +12174,8 @@ dumpFunc(Archive *fout, const FuncInfo *finfo)
 	int			nconfigitems = 0;
 	const char *keyword;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -12562,8 +12566,8 @@ dumpCast(Archive *fout, const CastInfo *cast)
 	const char *sourceType;
 	const char *targetType;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Cannot dump if we don't have the cast function's info */
@@ -12668,8 +12672,8 @@ dumpTransform(Archive *fout, const TransformInfo *transform)
 	char	   *lanname;
 	const char *transformType;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Cannot dump if we don't have the transform functions' info */
@@ -12817,8 +12821,8 @@ dumpOpr(Archive *fout, const OprInfo *oprinfo)
 	char	   *oprregproc;
 	char	   *oprref;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
@@ -13104,8 +13108,8 @@ dumpAccessMethod(Archive *fout, const AccessMethodInfo *aminfo)
 	PQExpBuffer delq;
 	char	   *qamname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -13207,8 +13211,8 @@ dumpOpclass(Archive *fout, const OpclassInfo *opcinfo)
 	bool		needComma;
 	int			i;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -13478,8 +13482,8 @@ dumpOpfamily(Archive *fout, const OpfamilyInfo *opfinfo)
 	bool		needComma;
 	int			i;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -13685,8 +13689,8 @@ dumpCollation(Archive *fout, const CollInfo *collinfo)
 	const char *colllocale;
 	const char *collicurules;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -13939,8 +13943,8 @@ dumpConversion(Archive *fout, const ConvInfo *convinfo)
 	const char *conproc;
 	bool		condefault;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -14087,8 +14091,8 @@ dumpAgg(Archive *fout, const AggInfo *agginfo)
 	const char *proparallel;
 	char		defaultfinalmodify;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -14417,8 +14421,8 @@ dumpTSParser(Archive *fout, const TSParserInfo *prsinfo)
 	PQExpBuffer delq;
 	char	   *qprsname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14485,8 +14489,8 @@ dumpTSDictionary(Archive *fout, const TSDictInfo *dictinfo)
 	char	   *nspname;
 	char	   *tmplname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14561,8 +14565,8 @@ dumpTSTemplate(Archive *fout, const TSTemplateInfo *tmplinfo)
 	PQExpBuffer delq;
 	char	   *qtmplname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14627,8 +14631,8 @@ dumpTSConfig(Archive *fout, const TSConfigInfo *cfginfo)
 	int			i_tokenname;
 	int			i_dictname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14739,8 +14743,8 @@ dumpForeignDataWrapper(Archive *fout, const FdwInfo *fdwinfo)
 	PQExpBuffer delq;
 	char	   *qfdwname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14812,8 +14816,8 @@ dumpForeignServer(Archive *fout, const ForeignServerInfo *srvinfo)
 	char	   *qsrvname;
 	char	   *fdwname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -15003,8 +15007,8 @@ dumpDefaultACL(Archive *fout, const DefaultACLInfo *daclinfo)
 	PQExpBuffer tag;
 	const char *type;
 
-	/* Do nothing in data-only dump, or if we're skipping ACLs */
-	if (dopt->dataOnly || dopt->aclsSkip)
+	/* Do nothing if not dumping schema, or if we're skipping ACLs */
+	if (!dopt->dumpSchema || dopt->aclsSkip)
 		return;
 
 	q = createPQExpBuffer();
@@ -15104,7 +15108,7 @@ dumpACL(Archive *fout, DumpId objDumpId, DumpId altDumpId,
 		return InvalidDumpId;
 
 	/* --data-only skips ACLs *except* large object ACLs */
-	if (dopt->dataOnly && strcmp(type, "LARGE OBJECT") != 0)
+	if (!dopt->dumpSchema && strcmp(type, "LARGE OBJECT") != 0)
 		return InvalidDumpId;
 
 	sql = createPQExpBuffer();
@@ -15233,13 +15237,13 @@ dumpSecLabel(Archive *fout, const char *type, const char *name,
 	 */
 	if (strcmp(type, "LARGE OBJECT") != 0)
 	{
-		if (dopt->dataOnly)
+		if (!dopt->dumpSchema)
 			return;
 	}
 	else
 	{
 		/* We do dump large object security labels in binary-upgrade mode */
-		if (dopt->schemaOnly && !dopt->binary_upgrade)
+		if (!dopt->dumpData && !dopt->binary_upgrade)
 			return;
 	}
 
@@ -15307,7 +15311,7 @@ dumpTableSecLabel(Archive *fout, const TableInfo *tbinfo, const char *reltypenam
 		return;
 
 	/* SecLabel are SCHEMA not data */
-	if (dopt->dataOnly)
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Search for comments associated with relation, using table */
@@ -15546,8 +15550,8 @@ dumpTable(Archive *fout, const TableInfo *tbinfo)
 	DumpId		tableAclDumpId = InvalidDumpId;
 	char	   *namecopy;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	if (tbinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
@@ -16643,8 +16647,8 @@ dumpTableAttach(Archive *fout, const TableAttachInfo *attachinfo)
 	PGresult   *res;
 	char	   *partbound;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -16715,8 +16719,8 @@ dumpAttrDef(Archive *fout, const AttrDefInfo *adinfo)
 	char	   *tag;
 	char	   *foreign;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Skip if not "separate"; it was dumped in the table's definition */
@@ -16804,8 +16808,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 	char	   *qindxname;
 	char	   *qqindxname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -16937,8 +16941,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 static void
 dumpIndexAttach(Archive *fout, const IndexAttachInfo *attachinfo)
 {
-	/* Do nothing in data-only dump */
-	if (fout->dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!fout->dopt->dumpSchema)
 		return;
 
 	if (attachinfo->partitionIdx->dobj.dump & DUMP_COMPONENT_DEFINITION)
@@ -16984,8 +16988,8 @@ dumpStatisticsExt(Archive *fout, const StatsExtInfo *statsextinfo)
 	PGresult   *res;
 	char	   *stxdef;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -17060,8 +17064,8 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
 	char	   *tag = NULL;
 	char	   *foreign;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -17797,8 +17801,8 @@ dumpTrigger(Archive *fout, const TriggerInfo *tginfo)
 	char	   *qtabname;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -17919,8 +17923,8 @@ dumpEventTrigger(Archive *fout, const EventTriggerInfo *evtinfo)
 	PQExpBuffer delqry;
 	char	   *qevtname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -18010,8 +18014,8 @@ dumpRule(Archive *fout, const RuleInfo *rinfo)
 	PGresult   *res;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index df119591cc..3475168a64 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -360,6 +360,10 @@ main(int argc, char **argv)
 	if (opts->single_txn && numWorkers > 1)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
+	/* set derivative flags */
+	opts->dumpSchema = (opts->dataOnly != 1);
+	opts->dumpData = (opts->schemaOnly != 1);
+
 	opts->disable_triggers = disable_triggers;
 	opts->enable_row_security = enable_row_security;
 	opts->noDataForFailedTables = no_data_for_failed_tables;
-- 
2.47.0

v30-0004-Enable-in-place-updates-for-pg_restore_relation_.patchapplication/x-patch; name=v30-0004-Enable-in-place-updates-for-pg_restore_relation_.patchDownload
From 1adbdb01db2a0498d247fd2f3a860945a12ab228 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Mon, 4 Nov 2024 14:02:57 -0500
Subject: [PATCH v30 4/4] Enable in-place updates for
 pg_restore_relation_stats.

This matches the behavior of the ANALYZE command, and would avoid
bloating pg_class in an upgrade situation wherein
pg_restore_relation_stats would be called for nearly every relation in
the database.
---
 src/backend/statistics/relation_stats.c | 72 +++++++++++++++++++------
 1 file changed, 55 insertions(+), 17 deletions(-)

diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index ed5dea2e05..939ad56fc2 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -22,6 +22,7 @@
 #include "statistics/stat_utils.h"
 #include "utils/fmgrprotos.h"
 #include "utils/syscache.h"
+#include "utils/fmgroids.h"
 
 #define DEFAULT_RELPAGES Int32GetDatum(0)
 #define DEFAULT_RELTUPLES Float4GetDatum(-1.0)
@@ -50,13 +51,14 @@ static struct StatsArgInfo relarginfo[] =
 	[NUM_RELATION_STATS_ARGS] = {0}
 };
 
-static bool relation_statistics_update(FunctionCallInfo fcinfo, int elevel);
+static bool relation_statistics_update(FunctionCallInfo fcinfo, int elevel,
+									   bool inplace);
 
 /*
  * Internal function for modifying statistics for a relation.
  */
 static bool
-relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
+relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
 {
 	Oid			reloid;
 	Relation	crel;
@@ -68,6 +70,7 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	int			ncols = 0;
 	TupleDesc	tupdesc;
 	bool		result = true;
+	void	   *inplace_state;
 
 	stats_check_required_arg(fcinfo, relarginfo, RELATION_ARG);
 	reloid = PG_GETARG_OID(RELATION_ARG);
@@ -81,7 +84,20 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	crel = table_open(RelationRelationId, RowExclusiveLock);
 
 	tupdesc = RelationGetDescr(crel);
-	ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(reloid));
+	if (inplace)
+	{
+		ScanKeyData key[1];
+
+		ctup = NULL;
+
+		ScanKeyInit(&key[0], Anum_pg_class_oid, BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(reloid));
+		systable_inplace_update_begin(crel, ClassOidIndexId, true, NULL, 1, key,
+									  &ctup, &inplace_state);
+	}
+	else
+		ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(reloid));
+
 	if (!HeapTupleIsValid(ctup))
 	{
 		ereport(elevel,
@@ -112,8 +128,13 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 		}
 		else if (relpages != pgcform->relpages)
 		{
-			replaces[ncols] = Anum_pg_class_relpages;
-			values[ncols] = Int32GetDatum(relpages);
+			if (inplace)
+				pgcform->relpages = relpages;
+			else
+			{
+				replaces[ncols] = Anum_pg_class_relpages;
+				values[ncols] = Int32GetDatum(relpages);
+			}
 			ncols++;
 		}
 	}
@@ -131,8 +152,13 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 		}
 		else if (reltuples != pgcform->reltuples)
 		{
-			replaces[ncols] = Anum_pg_class_reltuples;
-			values[ncols] = Float4GetDatum(reltuples);
+			if (inplace)
+				pgcform->reltuples = reltuples;
+			else
+			{
+				replaces[ncols] = Anum_pg_class_reltuples;
+				values[ncols] = Float4GetDatum(reltuples);
+			}
 			ncols++;
 		}
 
@@ -151,8 +177,13 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 		}
 		else if (relallvisible != pgcform->relallvisible)
 		{
-			replaces[ncols] = Anum_pg_class_relallvisible;
-			values[ncols] = Int32GetDatum(relallvisible);
+			if (inplace)
+				pgcform->relallvisible = relallvisible;
+			else
+			{
+				replaces[ncols] = Anum_pg_class_relallvisible;
+				values[ncols] = Int32GetDatum(relallvisible);
+			}
 			ncols++;
 		}
 	}
@@ -160,13 +191,20 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	/* only update pg_class if there is a meaningful change */
 	if (ncols > 0)
 	{
-		HeapTuple	newtup;
+		if (inplace)
+			systable_inplace_update_finish(inplace_state, ctup);
+		else
+		{
+			HeapTuple	newtup;
 
-		newtup = heap_modify_tuple_by_cols(ctup, tupdesc, ncols, replaces, values,
-										   nulls);
-		CatalogTupleUpdate(crel, &newtup->t_self, newtup);
-		heap_freetuple(newtup);
+			newtup = heap_modify_tuple_by_cols(ctup, tupdesc, ncols, replaces, values,
+											nulls);
+			CatalogTupleUpdate(crel, &newtup->t_self, newtup);
+			heap_freetuple(newtup);
+		}
 	}
+	else if (inplace)
+		systable_inplace_update_cancel(inplace_state);
 
 	/* release the lock, consistent with vac_update_relstats() */
 	table_close(crel, RowExclusiveLock);
@@ -182,7 +220,7 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 Datum
 pg_set_relation_stats(PG_FUNCTION_ARGS)
 {
-	relation_statistics_update(fcinfo, ERROR);
+	relation_statistics_update(fcinfo, ERROR, false);
 	PG_RETURN_VOID();
 }
 
@@ -206,7 +244,7 @@ pg_clear_relation_stats(PG_FUNCTION_ARGS)
 	newfcinfo->args[3].value = DEFAULT_RELALLVISIBLE;
 	newfcinfo->args[3].isnull = false;
 
-	relation_statistics_update(newfcinfo, ERROR);
+	relation_statistics_update(newfcinfo, ERROR, false);
 	PG_RETURN_VOID();
 }
 
@@ -224,7 +262,7 @@ pg_restore_relation_stats(PG_FUNCTION_ARGS)
 										  relarginfo, WARNING))
 		result = false;
 
-	if (!relation_statistics_update(positional_fcinfo, WARNING))
+	if (!relation_statistics_update(positional_fcinfo, WARNING, true))
 		result = false;
 
 	PG_RETURN_BOOL(result);
-- 
2.47.0

v30-0002-Remove-schemaOnly-dataOnly-from-dump-restore-opt.patchapplication/x-patch; name=v30-0002-Remove-schemaOnly-dataOnly-from-dump-restore-opt.patchDownload
From beca392b00f53b33438f0d019286239e45631cea Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Fri, 1 Nov 2024 11:56:34 -0400
Subject: [PATCH v30 2/4] Remove schemaOnly, dataOnly from dump/restore options
 structures.

The user-facing flags still exist, but the results of those flags are
already resolved into dumpSchema and dumpData.

All logic about which objects to dump which previously used schemaOnly
and dataOnly in the negative (ex. we should dump large objects because
schemaOnly is NOT set), which would have gotten unweildly when a third
option (statisticsOnly) was added.
---
 src/bin/pg_dump/pg_backup.h          |  4 ----
 src/bin/pg_dump/pg_backup_archiver.c | 26 +++++++++++++-------------
 src/bin/pg_dump/pg_dump.c            | 26 ++++++++++++++------------
 src/bin/pg_dump/pg_restore.c         | 15 +++++++++------
 4 files changed, 36 insertions(+), 35 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 9bd3266a63..f74e848714 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -116,8 +116,6 @@ typedef struct _restoreOptions
 	int			strict_names;
 
 	const char *filename;
-	int			dataOnly;
-	int			schemaOnly;
 	int			dumpSections;
 	int			verbose;
 	int			aclsSkip;
@@ -171,8 +169,6 @@ typedef struct _dumpOptions
 	int			binary_upgrade;
 
 	/* various user-settable parameters */
-	bool		schemaOnly;
-	bool		dataOnly;
 	int			dumpSections;	/* bitmask of chosen sections */
 	bool		aclsSkip;
 	const char *lockWaitTimeout;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 8c20c263c4..32c8e588f5 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -165,8 +165,8 @@ dumpOptionsFromRestoreOptions(RestoreOptions *ropt)
 	dopt->cparams.username = ropt->cparams.username ? pg_strdup(ropt->cparams.username) : NULL;
 	dopt->cparams.promptPassword = ropt->cparams.promptPassword;
 	dopt->outputClean = ropt->dropSchema;
-	dopt->dataOnly = ropt->dataOnly;
-	dopt->schemaOnly = ropt->schemaOnly;
+	dopt->dumpData = ropt->dumpData;
+	dopt->dumpSchema = ropt->dumpSchema;
 	dopt->if_exists = ropt->if_exists;
 	dopt->column_inserts = ropt->column_inserts;
 	dopt->dumpSections = ropt->dumpSections;
@@ -419,12 +419,12 @@ RestoreArchive(Archive *AHX)
 	 * Work out if we have an implied data-only restore. This can happen if
 	 * the dump was data only or if the user has used a toc list to exclude
 	 * all of the schema data. All we do is look for schema entries - if none
-	 * are found then we set the dataOnly flag.
+	 * are found then we set the data-only flag.
 	 *
 	 * We could scan for wanted TABLE entries, but that is not the same as
-	 * dataOnly. At this stage, it seems unnecessary (6-Mar-2001).
+	 * data-only. At this stage, it seems unnecessary (6-Mar-2001).
 	 */
-	if (!ropt->dataOnly)
+	if (ropt->dumpSchema)
 	{
 		int			impliedDataOnly = 1;
 
@@ -438,7 +438,7 @@ RestoreArchive(Archive *AHX)
 		}
 		if (impliedDataOnly)
 		{
-			ropt->dataOnly = impliedDataOnly;
+			ropt->dumpSchema = false;
 			pg_log_info("implied data-only restore");
 		}
 	}
@@ -824,7 +824,7 @@ restore_toc_entry(ArchiveHandle *AH, TocEntry *te, bool is_parallel)
 	/* Dump any relevant dump warnings to stderr */
 	if (!ropt->suppressDumpWarnings && strcmp(te->desc, "WARNING") == 0)
 	{
-		if (!ropt->dataOnly && te->defn != NULL && strlen(te->defn) != 0)
+		if (ropt->dumpSchema && te->defn != NULL && strlen(te->defn) != 0)
 			pg_log_warning("warning from original dump file: %s", te->defn);
 		else if (te->copyStmt != NULL && strlen(te->copyStmt) != 0)
 			pg_log_warning("warning from original dump file: %s", te->copyStmt);
@@ -1090,7 +1090,7 @@ _disableTriggersIfNecessary(ArchiveHandle *AH, TocEntry *te)
 	RestoreOptions *ropt = AH->public.ropt;
 
 	/* This hack is only needed in a data-only restore */
-	if (!ropt->dataOnly || !ropt->disable_triggers)
+	if (ropt->dumpSchema || !ropt->disable_triggers)
 		return;
 
 	pg_log_info("disabling triggers for %s", te->tag);
@@ -1116,7 +1116,7 @@ _enableTriggersIfNecessary(ArchiveHandle *AH, TocEntry *te)
 	RestoreOptions *ropt = AH->public.ropt;
 
 	/* This hack is only needed in a data-only restore */
-	if (!ropt->dataOnly || !ropt->disable_triggers)
+	if (ropt->dumpSchema || !ropt->disable_triggers)
 		return;
 
 	pg_log_info("enabling triggers for %s", te->tag);
@@ -3148,12 +3148,12 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 		return 0;
 
 	/* Mask it if we only want schema */
-	if (ropt->schemaOnly)
+	if (!ropt->dumpData)
 	{
 		/*
-		 * The sequence_data option overrides schemaOnly for SEQUENCE SET.
+		 * The sequence_data option overrides schema-only for SEQUENCE SET.
 		 *
-		 * In binary-upgrade mode, even with schemaOnly set, we do not mask
+		 * In binary-upgrade mode, even with schema-only set, we do not mask
 		 * out large objects.  (Only large object definitions, comments and
 		 * other metadata should be generated in binary-upgrade mode, not the
 		 * actual data, but that need not concern us here.)
@@ -3172,7 +3172,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	}
 
 	/* Mask it if we only want data */
-	if (ropt->dataOnly)
+	if (!ropt->dumpSchema)
 		res = res & REQ_DATA;
 
 	return res;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 3eb8523fd7..d5d2e51be2 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -425,6 +425,8 @@ main(int argc, char **argv)
 	char	   *compression_algorithm_str = "none";
 	char	   *error_detail = NULL;
 	bool		user_compression_defined = false;
+	bool		data_only = false;
+	bool		schema_only = false;
 	DataDirSyncMethod sync_method = DATA_DIR_SYNC_METHOD_FSYNC;
 
 	static DumpOptions dopt;
@@ -541,7 +543,7 @@ main(int argc, char **argv)
 		switch (c)
 		{
 			case 'a':			/* Dump data only */
-				dopt.dataOnly = true;
+				data_only = true;
 				break;
 
 			case 'b':			/* Dump LOs */
@@ -614,7 +616,7 @@ main(int argc, char **argv)
 				break;
 
 			case 's':			/* dump schema only */
-				dopt.schemaOnly = true;
+				schema_only = true;
 				break;
 
 			case 'S':			/* Username for superuser in plain text output */
@@ -778,24 +780,24 @@ main(int argc, char **argv)
 	if (dopt.binary_upgrade)
 		dopt.sequence_data = 1;
 
-	if (dopt.dataOnly && dopt.schemaOnly)
+	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
 
-	if (dopt.schemaOnly && foreign_servers_include_patterns.head != NULL)
+	if (schema_only && foreign_servers_include_patterns.head != NULL)
 		pg_fatal("options -s/--schema-only and --include-foreign-data cannot be used together");
 
 	if (numWorkers > 1 && foreign_servers_include_patterns.head != NULL)
 		pg_fatal("option --include-foreign-data is not supported with parallel backup");
 
-	if (dopt.dataOnly && dopt.outputClean)
+	if (data_only && dopt.outputClean)
 		pg_fatal("options -c/--clean and -a/--data-only cannot be used together");
 
 	if (dopt.if_exists && !dopt.outputClean)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
 	/* set derivative flags */
-	dopt.dumpSchema = (!dopt.dataOnly);
-	dopt.dumpData = (!dopt.schemaOnly);
+	dopt.dumpSchema = (!data_only);
+	dopt.dumpData = (!schema_only);
 
 	/*
 	 * --inserts are already implied above if --column-inserts or
@@ -1093,8 +1095,8 @@ main(int argc, char **argv)
 	ropt->cparams.username = dopt.cparams.username ? pg_strdup(dopt.cparams.username) : NULL;
 	ropt->cparams.promptPassword = dopt.cparams.promptPassword;
 	ropt->dropSchema = dopt.outputClean;
-	ropt->dataOnly = dopt.dataOnly;
-	ropt->schemaOnly = dopt.schemaOnly;
+	ropt->dumpData = dopt.dumpData;
+	ropt->dumpSchema = dopt.dumpSchema;
 	ropt->if_exists = dopt.if_exists;
 	ropt->column_inserts = dopt.column_inserts;
 	ropt->dumpSections = dopt.dumpSections;
@@ -1989,7 +1991,7 @@ selectDumpableType(TypeInfo *tyinfo, Archive *fout)
  *		Mark a default ACL as to be dumped or not
  *
  * For per-schema default ACLs, dump if the schema is to be dumped.
- * Otherwise dump if we are dumping "everything".  Note that dataOnly
+ * Otherwise dump if we are dumping "everything".  Note that data-only
  * and aclsSkip are checked separately.
  */
 static void
@@ -17414,7 +17416,7 @@ collectSequences(Archive *fout)
 	if (fout->remoteVersion < 100000)
 		return;
 	else if (fout->remoteVersion < 180000 ||
-			 (fout->dopt->schemaOnly && !fout->dopt->sequence_data))
+			 (!fout->dopt->dumpData && !fout->dopt->sequence_data))
 		query = "SELECT seqrelid, format_type(seqtypid, NULL), "
 			"seqstart, seqincrement, "
 			"seqmax, seqmin, "
@@ -18281,7 +18283,7 @@ processExtensionTables(Archive *fout, ExtensionInfo extinfo[],
 	 * objects for them, ensuring their data will be dumped even though the
 	 * tables themselves won't be.
 	 *
-	 * Note that we create TableDataInfo objects even in schemaOnly mode, ie,
+	 * Note that we create TableDataInfo objects even in schema-only mode, ie,
 	 * user data in a configuration table is treated like schema data. This
 	 * seems appropriate since system data in a config table would get
 	 * reloaded by CREATE EXTENSION.  If the extension is not listed in the
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 3475168a64..f6e04e3a3b 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -77,6 +77,9 @@ main(int argc, char **argv)
 	static int	no_subscriptions = 0;
 	static int	strict_names = 0;
 
+	bool		data_only = false;
+	bool		schema_only = false;
+
 	struct option cmdopts[] = {
 		{"clean", 0, NULL, 'c'},
 		{"create", 0, NULL, 'C'},
@@ -161,7 +164,7 @@ main(int argc, char **argv)
 		switch (c)
 		{
 			case 'a':			/* Dump data only */
-				opts->dataOnly = 1;
+				data_only = true;
 				break;
 			case 'c':			/* clean (i.e., drop) schema prior to create */
 				opts->dropSchema = 1;
@@ -237,7 +240,7 @@ main(int argc, char **argv)
 				simple_string_list_append(&opts->triggerNames, optarg);
 				break;
 			case 's':			/* dump schema only */
-				opts->schemaOnly = 1;
+				schema_only = true;
 				break;
 			case 'S':			/* Superuser username */
 				if (strlen(optarg) != 0)
@@ -340,10 +343,10 @@ main(int argc, char **argv)
 		opts->useDB = 1;
 	}
 
-	if (opts->dataOnly && opts->schemaOnly)
+	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
 
-	if (opts->dataOnly && opts->dropSchema)
+	if (data_only && opts->dropSchema)
 		pg_fatal("options -c/--clean and -a/--data-only cannot be used together");
 
 	if (opts->single_txn && opts->txn_size > 0)
@@ -361,8 +364,8 @@ main(int argc, char **argv)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
 	/* set derivative flags */
-	opts->dumpSchema = (opts->dataOnly != 1);
-	opts->dumpData = (opts->schemaOnly != 1);
+	opts->dumpSchema = (!data_only);
+	opts->dumpData = (!schema_only);
 
 	opts->disable_triggers = disable_triggers;
 	opts->enable_row_security = enable_row_security;
-- 
2.47.0

v30-0003-Enable-dumping-of-table-index-stats-in-pg_dump.patchapplication/x-patch; name=v30-0003-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From 85969bb9df4049b175ada9f613efd55e71055f7e Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH v30 3/4] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will generate a statement that
calls pg_set_relation_stats(), and it will generate a series of
statements that call pg_set_attribute_stats(), one per attribute. These
statements will restore the statistics of the current system onto the
destination system.

Adds the command-line options -X / --statistics-only, which are mutually
exclusive to --schema-only and --data-only.

Statistics are not dumped when --schema-only is specified, except during
a binary upgrade.

As is the pattern with pg_dump options, staistics can be disabled using
--no-statistics.

Table statistics are dumped in the data section. This is true even if
dumping stats in a binary upgrade. Index and Materialized View
statistics are dumped in the post-data section.
---
 src/bin/pg_dump/pg_backup.h          |   4 +
 src/bin/pg_dump/pg_backup_archiver.c |   5 +
 src/bin/pg_dump/pg_dump.c            | 395 ++++++++++++++++++++++++++-
 src/bin/pg_dump/pg_dump.h            |   8 +
 src/bin/pg_dump/pg_dump_sort.c       |   5 +
 src/bin/pg_dump/pg_dumpall.c         |   5 +
 src/bin/pg_dump/pg_restore.c         |  18 +-
 src/bin/pg_dump/t/001_basic.pl       |  18 ++
 doc/src/sgml/ref/pg_dump.sgml        |  36 ++-
 doc/src/sgml/ref/pg_restore.sgml     |  31 ++-
 10 files changed, 509 insertions(+), 16 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index f74e848714..b03b69ff00 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -113,6 +113,7 @@ typedef struct _restoreOptions
 	int			no_publications;	/* Skip publication entries */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
@@ -160,6 +161,7 @@ typedef struct _restoreOptions
 	/* flags derived entirely from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } RestoreOptions;
 
 typedef struct _dumpOptions
@@ -182,6 +184,7 @@ typedef struct _dumpOptions
 	int			no_security_labels;
 	int			no_publications;
 	int			no_subscriptions;
+	int			no_statistics;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
@@ -208,6 +211,7 @@ typedef struct _dumpOptions
 	/* flags derived entirely from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } DumpOptions;
 
 /*
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 32c8e588f5..4edcb4eca5 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2958,6 +2958,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's a stats dump, maybe ignore it */
+	if (ropt->no_statistics && strcmp(te->desc, "STATISTICS") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2987,6 +2991,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index d5d2e51be2..1f1845c396 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -427,6 +427,7 @@ main(int argc, char **argv)
 	bool		user_compression_defined = false;
 	bool		data_only = false;
 	bool		schema_only = false;
+	bool		statistics_only = false;
 	DataDirSyncMethod sync_method = DATA_DIR_SYNC_METHOD_FSYNC;
 
 	static DumpOptions dopt;
@@ -464,6 +465,7 @@ main(int argc, char **argv)
 		{"encoding", required_argument, NULL, 'E'},
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
+		{"statistics-only", no_argument, NULL, 'X'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -492,6 +494,7 @@ main(int argc, char **argv)
 		{"no-comments", no_argument, &dopt.no_comments, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -537,7 +540,7 @@ main(int argc, char **argv)
 
 	InitDumpOptions(&dopt);
 
-	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:RsS:t:T:U:vwWxZ:",
+	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:PRsS:t:T:U:vwWxXZ:",
 							long_options, &optindex)) != -1)
 	{
 		switch (c)
@@ -611,6 +614,10 @@ main(int argc, char **argv)
 				dopt.cparams.pgport = pg_strdup(optarg);
 				break;
 
+			case 'X':			/* Dump statistics only */
+				statistics_only = true;
+				break;
+
 			case 'R':
 				/* no-op, still accepted for backwards compatibility */
 				break;
@@ -782,6 +789,13 @@ main(int argc, char **argv)
 
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (schema_only && statistics_only)
+		pg_fatal("options -s/--schema-only and -X/--statistics-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
+
+	if (statistics_only && dopt.no_statistics)
+		pg_fatal("options -X/--statistics-only and --no-statistics cannot be used together");
 
 	if (schema_only && foreign_servers_include_patterns.head != NULL)
 		pg_fatal("options -s/--schema-only and --include-foreign-data cannot be used together");
@@ -796,8 +810,20 @@ main(int argc, char **argv)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
 	/* set derivative flags */
-	dopt.dumpSchema = (!data_only);
-	dopt.dumpData = (!schema_only);
+	dopt.dumpData = data_only || (!schema_only && !statistics_only);
+	dopt.dumpSchema = schema_only || (!data_only && !statistics_only);
+
+	if (statistics_only)
+		/* stats are only thing wanted */
+		dopt.dumpStatistics = true;
+	else if (dopt.no_statistics)
+		/* stats specifically excluded */
+		dopt.dumpStatistics = false;
+	else if (dopt.binary_upgrade)
+		/* binary upgrade and not specifically excluded */
+		dopt.dumpStatistics = true;
+	else
+		dopt.dumpStatistics = (!data_only && !schema_only);
 
 	/*
 	 * --inserts are already implied above if --column-inserts or
@@ -1097,6 +1123,7 @@ main(int argc, char **argv)
 	ropt->dropSchema = dopt.outputClean;
 	ropt->dumpData = dopt.dumpData;
 	ropt->dumpSchema = dopt.dumpSchema;
+	ropt->dumpStatistics = dopt.dumpStatistics;
 	ropt->if_exists = dopt.if_exists;
 	ropt->column_inserts = dopt.column_inserts;
 	ropt->dumpSections = dopt.dumpSections;
@@ -1175,7 +1202,7 @@ help(const char *progname)
 	printf(_("  -?, --help                   show this help, then exit\n"));
 
 	printf(_("\nOptions controlling the output content:\n"));
-	printf(_("  -a, --data-only              dump only the data, not the schema\n"));
+	printf(_("  -a, --data-only              dump only the data, not the schema or statistics\n"));
 	printf(_("  -b, --large-objects          include large objects in dump\n"));
 	printf(_("  --blobs                      (same as --large-objects, deprecated)\n"));
 	printf(_("  -B, --no-large-objects       exclude large objects in dump\n"));
@@ -1188,11 +1215,12 @@ help(const char *progname)
 	printf(_("  -N, --exclude-schema=PATTERN do NOT dump the specified schema(s)\n"));
 	printf(_("  -O, --no-owner               skip restoration of object ownership in\n"
 			 "                               plain-text format\n"));
-	printf(_("  -s, --schema-only            dump only the schema, no data\n"));
+	printf(_("  -s, --schema-only            dump only the schema, no data or statistics\n"));
 	printf(_("  -S, --superuser=NAME         superuser user name to use in plain-text format\n"));
 	printf(_("  -t, --table=PATTERN          dump only the specified table(s)\n"));
 	printf(_("  -T, --exclude-table=PATTERN  do NOT dump the specified table(s)\n"));
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
+	printf(_("  -X, --statistics-only        dump only the statistics, not schema or data\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
@@ -1219,6 +1247,7 @@ help(const char *progname)
 	printf(_("  --no-comments                do not dump comments\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -6769,6 +6798,42 @@ getFuncs(Archive *fout)
 	destroyPQExpBuffer(query);
 }
 
+/*
+ * getRelationStatistics
+ *    register the statistics object as a dependent of the relation.
+ *
+ */
+static RelStatsInfo *
+getRelationStatistics(Archive *fout, DumpableObject *rel, char relkind)
+{
+	if ((relkind == RELKIND_RELATION) ||
+		(relkind == RELKIND_PARTITIONED_TABLE) ||
+		(relkind == RELKIND_INDEX) ||
+		(relkind == RELKIND_PARTITIONED_INDEX) ||
+		(relkind == RELKIND_MATVIEW))
+	{
+		RelStatsInfo	   *info = pg_malloc0(sizeof(RelStatsInfo));
+		DumpableObject	   *dobj = &info->dobj;
+
+		dobj->objType = DO_REL_STATS;
+		dobj->catId.tableoid = 0;
+		dobj->catId.oid = 0;
+		AssignDumpId(dobj);
+		dobj->dependencies = (DumpId *) pg_malloc(sizeof(DumpId));
+		dobj->dependencies[0] = rel->dumpId;
+		dobj->nDeps = 1;
+		dobj->allocDeps = 1;
+		dobj->components |= DUMP_COMPONENT_STATISTICS;
+		dobj->dump = rel->dump;
+		dobj->name = pg_strdup(rel->name);
+		dobj->namespace = rel->namespace;
+		info->relkind = relkind;
+
+		return info;
+	}
+	return NULL;
+}
+
 /*
  * getTables
  *	  read all the tables (no indexes) in the system catalogs,
@@ -7146,6 +7211,7 @@ getTables(Archive *fout, int *numTables)
 
 		/* Tables have data */
 		tblinfo[i].dobj.components |= DUMP_COMPONENT_DATA;
+		tblinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
 
 		/* Mark whether table has an ACL */
 		if (!PQgetisnull(res, i, i_relacl))
@@ -7194,6 +7260,8 @@ getTables(Archive *fout, int *numTables)
 				}
 			}
 		}
+		if (tblinfo[i].interesting)
+			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relkind);
 	}
 
 	if (query->len != 0)
@@ -7638,11 +7706,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 		for (int c = 0; c < numinds; c++, j++)
 		{
 			char		contype;
+			char		indexkind;
+			RelStatsInfo   *relstats;
 
 			indxinfo[j].dobj.objType = DO_INDEX;
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -7665,7 +7736,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			{
 				NULL, NULL
 			};
+
+			if (indxinfo[j].parentidx == 0)
+				indexkind = RELKIND_INDEX;
+			else
+				indexkind = RELKIND_PARTITIONED_INDEX;
+
 			contype = *(PQgetvalue(res, j, i_contype));
+			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, indexkind);
 
 			if (contype == 'p' || contype == 'u' || contype == 'x')
 			{
@@ -7699,6 +7777,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				constrinfo->separate = true;
 
 				indxinfo[j].indexconstraint = constrinfo->dobj.dumpId;
+				if (relstats != NULL)
+					addObjectDependency(&relstats->dobj, constrinfo->dobj.dumpId);
 			}
 			else
 			{
@@ -10136,6 +10216,296 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * Tabular description of the parameters to pg_restore_relation_stats()
+ * required, param_name, param_type
+ */
+static const char *rel_stats_arginfo[][3] = {
+	{"f", "relation", "regclass"},
+	{"f", "version", "integer"},
+	{"f", "relpages", "integer"},
+	{"f", "reltuples", "real"},
+	{"f", "relallvisible", "integer"},
+};
+
+/*
+ * Tabular description of the parameters to pg_restore_attribute_stats()
+ * required, param_name, param_type
+ */
+static const char *att_stats_arginfo[][3] = {
+	{"f", "relation", "regclass"},
+	{"f", "attname", "name"},
+	{"f", "inherited", "boolean"},
+	{"f", "version", "integer"},
+	{"f", "null_frac", "float4"},
+	{"f", "avg_width", "integer"},
+	{"f", "n_distinct", "float4"},
+	{"f", "most_common_vals", "text"},
+	{"f", "most_common_freqs", "float4[]"},
+	{"f", "histogram_bounds", "text"},
+	{"f", "correlation", "float4"},
+	{"f", "most_common_elems", "text"},
+	{"f", "most_common_elem_freqs", "float4[]"},
+	{"f", "elem_count_histogram", "float4[]"},
+	{"f", "range_length_histogram", "text"},
+	{"f", "range_empty_frac", "float4"},
+	{"f", "range_bounds_histogram", "text"},
+};
+
+/*
+ * getRelStatsExportQuery --
+ *
+ * Generate a query that will fetch all relation (e.g. pg_class)
+ * stats for a given relation.
+ */
+static void
+getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "current_setting('server_version_num') AS version, "
+						 "c.relpages, c.reltuples, c.relallvisible "
+						 "FROM pg_class c "
+						 "JOIN pg_namespace n "
+						 "ON n.oid = c.relnamespace "
+						 "WHERE n.nspname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND c.relname = ");
+	appendStringLiteralAH(query, relname, fout);
+}
+
+/*
+ * getAttStatsExportQuery --
+ *
+ * Generate a query that will fetch all attribute (e.g. pg_statistic)
+ * stats for a given relation.
+ */
+static void
+getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "s.attname,"
+						 "s.inherited,"
+						 "current_setting('server_version_num') AS version, "
+						 "s.null_frac,"
+						 "s.avg_width,"
+						 "s.n_distinct,"
+						 "s.most_common_vals,"
+						 "s.most_common_freqs,"
+						 "s.histogram_bounds,"
+						 "s.correlation,"
+						 "s.most_common_elems,"
+						 "s.most_common_elem_freqs,"
+						 "s.elem_count_histogram,");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 "s.range_length_histogram,"
+							 "s.range_empty_frac,"
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(query,
+							 "NULL AS range_length_histogram,"
+							 "NULL AS range_empty_frac,"
+							 "NULL AS range_bounds_histogram ");
+
+	appendPQExpBufferStr(query,
+						 "FROM pg_stats s "
+						 "JOIN pg_namespace n "
+						 "ON n.nspname = s.schemaname "
+						 "JOIN pg_class c "
+						 "ON c.relname = s.tablename "
+						 "AND c.relnamespace = n.oid "
+						 "WHERE s.schemaname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND s.tablename = ");
+	appendStringLiteralAH(query, relname, fout);
+	appendPQExpBufferStr(query, " ORDER BY s.attname, s.inherited");
+}
+
+
+/*
+ * appendNamedArgument --
+ *
+ * Convenience routine for constructing parameters of the form:
+ * 'paraname', 'value'::type
+ */
+static void
+appendNamedArgument(PQExpBuffer out, Archive *fout,
+					const char *argname, bool positional,
+					const char *argval, const char *argtype)
+{
+	appendPQExpBufferStr(out, "\t");
+
+	if (!positional)
+	{
+		appendStringLiteralAH(out, argname, fout);
+		appendPQExpBufferStr(out, ", ");
+	}
+
+	appendStringLiteralAH(out, argval, fout);
+	appendPQExpBuffer(out, "::%s", argtype);
+}
+
+/*
+ * appendRelStatsImport --
+ *
+ * Append a formatted pg_restore_relation_stats statement.
+ */
+static void
+appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	const char *sep = "";
+
+	if (PQntuples(res) == 0)
+		return;
+
+	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
+
+	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
+	{
+		bool		positional = (rel_stats_arginfo[argno][0][0] == 't');
+		const char *argname = rel_stats_arginfo[argno][1];
+		const char *argtype = rel_stats_arginfo[argno][2];
+		int			fieldno = PQfnumber(res, argname);
+
+		if (fieldno < 0)
+			pg_fatal("relation stats export query missing field '%s'",
+					 argname);
+
+		if (PQgetisnull(res, 0, fieldno))
+		{
+			if (!positional)
+				pg_fatal("relation stats export query unexpected NULL in '%s'",
+						 argname);
+			else
+				continue;
+		}
+
+		appendPQExpBufferStr(out, sep);
+		appendNamedArgument(out, fout, argname, positional,
+							PQgetvalue(res, 0, fieldno), argtype);
+
+		sep = ",\n";
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+/*
+ * appendAttStatsImport --
+ *
+ * Append a series of formatted pg_restore_attribute_stats statements.
+ */
+static void
+appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	{
+		const char *sep = "";
+
+		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+		for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
+		{
+			bool		positional = (att_stats_arginfo[argno][0][0] == 't');
+			const char *argname = att_stats_arginfo[argno][1];
+			const char *argtype = att_stats_arginfo[argno][2];
+			int			fieldno = PQfnumber(res, argname);
+
+			if (fieldno < 0)
+				pg_fatal("attribute stats export query missing field '%s'",
+						 argname);
+
+			if (PQgetisnull(res, rownum, fieldno))
+			{
+				if (positional)
+					pg_fatal("attribute stats export query unexpected NULL in '%s'",
+							 argname);
+				else
+					continue;
+			}
+
+			appendPQExpBufferStr(out, sep);
+			appendNamedArgument(out, fout, argname, positional,
+								PQgetvalue(res, rownum, fieldno), argtype);
+			sep = ",\n";
+		}
+		appendPQExpBufferStr(out, "\n);\n");
+	}
+}
+
+/*
+ * get statistics dump section, which depends on the parent object type
+ *
+ * objects created in SECTION_PRE_DATA have stats in SECTION_DATA
+ * objects created in SECTION_POST_DATA have stats in SECTION_POST_DATA
+ */
+static teSection
+statisticsDumpSection(const RelStatsInfo *rsinfo)
+{
+
+	if ((rsinfo->relkind == RELKIND_MATVIEW) ||
+		(rsinfo->relkind == RELKIND_INDEX) ||
+		(rsinfo->relkind == RELKIND_PARTITIONED_INDEX))
+			return SECTION_POST_DATA;
+
+	return SECTION_DATA;
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	PGresult	   *res;
+	PQExpBuffer		query;
+	PQExpBuffer		out;
+	PQExpBuffer		tag;
+	DumpableObject *dobj = (DumpableObject *) &rsinfo->dobj;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", "STATISTICS DATA", fmtId(dobj->name));
+
+	query = createPQExpBuffer();
+	out = createPQExpBuffer();
+
+	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendRelStatsImport(out, fout, res);
+	PQclear(res);
+
+	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendAttStatsImport(out, fout, res);
+	PQclear(res);
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATISTICS DATA",
+							  .section = statisticsDumpSection(rsinfo),
+							  .createStmt = out->data,
+							  .deps = dobj->dependencies,
+							  .nDeps = dobj->nDeps));
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(out);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -10584,6 +10954,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_SUBSCRIPTION_REL:
 			dumpSubscriptionTable(fout, (const SubRelInfo *) dobj);
 			break;
+		case DO_REL_STATS:
+			dumpRelationStats(fout, (const RelStatsInfo *) dobj);
+			break;
 		case DO_PRE_DATA_BOUNDARY:
 		case DO_POST_DATA_BOUNDARY:
 			/* never dumped, nothing to do */
@@ -16921,6 +17294,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 		free(indstatvalsarray);
 	}
 
+	/* Comments and stats share same .dep */
+
 	/* Dump Index Comments */
 	if (indxinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
 		dumpComment(fout, "INDEX", qindxname,
@@ -18708,6 +19083,16 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 				/* must come after the pre-data boundary */
 				addObjectDependency(dobj, preDataBound->dumpId);
 				break;
+			case DO_REL_STATS:
+				/* stats section varies by parent object type, DATA or POST */
+				if (statisticsDumpSection((RelStatsInfo *) dobj) == SECTION_DATA)
+				{
+					addObjectDependency(dobj, preDataBound->dumpId);
+					addObjectDependency(postDataBound, dobj->dumpId);
+				}
+				else
+					addObjectDependency(dobj, postDataBound->dumpId);
+				break;
 		}
 	}
 }
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 9f907ed5ad..5039ae6e2b 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -84,6 +84,7 @@ typedef enum
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION,
 	DO_SUBSCRIPTION_REL,		/* see note for SubRelInfo */
+	DO_REL_STATS,
 } DumpableObjectType;
 
 /*
@@ -101,6 +102,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
@@ -417,6 +419,12 @@ typedef struct _indexAttachInfo
 	IndxInfo   *partitionIdx;	/* link to index on partition */
 } IndexAttachInfo;
 
+typedef struct _relStatsInfo
+{
+	DumpableObject	dobj;
+	char			relkind;		/* 'r', 'v', 'c', etc */
+} RelStatsInfo;
+
 typedef struct _statsExtInfo
 {
 	DumpableObject dobj;
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 675bbf1233..e0afe48e0e 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -1501,6 +1501,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "POST-DATA BOUNDARY  (ID %d)",
 					 obj->dumpId);
 			return;
+		case DO_REL_STATS:
+			snprintf(buf, bufsize,
+					 "RELATION STATISTICS FOR %s  (ID %d OID %u)",
+					 obj->name, obj->dumpId, obj->catId.oid);
+			return;
 	}
 	/* shouldn't get here */
 	snprintf(buf, bufsize,
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index e3ad8fb295..1799a03ff1 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -103,6 +103,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -172,6 +173,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -451,6 +453,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -666,6 +670,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index f6e04e3a3b..41857066c4 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -64,6 +64,7 @@ main(int argc, char **argv)
 	int			numWorkers = 1;
 	Archive    *AH;
 	char	   *inputFileSpec;
+	bool		statistics_only = false;
 	static int	disable_triggers = 0;
 	static int	enable_row_security = 0;
 	static int	if_exists = 0;
@@ -75,6 +76,7 @@ main(int argc, char **argv)
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
 	static int	no_subscriptions = 0;
+	static int  no_statistics = 0;
 	static int	strict_names = 0;
 
 	bool		data_only = false;
@@ -110,6 +112,7 @@ main(int argc, char **argv)
 		{"username", 1, NULL, 'U'},
 		{"verbose", 0, NULL, 'v'},
 		{"single-transaction", 0, NULL, '1'},
+		{"statistics-only", no_argument, NULL, 'P'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -130,6 +133,7 @@ main(int argc, char **argv)
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
+		{"no-statistics", no_argument, &no_statistics, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -273,6 +277,10 @@ main(int argc, char **argv)
 				opts->aclsSkip = 1;
 				break;
 
+			case 'X':			/* Restore statistics only */
+				statistics_only = true;
+				break;
+
 			case '1':			/* Restore data in a single transaction */
 				opts->single_txn = true;
 				opts->exit_on_error = true;
@@ -345,6 +353,10 @@ main(int argc, char **argv)
 
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
+	if (schema_only && statistics_only)
+		pg_fatal("options -s/--schema-only and -X/--statistics-only cannot be used together");
 
 	if (data_only && opts->dropSchema)
 		pg_fatal("options -c/--clean and -a/--data-only cannot be used together");
@@ -364,8 +376,9 @@ main(int argc, char **argv)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
 	/* set derivative flags */
-	opts->dumpSchema = (!data_only);
-	opts->dumpData = (!schema_only);
+	opts->dumpSchema = (!data_only && !statistics_only);
+	opts->dumpData = (!schema_only && !statistics_only);
+	opts->dumpStatistics = (!no_statistics && !data_only && !schema_only);
 
 	opts->disable_triggers = disable_triggers;
 	opts->enable_row_security = enable_row_security;
@@ -377,6 +390,7 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
diff --git a/src/bin/pg_dump/t/001_basic.pl b/src/bin/pg_dump/t/001_basic.pl
index b9d13a0e1d..76ebf15f55 100644
--- a/src/bin/pg_dump/t/001_basic.pl
+++ b/src/bin/pg_dump/t/001_basic.pl
@@ -50,12 +50,30 @@ command_fails_like(
 	'pg_dump: options -s/--schema-only and -a/--data-only cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '-s', '-X' ],
+	qr/\Qpg_dump: error: options -s\/--schema-only and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: error: options -s/--schema-only and -X/--statistics-only cannot be used together'
+);
+
+command_fails_like(
+	[ 'pg_dump', '-a', '-X' ],
+	qr/\Qpg_dump: error: options -a\/--data-only and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: error: options -a/--data-only and -X/--statistics-only cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-s', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: options -s\/--schema-only and --include-foreign-data cannot be used together\E/,
 	'pg_dump: options -s/--schema-only and --include-foreign-data cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '--statistics-only', '--no-statistics' ],
+	qr/\Qpg_dump: error: options -X\/--statistics-only and --no-statistics cannot be used together\E/,
+	'pg_dump: options -X\/--statistics-only and --no-statistics cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-j2', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: option --include-foreign-data is not supported with parallel backup\E/,
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index ffc29b04fb..99f25e40c0 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -123,7 +123,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Dump only the data, not the schema (data definitions).
+        Dump only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are dumped.
        </para>
 
@@ -141,8 +141,9 @@ PostgreSQL documentation
       <listitem>
        <para>
         Include large objects in the dump.  This is the default behavior
-        except when <option>--schema</option>, <option>--table</option>, or
-        <option>--schema-only</option> is specified.  The <option>-b</option>
+        except when <option>--schema</option>, <option>--table</option>,
+        <option>--schema-only</option>, or
+        <option>--statistics-only</option> is specified.  The <option>-b</option>
         switch is therefore only useful to add large objects to dumps
         where a specific schema or table has been requested.  Note that
         large objects are considered data and therefore will be included when
@@ -516,10 +517,11 @@ PostgreSQL documentation
       <term><option>--schema-only</option></term>
       <listitem>
        <para>
-        Dump only the object definitions (schema), not data.
+        Dump only the object definitions (schema), not data or statistics.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive to <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
         <option>--section=pre-data --section=post-data</option>.
@@ -652,6 +654,18 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Dump only the statistics, not the schema (data definitions) or data.
+        Statistics for tables, materialized views, and indexes are dumped.
+       </para>
+
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-Z <replaceable class="parameter">level</replaceable></option></term>
       <term><option>-Z <replaceable class="parameter">method</replaceable></option>[:<replaceable>detail</replaceable>]</term>
@@ -833,7 +847,8 @@ PostgreSQL documentation
         though you do not need the data in it.
        </para>
        <para>
-        To exclude data for all tables in the database, see <option>--schema-only</option>.
+        To exclude data for all tables in the database, see <option>--schema-only</option>
+        or <option>--statistics-only</option>.
        </para>
       </listitem>
      </varlistentry>
@@ -1098,6 +1113,15 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index b8b27e1719..ff1441a243 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -94,7 +94,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Restore only the data, not the schema (data definitions).
+        Restore only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are restored,
         if present in the archive.
        </para>
@@ -483,10 +483,11 @@ PostgreSQL documentation
         to the extent that schema entries are present in the archive.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive of <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
-        <option>--section=pre-data --section=post-data</option>.
+        <option>--section=pre-data --section=post-data --no-statistics</option>.
        </para>
        <para>
         (Do not confuse this with the <option>--schema</option> option, which
@@ -599,6 +600,20 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Restore only the statistics, not schema or data.
+       </para>
+       <para>
+        (Do not confuse this with the <option>--schema</option> option, which
+        uses the word <quote>schema</quote> in a different meaning.)
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-1</option></term>
       <term><option>--single-transaction</option></term>
@@ -723,6 +738,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore statistics, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
-- 
2.47.0

v30-0005-Enable-pg_clear_relation_stats-to-handle-differe.patchapplication/x-patch; name=v30-0005-Enable-pg_clear_relation_stats-to-handle-differe.patchDownload
From 7f68a7e4d84ee5f1376eb4d8681773e4eaf305e3 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Mon, 4 Nov 2024 16:21:39 -0500
Subject: [PATCH v30 5/5] Enable pg_clear_relation_stats to handle different
 default relpages.

If it comes to pass that relpages has the default of -1.0 for
partitioned tables (and indexes), then this patch will handle that.
---
 src/backend/statistics/relation_stats.c | 47 +++++++++++++------------
 1 file changed, 24 insertions(+), 23 deletions(-)

diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index 939ad56fc2..1aab63f5d8 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -19,15 +19,12 @@
 
 #include "access/heapam.h"
 #include "catalog/indexing.h"
+#include "catalog/pg_class_d.h"
 #include "statistics/stat_utils.h"
 #include "utils/fmgrprotos.h"
 #include "utils/syscache.h"
 #include "utils/fmgroids.h"
 
-#define DEFAULT_RELPAGES Int32GetDatum(0)
-#define DEFAULT_RELTUPLES Float4GetDatum(-1.0)
-#define DEFAULT_RELALLVISIBLE Int32GetDatum(0)
-
 /*
  * Positional argument numbers, names, and types for
  * relation_statistics_update().
@@ -51,14 +48,11 @@ static struct StatsArgInfo relarginfo[] =
 	[NUM_RELATION_STATS_ARGS] = {0}
 };
 
-static bool relation_statistics_update(FunctionCallInfo fcinfo, int elevel,
-									   bool inplace);
-
 /*
  * Internal function for modifying statistics for a relation.
  */
 static bool
-relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
+relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace, bool clear)
 {
 	Oid			reloid;
 	Relation	crel;
@@ -110,9 +104,19 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
 	pgcform = (Form_pg_class) GETSTRUCT(ctup);
 
 	/* relpages */
-	if (!PG_ARGISNULL(RELPAGES_ARG))
+	if (!PG_ARGISNULL(RELPAGES_ARG) || clear)
 	{
-		int32		relpages = PG_GETARG_INT32(RELPAGES_ARG);
+		int32		relpages;
+
+		if (clear)
+			/* relpages default varies by relkind */
+			if ((crel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) ||
+				(crel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+				relpages = -1;
+			else
+				relpages = 0;
+		else
+			relpages = PG_GETARG_INT32(RELPAGES_ARG);
 
 		/*
 		 * Partitioned tables may have relpages=-1. Note: for relations with
@@ -139,9 +143,9 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
 		}
 	}
 
-	if (!PG_ARGISNULL(RELTUPLES_ARG))
+	if (!PG_ARGISNULL(RELTUPLES_ARG) || clear)
 	{
-		float		reltuples = PG_GETARG_FLOAT4(RELTUPLES_ARG);
+		float		reltuples = (clear) ? -1.0 : PG_GETARG_FLOAT4(RELTUPLES_ARG);
 
 		if (reltuples < -1.0)
 		{
@@ -164,9 +168,9 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
 
 	}
 
-	if (!PG_ARGISNULL(RELALLVISIBLE_ARG))
+	if (!PG_ARGISNULL(RELALLVISIBLE_ARG) || clear)
 	{
-		int32		relallvisible = PG_GETARG_INT32(RELALLVISIBLE_ARG);
+		int32		relallvisible = (clear) ? 0 : PG_GETARG_INT32(RELALLVISIBLE_ARG);
 
 		if (relallvisible < 0)
 		{
@@ -220,7 +224,7 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
 Datum
 pg_set_relation_stats(PG_FUNCTION_ARGS)
 {
-	relation_statistics_update(fcinfo, ERROR, false);
+	relation_statistics_update(fcinfo, ERROR, false, false);
 	PG_RETURN_VOID();
 }
 
@@ -237,14 +241,11 @@ pg_clear_relation_stats(PG_FUNCTION_ARGS)
 
 	newfcinfo->args[0].value = PG_GETARG_OID(0);
 	newfcinfo->args[0].isnull = PG_ARGISNULL(0);
-	newfcinfo->args[1].value = DEFAULT_RELPAGES;
-	newfcinfo->args[1].isnull = false;
-	newfcinfo->args[2].value = DEFAULT_RELTUPLES;
-	newfcinfo->args[2].isnull = false;
-	newfcinfo->args[3].value = DEFAULT_RELALLVISIBLE;
-	newfcinfo->args[3].isnull = false;
+	newfcinfo->args[1].isnull = true;
+	newfcinfo->args[2].isnull = true;
+	newfcinfo->args[3].isnull = true;
 
-	relation_statistics_update(newfcinfo, ERROR, false);
+	relation_statistics_update(newfcinfo, ERROR, false, true);
 	PG_RETURN_VOID();
 }
 
@@ -262,7 +263,7 @@ pg_restore_relation_stats(PG_FUNCTION_ARGS)
 										  relarginfo, WARNING))
 		result = false;
 
-	if (!relation_statistics_update(positional_fcinfo, WARNING, true))
+	if (!relation_statistics_update(positional_fcinfo, WARNING, true, false))
 		result = false;
 
 	PG_RETURN_BOOL(result);
-- 
2.47.0

#219Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#218)
11 attachment(s)
Re: Statistics Import and Export

WHAT IS NOT DONE - EXTENDED STATISTICS

It is a general consensus in the community that "nobody uses extended
statistics", though I've had difficulty getting actual figures to back this
up, even from my own employer. Surveying several vendors at PgConf.EU, the
highest estimate was that at most 1% of their customers used extended
statistics, though more probably should. This reinforces my belief that a
feature that would eliminate a major pain point in upgrades for 99% of
customers shouldn't be held back by the fact that the other 1% only have a
reduced hassle.

However, having relation and attribute statistics carry over on major
version upgrades presents a slight problem: running vacuumdb
--analyze-in-stages after such an upgrade is completely unnecessary for
those without extended statistics, and would actually result in _worse_
statistics for the database until the last stage is complete. Granted,
we've had great difficulty getting users to know that vacuumdb is a thing
that should be run, but word has slowly spread through our own
documentation and those "This one simple trick will make your postgres go
fast post-upgrade" blog posts. Those posts will continue to lurk in search
results long after this feature goes into release, and it would be a rude
surprise to users to find out that the extra work they put in to learn
about a feature that helped their upgrade in 17 was suddenly detrimental
(albeit temporarily) in 18. We should never punish people for only being a
little-bit current in their knowledge. Moreover, this surprise would
persist even after we add extended statistics import function functionality.

I presented this problem to several people at PgConf.EU, and the consensus
least-bad solution was that vacuumdb should filter out tables that are not
missing any statistics when using options --analyze, --analyze-only, and
--analyze-in-stages, with an additional flag for now called --force-analyze
to restore the un-filtered functionality. This gives the outcome tree:

1. Users who do not have extended statistics and do not use (or not even
know about) vacuumdb will be blissfully unaware, and will get better
post-upgrade performance.
2. Users who do not have extended statistics but use vacuumdb
--analyze-in-stages will be pleasantly surprised that the vacuumdb run is
almost a no-op, and completes quickly. Those who are surprised by this and
re-run vacuumdb --analyze-in-stages will get another no-op.
3. Users who have extended statistics and use vacuumdb --analyze-in-stages
will get a quicker vacuumdb run, as only the tables with extended stats
will pass the filter. Subsequent re-runs of vacuumdb --analyze-in-stages
would be the no-op.
4. Users who have extended statistics and don't use vacuumdb will still
get better performance than they would have without any stats imported.

In case anyone is curious, I'm defining "missing stats" as a table/matview
with any of the following:

1. A table with an attribute that lacks a corresponding pg_statistic row.
2. A table with an index with an expression attribute that lacks a
corresponding pg_statistic row (non-expression attributes just borrow the
pg_statistic row from the table's attribute).
3. A table with at least one extended statistic that does not have a
corresponding pg_statistic_ext_data row.

Note that none of these criteria are concerned with the substance of the
statistics (ex. pg_statistic row should have mcv stats but does not),
merely their row-existence.

Some rejected alternative solutions were:

1. Adding a new option --analyze-missing-stats. While simple, few people
would learn about it, knowledge of it would be drowned out by the
aforementioned sea of existing blog posts.
2. Adding --analyze-missing-stats and making --analyze-in-stages fail with
an error message educating the user about --analyze-missing-stats. Users
might not see the error, existing tooling wouldn't be able to act on the
error, and there are legitimate non-upgrade uses of --analyze-in-stages.

MAIN CONCERN GOING FORWARD

This change to vacuumdb will require some reworking of the
vacuum_one_database() function so that the list of tables analyzed is
preserved across the stages, as subsequent stages runs won't be able to
detect which tables were previously missing stats.

Here's a rebased patchset, with the changes to vacuum broken down into baby
steps to make things easier on a reviewer.

0001-0005 same as before, rebased.
0006-0009 structural changes to vacuumdb to make the new feature simpler.
0010 add test issues_sql_unlike, this was needed because we didn't have a
way to determine that a given line _wasn't_ printed.
0011 add --force-analyze to vacuumdb and filter out tables with existing
statistics otherwise.

Attachments:

v31-0001-Add-derivative-flags-dumpSchema-dumpData.patchtext/x-patch; charset=US-ASCII; name=v31-0001-Add-derivative-flags-dumpSchema-dumpData.patchDownload
From 4fe5cce76ef23fef9fcd5543a437bbdb99f052c2 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 4 May 2024 04:52:38 -0400
Subject: [PATCH v31 01/11] Add derivative flags dumpSchema, dumpData.

User-set flags --schema-only and --data-only are often consulted by
various operations to determine if they should be skipped or not. While
this logic works when there are only two mutually-exclusive -only
options, it will get progressively more confusing when more are added.

In anticipation of this, create the flags dumpSchema and dumpData which
are derivative of the existing options schemaOnly and dataOnly. This
allows us to restate current skip-this-section tests in terms of what is
enabled, rather than checking if the other -only mode is turned off.
---
 src/bin/pg_dump/pg_backup.h  |   8 ++
 src/bin/pg_dump/pg_dump.c    | 186 ++++++++++++++++++-----------------
 src/bin/pg_dump/pg_restore.c |   4 +
 3 files changed, 107 insertions(+), 91 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 68ae2970ad..9bd3266a63 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -158,6 +158,10 @@ typedef struct _restoreOptions
 	int			enable_row_security;
 	int			sequence_data;	/* dump sequence data even in schema-only mode */
 	int			binary_upgrade;
+
+	/* flags derived entirely from the user-settable flags */
+	bool		dumpSchema;
+	bool		dumpData;
 } RestoreOptions;
 
 typedef struct _dumpOptions
@@ -204,6 +208,10 @@ typedef struct _dumpOptions
 
 	int			sequence_data;	/* dump sequence data even in schema-only mode */
 	int			do_nothing;
+
+	/* flags derived entirely from the user-settable flags */
+	bool		dumpSchema;
+	bool		dumpData;
 } DumpOptions;
 
 /*
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a8c141b689..7ce1c7a9ce 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -795,6 +795,10 @@ main(int argc, char **argv)
 	if (dopt.if_exists && !dopt.outputClean)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
+	/* set derivative flags */
+	dopt.dumpSchema = (!dopt.dataOnly);
+	dopt.dumpData = (!dopt.schemaOnly);
+
 	/*
 	 * --inserts are already implied above if --column-inserts or
 	 * --rows-per-insert were specified.
@@ -977,7 +981,7 @@ main(int argc, char **argv)
 	 * -s means "schema only" and LOs are data, not schema, so we never
 	 * include LOs when -s is used.
 	 */
-	if (dopt.include_everything && !dopt.schemaOnly && !dopt.dontOutputLOs)
+	if (dopt.include_everything && dopt.dumpData && !dopt.dontOutputLOs)
 		dopt.outputLOs = true;
 
 	/*
@@ -991,15 +995,15 @@ main(int argc, char **argv)
 	 */
 	tblinfo = getSchemaData(fout, &numTables);
 
-	if (!dopt.schemaOnly)
+	if (dopt.dumpData)
 	{
 		getTableData(&dopt, tblinfo, numTables, 0);
 		buildMatViewRefreshDependencies(fout);
-		if (dopt.dataOnly)
+		if (!dopt.dumpSchema)
 			getTableDataFKConstraints();
 	}
 
-	if (dopt.schemaOnly && dopt.sequence_data)
+	if (!dopt.dumpData && dopt.sequence_data)
 		getTableData(&dopt, tblinfo, numTables, RELKIND_SEQUENCE);
 
 	/*
@@ -4161,8 +4165,8 @@ dumpPolicy(Archive *fout, const PolicyInfo *polinfo)
 	const char *cmd;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
@@ -4383,8 +4387,8 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 	char	   *qpubname;
 	bool		first = true;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	delq = createPQExpBuffer();
@@ -4701,8 +4705,8 @@ dumpPublicationNamespace(Archive *fout, const PublicationSchemaInfo *pubsinfo)
 	PQExpBuffer query;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	tag = psprintf("%s %s", pubinfo->dobj.name, schemainfo->dobj.name);
@@ -4744,8 +4748,8 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 	PQExpBuffer query;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	tag = psprintf("%s %s", pubinfo->dobj.name, tbinfo->dobj.name);
@@ -5130,8 +5134,8 @@ dumpSubscriptionTable(Archive *fout, const SubRelInfo *subrinfo)
 	PQExpBuffer query;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	Assert(fout->dopt->binary_upgrade && fout->remoteVersion >= 170000);
@@ -5204,8 +5208,8 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo)
 	int			i;
 	char		two_phase_disabled[] = {LOGICALREP_TWOPHASE_STATE_DISABLED, '\0'};
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	delq = createPQExpBuffer();
@@ -7356,8 +7360,8 @@ getPartitioningInfo(Archive *fout)
 	/* hash partitioning didn't exist before v11 */
 	if (fout->remoteVersion < 110000)
 		return;
-	/* needn't bother if schema-only dump */
-	if (fout->dopt->schemaOnly)
+	/* needn't bother if not dumping data */
+	if (!fout->dopt->dumpData)
 		return;
 
 	query = createPQExpBuffer();
@@ -9055,7 +9059,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * Now get info about column defaults.  This is skipped for a data-only
 	 * dump, as it is only needed for table schemas.
 	 */
-	if (!dopt->dataOnly && tbloids->len > 1)
+	if (dopt->dumpSchema && tbloids->len > 1)
 	{
 		AttrDefInfo *attrdefs;
 		int			numDefaults;
@@ -9185,7 +9189,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * Get info about table CHECK constraints.  This is skipped for a
 	 * data-only dump, as it is only needed for table schemas.
 	 */
-	if (!dopt->dataOnly && checkoids->len > 2)
+	if (dopt->dumpSchema && checkoids->len > 2)
 	{
 		ConstraintInfo *constrs;
 		int			numConstrs;
@@ -10199,13 +10203,13 @@ dumpCommentExtended(Archive *fout, const char *type,
 	/* Comments are schema not data ... except LO comments are data */
 	if (strcmp(type, "LARGE OBJECT") != 0)
 	{
-		if (dopt->dataOnly)
+		if (!dopt->dumpSchema)
 			return;
 	}
 	else
 	{
 		/* We do dump LO comments in binary-upgrade mode */
-		if (dopt->schemaOnly && !dopt->binary_upgrade)
+		if (!dopt->dumpData && !dopt->binary_upgrade)
 			return;
 	}
 
@@ -10312,7 +10316,7 @@ dumpTableComment(Archive *fout, const TableInfo *tbinfo,
 		return;
 
 	/* Comments are SCHEMA not data */
-	if (dopt->dataOnly)
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Search for comments associated with relation, using table */
@@ -10758,8 +10762,8 @@ dumpNamespace(Archive *fout, const NamespaceInfo *nspinfo)
 	PQExpBuffer delq;
 	char	   *qnspname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -10835,8 +10839,8 @@ dumpExtension(Archive *fout, const ExtensionInfo *extinfo)
 	PQExpBuffer delq;
 	char	   *qextname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -10960,8 +10964,8 @@ dumpType(Archive *fout, const TypeInfo *tyinfo)
 {
 	DumpOptions *dopt = fout->dopt;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Dump out in proper style */
@@ -12071,8 +12075,8 @@ dumpShellType(Archive *fout, const ShellTypeInfo *stinfo)
 	DumpOptions *dopt = fout->dopt;
 	PQExpBuffer q;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -12123,8 +12127,8 @@ dumpProcLang(Archive *fout, const ProcLangInfo *plang)
 	FuncInfo   *inlineInfo = NULL;
 	FuncInfo   *validatorInfo = NULL;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
@@ -12331,8 +12335,8 @@ dumpFunc(Archive *fout, const FuncInfo *finfo)
 	int			nconfigitems = 0;
 	const char *keyword;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -12723,8 +12727,8 @@ dumpCast(Archive *fout, const CastInfo *cast)
 	const char *sourceType;
 	const char *targetType;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Cannot dump if we don't have the cast function's info */
@@ -12829,8 +12833,8 @@ dumpTransform(Archive *fout, const TransformInfo *transform)
 	char	   *lanname;
 	const char *transformType;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Cannot dump if we don't have the transform functions' info */
@@ -12978,8 +12982,8 @@ dumpOpr(Archive *fout, const OprInfo *oprinfo)
 	char	   *oprregproc;
 	char	   *oprref;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
@@ -13265,8 +13269,8 @@ dumpAccessMethod(Archive *fout, const AccessMethodInfo *aminfo)
 	PQExpBuffer delq;
 	char	   *qamname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -13368,8 +13372,8 @@ dumpOpclass(Archive *fout, const OpclassInfo *opcinfo)
 	bool		needComma;
 	int			i;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -13639,8 +13643,8 @@ dumpOpfamily(Archive *fout, const OpfamilyInfo *opfinfo)
 	bool		needComma;
 	int			i;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -13846,8 +13850,8 @@ dumpCollation(Archive *fout, const CollInfo *collinfo)
 	const char *colllocale;
 	const char *collicurules;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -14100,8 +14104,8 @@ dumpConversion(Archive *fout, const ConvInfo *convinfo)
 	const char *conproc;
 	bool		condefault;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -14248,8 +14252,8 @@ dumpAgg(Archive *fout, const AggInfo *agginfo)
 	const char *proparallel;
 	char		defaultfinalmodify;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -14578,8 +14582,8 @@ dumpTSParser(Archive *fout, const TSParserInfo *prsinfo)
 	PQExpBuffer delq;
 	char	   *qprsname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14646,8 +14650,8 @@ dumpTSDictionary(Archive *fout, const TSDictInfo *dictinfo)
 	char	   *nspname;
 	char	   *tmplname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14722,8 +14726,8 @@ dumpTSTemplate(Archive *fout, const TSTemplateInfo *tmplinfo)
 	PQExpBuffer delq;
 	char	   *qtmplname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14788,8 +14792,8 @@ dumpTSConfig(Archive *fout, const TSConfigInfo *cfginfo)
 	int			i_tokenname;
 	int			i_dictname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14900,8 +14904,8 @@ dumpForeignDataWrapper(Archive *fout, const FdwInfo *fdwinfo)
 	PQExpBuffer delq;
 	char	   *qfdwname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14973,8 +14977,8 @@ dumpForeignServer(Archive *fout, const ForeignServerInfo *srvinfo)
 	char	   *qsrvname;
 	char	   *fdwname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -15164,8 +15168,8 @@ dumpDefaultACL(Archive *fout, const DefaultACLInfo *daclinfo)
 	PQExpBuffer tag;
 	const char *type;
 
-	/* Do nothing in data-only dump, or if we're skipping ACLs */
-	if (dopt->dataOnly || dopt->aclsSkip)
+	/* Do nothing if not dumping schema, or if we're skipping ACLs */
+	if (!dopt->dumpSchema || dopt->aclsSkip)
 		return;
 
 	q = createPQExpBuffer();
@@ -15265,7 +15269,7 @@ dumpACL(Archive *fout, DumpId objDumpId, DumpId altDumpId,
 		return InvalidDumpId;
 
 	/* --data-only skips ACLs *except* large object ACLs */
-	if (dopt->dataOnly && strcmp(type, "LARGE OBJECT") != 0)
+	if (!dopt->dumpSchema && strcmp(type, "LARGE OBJECT") != 0)
 		return InvalidDumpId;
 
 	sql = createPQExpBuffer();
@@ -15394,13 +15398,13 @@ dumpSecLabel(Archive *fout, const char *type, const char *name,
 	 */
 	if (strcmp(type, "LARGE OBJECT") != 0)
 	{
-		if (dopt->dataOnly)
+		if (!dopt->dumpSchema)
 			return;
 	}
 	else
 	{
 		/* We do dump large object security labels in binary-upgrade mode */
-		if (dopt->schemaOnly && !dopt->binary_upgrade)
+		if (!dopt->dumpData && !dopt->binary_upgrade)
 			return;
 	}
 
@@ -15468,7 +15472,7 @@ dumpTableSecLabel(Archive *fout, const TableInfo *tbinfo, const char *reltypenam
 		return;
 
 	/* SecLabel are SCHEMA not data */
-	if (dopt->dataOnly)
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Search for comments associated with relation, using table */
@@ -15707,8 +15711,8 @@ dumpTable(Archive *fout, const TableInfo *tbinfo)
 	DumpId		tableAclDumpId = InvalidDumpId;
 	char	   *namecopy;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	if (tbinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
@@ -16895,8 +16899,8 @@ dumpTableAttach(Archive *fout, const TableAttachInfo *attachinfo)
 	PGresult   *res;
 	char	   *partbound;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -16967,8 +16971,8 @@ dumpAttrDef(Archive *fout, const AttrDefInfo *adinfo)
 	char	   *tag;
 	char	   *foreign;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Skip if not "separate"; it was dumped in the table's definition */
@@ -17056,8 +17060,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 	char	   *qindxname;
 	char	   *qqindxname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -17189,8 +17193,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 static void
 dumpIndexAttach(Archive *fout, const IndexAttachInfo *attachinfo)
 {
-	/* Do nothing in data-only dump */
-	if (fout->dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!fout->dopt->dumpSchema)
 		return;
 
 	if (attachinfo->partitionIdx->dobj.dump & DUMP_COMPONENT_DEFINITION)
@@ -17236,8 +17240,8 @@ dumpStatisticsExt(Archive *fout, const StatsExtInfo *statsextinfo)
 	PGresult   *res;
 	char	   *stxdef;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -17312,8 +17316,8 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
 	char	   *tag = NULL;
 	char	   *foreign;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -18049,8 +18053,8 @@ dumpTrigger(Archive *fout, const TriggerInfo *tginfo)
 	char	   *qtabname;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -18171,8 +18175,8 @@ dumpEventTrigger(Archive *fout, const EventTriggerInfo *evtinfo)
 	PQExpBuffer delqry;
 	char	   *qevtname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -18262,8 +18266,8 @@ dumpRule(Archive *fout, const RuleInfo *rinfo)
 	PGresult   *res;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index f2c1020d05..d2db23c75a 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -359,6 +359,10 @@ main(int argc, char **argv)
 	if (opts->single_txn && numWorkers > 1)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
+	/* set derivative flags */
+	opts->dumpSchema = (opts->dataOnly != 1);
+	opts->dumpData = (opts->schemaOnly != 1);
+
 	opts->disable_triggers = disable_triggers;
 	opts->enable_row_security = enable_row_security;
 	opts->noDataForFailedTables = no_data_for_failed_tables;
-- 
2.47.0

v31-0009-preserve-catalog-lists-across-staged-runs.patchtext/x-patch; charset=US-ASCII; name=v31-0009-preserve-catalog-lists-across-staged-runs.patchDownload
From 4a4690875faf8952e4f6f83f6f50f761ab1adb42 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 6 Nov 2024 21:30:49 -0500
Subject: [PATCH v31 09/11] preserve catalog lists across staged runs

---
 src/bin/scripts/vacuumdb.c | 113 ++++++++++++++++++++++++++-----------
 1 file changed, 80 insertions(+), 33 deletions(-)

diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index 36f4796db0..b13f3c4224 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -68,6 +68,9 @@ static void vacuum_one_database(ConnParams *cparams,
 								int stage,
 								SimpleStringList *objects,
 								int concurrentCons,
+								PGconn *conn,
+								SimpleStringList *dbtables,
+								int ntups,
 								const char *progname, bool echo, bool quiet);
 
 static void vacuum_all_databases(ConnParams *cparams,
@@ -83,6 +86,14 @@ static void prepare_vacuum_command(PQExpBuffer sql, int serverVersion,
 static void run_vacuum_command(PGconn *conn, const char *sql, bool echo,
 							   const char *table);
 
+static void check_conn_options(PGconn *conn, vacuumingOptions *vacopts);
+
+static void
+print_processing_notice(PGconn *conn, int stage, const char *progname, bool quiet);
+
+static SimpleStringList * generate_catalog_list(PGconn *conn, vacuumingOptions *vacopts,
+												SimpleStringList *objects, bool echo, int *ntups);
+
 static void help(const char *progname);
 
 void		check_objfilter(void);
@@ -386,6 +397,11 @@ main(int argc, char *argv[])
 	}
 	else
 	{
+		PGconn	   *conn;
+		int			ntup;
+		SimpleStringList   *found_objects;
+		int			stage;
+
 		if (dbname == NULL)
 		{
 			if (getenv("PGDATABASE"))
@@ -397,25 +413,37 @@ main(int argc, char *argv[])
 		}
 
 		cparams.dbname = dbname;
+		stage = (analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
+
+		conn = connectDatabase(&cparams, progname, echo, false, true);
+		check_conn_options(conn, &vacopts);
+		print_processing_notice(conn, stage, progname, quiet);
+		found_objects = generate_catalog_list(conn, &vacopts, &objects, echo, &ntup);
 
 		if (analyze_in_stages)
 		{
-			int			stage;
-
 			for (stage = 0; stage < ANALYZE_NUM_STAGES; stage++)
 			{
-				vacuum_one_database(&cparams, &vacopts,
-									stage,
+				/* the last pass disconnected the conn */
+				if (stage > 0)
+					conn = connectDatabase(&cparams, progname, echo, false, true);
+
+				vacuum_one_database(&cparams, &vacopts, stage,
 									&objects,
 									concurrentCons,
+									conn,
+									found_objects,
+									ntup,
 									progname, echo, quiet);
 			}
 		}
 		else
-			vacuum_one_database(&cparams, &vacopts,
-								ANALYZE_NO_STAGE,
+			vacuum_one_database(&cparams, &vacopts, stage,
 								&objects,
 								concurrentCons,
+								conn,
+								found_objects,
+								ntup,
 								progname, echo, quiet);
 	}
 
@@ -791,16 +819,17 @@ vacuum_one_database(ConnParams *cparams,
 					int stage,
 					SimpleStringList *objects,
 					int concurrentCons,
+					PGconn *conn,
+					SimpleStringList *dbtables,
+					int ntups,
 					const char *progname, bool echo, bool quiet)
 {
 	PQExpBufferData sql;
-	PGconn	   *conn;
 	SimpleStringListCell *cell;
 	ParallelSlotArray *sa;
-	int			ntups;
 	bool		failed = false;
 	const char *initcmd;
-	SimpleStringList *dbtables;
+
 	const char *stage_commands[] = {
 		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
 		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
@@ -810,13 +839,6 @@ vacuum_one_database(ConnParams *cparams,
 	Assert(stage == ANALYZE_NO_STAGE ||
 		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
 
-	conn = connectDatabase(cparams, progname, echo, false, true);
-
-	check_conn_options(conn, vacopts);
-	print_processing_notice(conn, stage, progname, quiet);
-
-	dbtables = generate_catalog_list(conn, vacopts, objects, echo, &ntups);
-
 	/*
 	 * If no rows are returned, there are no matching tables, so we are done.
 	 */
@@ -928,7 +950,7 @@ finish:
 }
 
 /*
- * Vacuum/analyze all connectable databases.
+ * Vacuum/analyze all ccparams->override_dbname = PQgetvalue(result, i, 0);onnectable databases.
  *
  * In analyze-in-stages mode, we process all databases in one stage before
  * moving on to the next stage.  That ensure minimal stats are available
@@ -944,8 +966,13 @@ vacuum_all_databases(ConnParams *cparams,
 {
 	PGconn	   *conn;
 	PGresult   *result;
-	int			stage;
 	int			i;
+	int			stage;
+
+	SimpleStringList  **found_objects;
+	int				   *num_tuples;
+
+	stage = (analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
 
 	conn = connectMaintenanceDatabase(cparams, progname, echo);
 	result = executeQuery(conn,
@@ -953,7 +980,33 @@ vacuum_all_databases(ConnParams *cparams,
 						  echo);
 	PQfinish(conn);
 
-	if (analyze_in_stages)
+	/*
+	 * connect to each database, check validity of options,
+	 * build the list of found objects per database,
+	 * and run the first/only vacuum stage
+	 */
+	found_objects = palloc(PQntuples(result) * sizeof(SimpleStringList *));
+	num_tuples = palloc(PQntuples(result) * sizeof (int));
+
+	for (i = 0; i < PQntuples(result); i++)
+	{
+		cparams->override_dbname = PQgetvalue(result, i, 0);
+		conn = connectDatabase(cparams, progname, echo, false, true);
+		check_conn_options(conn, vacopts);
+		print_processing_notice(conn, stage, progname, quiet);
+		found_objects[i] = generate_catalog_list(conn, vacopts, objects, echo, &num_tuples[i]);
+
+		vacuum_one_database(cparams, vacopts,
+							stage,
+							objects,
+							concurrentCons,
+							conn,
+							found_objects[i],
+							num_tuples[i],
+							progname, echo, quiet);
+	}
+
+	if (stage != ANALYZE_NO_STAGE)
 	{
 		/*
 		 * When analyzing all databases in stages, we analyze them all in the
@@ -963,35 +1016,29 @@ vacuum_all_databases(ConnParams *cparams,
 		 * This means we establish several times as many connections, but
 		 * that's a secondary consideration.
 		 */
-		for (stage = 0; stage < ANALYZE_NUM_STAGES; stage++)
+		for (stage = 1; stage < ANALYZE_NUM_STAGES; stage++)
 		{
 			for (i = 0; i < PQntuples(result); i++)
 			{
 				cparams->override_dbname = PQgetvalue(result, i, 0);
+				conn = connectDatabase(cparams, progname, echo, false, true);
+				print_processing_notice(conn, stage, progname, quiet);
 
 				vacuum_one_database(cparams, vacopts,
 									stage,
 									objects,
 									concurrentCons,
+									conn,
+									found_objects[i],
+									num_tuples[i],
 									progname, echo, quiet);
 			}
 		}
 	}
-	else
-	{
-		for (i = 0; i < PQntuples(result); i++)
-		{
-			cparams->override_dbname = PQgetvalue(result, i, 0);
-
-			vacuum_one_database(cparams, vacopts,
-								ANALYZE_NO_STAGE,
-								objects,
-								concurrentCons,
-								progname, echo, quiet);
-		}
-	}
 
 	PQclear(result);
+	pfree(found_objects);
+	pfree(num_tuples);
 }
 
 /*
-- 
2.47.0

v31-0006-split-out-check_conn_options.patchtext/x-patch; charset=US-ASCII; name=v31-0006-split-out-check_conn_options.patchDownload
From af5712c95ca2b2103f1d089ca5836a2e89eca86a Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 6 Nov 2024 15:20:07 -0500
Subject: [PATCH v31 06/11] split out check_conn_options

---
 src/bin/scripts/vacuumdb.c | 103 ++++++++++++++++++++-----------------
 1 file changed, 57 insertions(+), 46 deletions(-)

diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index d07ab7d67e..7b97a9428a 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -10,6 +10,7 @@
  *-------------------------------------------------------------------------
  */
 
+#include "libpq-fe.h"
 #include "postgres_fe.h"
 
 #include <limits.h>
@@ -459,55 +460,11 @@ escape_quotes(const char *src)
 }
 
 /*
- * vacuum_one_database
- *
- * Process tables in the given database.  If the 'tables' list is empty,
- * process all tables in the database.
- *
- * Note that this function is only concerned with running exactly one stage
- * when in analyze-in-stages mode; caller must iterate on us if necessary.
- *
- * If concurrentCons is > 1, multiple connections are used to vacuum tables
- * in parallel.  In this case and if the table list is empty, we first obtain
- * a list of tables from the database.
+ * Check connection options for compatibility with the connected database.
  */
 static void
-vacuum_one_database(ConnParams *cparams,
-					vacuumingOptions *vacopts,
-					int stage,
-					SimpleStringList *objects,
-					int concurrentCons,
-					const char *progname, bool echo, bool quiet)
+check_conn_options(PGconn *conn, vacuumingOptions *vacopts)
 {
-	PQExpBufferData sql;
-	PQExpBufferData buf;
-	PQExpBufferData catalog_query;
-	PGresult   *res;
-	PGconn	   *conn;
-	SimpleStringListCell *cell;
-	ParallelSlotArray *sa;
-	SimpleStringList dbtables = {NULL, NULL};
-	int			i;
-	int			ntups;
-	bool		failed = false;
-	bool		objects_listed = false;
-	const char *initcmd;
-	const char *stage_commands[] = {
-		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
-		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
-		"RESET default_statistics_target;"
-	};
-	const char *stage_messages[] = {
-		gettext_noop("Generating minimal optimizer statistics (1 target)"),
-		gettext_noop("Generating medium optimizer statistics (10 targets)"),
-		gettext_noop("Generating default (full) optimizer statistics")
-	};
-
-	Assert(stage == ANALYZE_NO_STAGE ||
-		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
-
-	conn = connectDatabase(cparams, progname, echo, false, true);
-
 	if (vacopts->disable_page_skipping && PQserverVersion(conn) < 90600)
 	{
 		PQfinish(conn);
@@ -575,6 +532,60 @@ vacuum_one_database(ConnParams *cparams,
 
 	/* skip_database_stats is used automatically if server supports it */
 	vacopts->skip_database_stats = (PQserverVersion(conn) >= 160000);
+}
+
+
+/*
+ * vacuum_one_database
+ *
+ * Process tables in the given database.  If the 'tables' list is empty,
+ * process all tables in the database.
+ *
+ * Note that this function is only concerned with running exactly one stage
+ * when in analyze-in-stages mode; caller must iterate on us if necessary.
+ *
+ * If concurrentCons is > 1, multiple connections are used to vacuum tables
+ * in parallel.  In this case and if the table list is empty, we first obtain
+ * a list of tables from the database.
+ */
+static void
+vacuum_one_database(ConnParams *cparams,
+					vacuumingOptions *vacopts,
+					int stage,
+					SimpleStringList *objects,
+					int concurrentCons,
+					const char *progname, bool echo, bool quiet)
+{
+	PQExpBufferData sql;
+	PQExpBufferData buf;
+	PQExpBufferData catalog_query;
+	PGresult   *res;
+	PGconn	   *conn;
+	SimpleStringListCell *cell;
+	ParallelSlotArray *sa;
+	SimpleStringList dbtables = {NULL, NULL};
+	int			i;
+	int			ntups;
+	bool		failed = false;
+	bool		objects_listed = false;
+	const char *initcmd;
+	const char *stage_commands[] = {
+		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
+		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
+		"RESET default_statistics_target;"
+	};
+	const char *stage_messages[] = {
+		gettext_noop("Generating minimal optimizer statistics (1 target)"),
+		gettext_noop("Generating medium optimizer statistics (10 targets)"),
+		gettext_noop("Generating default (full) optimizer statistics")
+	};
+
+	Assert(stage == ANALYZE_NO_STAGE ||
+		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
+
+	conn = connectDatabase(cparams, progname, echo, false, true);
+
+	check_conn_options(conn, vacopts);
 
 	if (!quiet)
 	{
-- 
2.47.0

v31-0008-split-out-generate_catalog_list.patchtext/x-patch; charset=US-ASCII; name=v31-0008-split-out-generate_catalog_list.patchDownload
From 84b0f5a8de63d9f736240fb8068b1a6698347589 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 6 Nov 2024 16:15:16 -0500
Subject: [PATCH v31 08/11] split out generate_catalog_list

---
 src/bin/scripts/vacuumdb.c | 176 +++++++++++++++++++++----------------
 1 file changed, 99 insertions(+), 77 deletions(-)

diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index e9946f79b2..36f4796db0 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -560,67 +560,38 @@ print_processing_notice(PGconn *conn, int stage, const char *progname, bool quie
 }
 
 /*
- * vacuum_one_database
- *
- * Process tables in the given database.  If the 'tables' list is empty,
- * process all tables in the database.
- *
- * Note that this function is only concerned with running exactly one stage
- * when in analyze-in-stages mode; caller must iterate on us if necessary.
- *
- * If concurrentCons is > 1, multiple connections are used to vacuum tables
- * in parallel.  In this case and if the table list is empty, we first obtain
- * a list of tables from the database.
- */
-static void
-vacuum_one_database(ConnParams *cparams,
-					vacuumingOptions *vacopts,
-					int stage,
-					SimpleStringList *objects,
-					int concurrentCons,
-					const char *progname, bool echo, bool quiet)
+	* Prepare the list of tables to process by querying the catalogs.
+	*
+	* Since we execute the constructed query with the default search_path
+	* (which could be unsafe), everything in this query MUST be fully
+	* qualified.
+	*
+	* First, build a WITH clause for the catalog query if any tables were
+	* specified, with a set of values made of relation names and their
+	* optional set of columns.  This is used to match any provided column
+	* lists with the generated qualified identifiers and to filter for the
+	* tables provided via --table.  If a listed table does not exist, the
+	* catalog query will fail.
+	*/
+static SimpleStringList *
+generate_catalog_list(PGconn *conn,
+					  vacuumingOptions *vacopts,
+					  SimpleStringList *objects,
+					  bool echo,
+					  int *ntups)
 {
-	PQExpBufferData sql;
-	PQExpBufferData buf;
 	PQExpBufferData catalog_query;
-	PGresult   *res;
-	PGconn	   *conn;
+	PQExpBufferData buf;
+	SimpleStringList *dbtables;
 	SimpleStringListCell *cell;
-	ParallelSlotArray *sa;
-	SimpleStringList dbtables = {NULL, NULL};
-	int			i;
-	int			ntups;
-	bool		failed = false;
 	bool		objects_listed = false;
-	const char *initcmd;
-	const char *stage_commands[] = {
-		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
-		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
-		"RESET default_statistics_target;"
-	};
+	PGresult   *res;
+	int			i;
 
-	Assert(stage == ANALYZE_NO_STAGE ||
-		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
+	dbtables = palloc(sizeof(SimpleStringList));
+	dbtables->head = NULL;
+	dbtables->tail = NULL;
 
-	conn = connectDatabase(cparams, progname, echo, false, true);
-
-	check_conn_options(conn, vacopts);
-	print_processing_notice(conn, stage, progname, quiet);
-
-	/*
-	 * Prepare the list of tables to process by querying the catalogs.
-	 *
-	 * Since we execute the constructed query with the default search_path
-	 * (which could be unsafe), everything in this query MUST be fully
-	 * qualified.
-	 *
-	 * First, build a WITH clause for the catalog query if any tables were
-	 * specified, with a set of values made of relation names and their
-	 * optional set of columns.  This is used to match any provided column
-	 * lists with the generated qualified identifiers and to filter for the
-	 * tables provided via --table.  If a listed table does not exist, the
-	 * catalog query will fail.
-	 */
 	initPQExpBuffer(&catalog_query);
 	for (cell = objects ? objects->head : NULL; cell; cell = cell->next)
 	{
@@ -771,40 +742,91 @@ vacuum_one_database(ConnParams *cparams,
 	appendPQExpBufferStr(&catalog_query, " ORDER BY c.relpages DESC;");
 	executeCommand(conn, "RESET search_path;", echo);
 	res = executeQuery(conn, catalog_query.data, echo);
+	*ntups = PQntuples(res);
 	termPQExpBuffer(&catalog_query);
 	PQclear(executeQuery(conn, ALWAYS_SECURE_SEARCH_PATH_SQL, echo));
 
-	/*
-	 * If no rows are returned, there are no matching tables, so we are done.
-	 */
-	ntups = PQntuples(res);
-	if (ntups == 0)
-	{
-		PQclear(res);
-		PQfinish(conn);
-		return;
-	}
-
 	/*
 	 * Build qualified identifiers for each table, including the column list
 	 * if given.
 	 */
-	initPQExpBuffer(&buf);
-	for (i = 0; i < ntups; i++)
+	if (*ntups > 0)
 	{
-		appendPQExpBufferStr(&buf,
-							 fmtQualifiedId(PQgetvalue(res, i, 1),
-											PQgetvalue(res, i, 0)));
+		initPQExpBuffer(&buf);
+		for (i = 0; i < *ntups; i++)
+		{
+			appendPQExpBufferStr(&buf,
+								fmtQualifiedId(PQgetvalue(res, i, 1),
+												PQgetvalue(res, i, 0)));
 
-		if (objects_listed && !PQgetisnull(res, i, 2))
-			appendPQExpBufferStr(&buf, PQgetvalue(res, i, 2));
+			if (objects_listed && !PQgetisnull(res, i, 2))
+				appendPQExpBufferStr(&buf, PQgetvalue(res, i, 2));
 
-		simple_string_list_append(&dbtables, buf.data);
-		resetPQExpBuffer(&buf);
+			simple_string_list_append(dbtables, buf.data);
+			resetPQExpBuffer(&buf);
+		}
+		termPQExpBuffer(&buf);
 	}
-	termPQExpBuffer(&buf);
 	PQclear(res);
 
+	return dbtables;
+}
+
+/*
+ * vacuum_one_database
+ *
+ * Process tables in the given database.  If the 'tables' list is empty,
+ * process all tables in the database.
+ *
+ * Note that this function is only concerned with running exactly one stage
+ * when in analyze-in-stages mode; caller must iterate on us if necessary.
+ *
+ * If concurrentCons is > 1, multiple connections are used to vacuum tables
+ * in parallel.  In this case and if the table list is empty, we first obtain
+ * a list of tables from the database.
+ */
+static void
+vacuum_one_database(ConnParams *cparams,
+					vacuumingOptions *vacopts,
+					int stage,
+					SimpleStringList *objects,
+					int concurrentCons,
+					const char *progname, bool echo, bool quiet)
+{
+	PQExpBufferData sql;
+	PGconn	   *conn;
+	SimpleStringListCell *cell;
+	ParallelSlotArray *sa;
+	int			ntups;
+	bool		failed = false;
+	const char *initcmd;
+	SimpleStringList *dbtables;
+	const char *stage_commands[] = {
+		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
+		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
+		"RESET default_statistics_target;"
+	};
+
+	Assert(stage == ANALYZE_NO_STAGE ||
+		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
+
+	conn = connectDatabase(cparams, progname, echo, false, true);
+
+	check_conn_options(conn, vacopts);
+	print_processing_notice(conn, stage, progname, quiet);
+
+	dbtables = generate_catalog_list(conn, vacopts, objects, echo, &ntups);
+
+	/*
+	 * If no rows are returned, there are no matching tables, so we are done.
+	 */
+	if (ntups == 0)
+	{
+		PQfinish(conn);
+		return;
+	}
+
+
 	/*
 	 * Ensure concurrentCons is sane.  If there are more connections than
 	 * vacuumable relations, we don't need to use them all.
@@ -837,7 +859,7 @@ vacuum_one_database(ConnParams *cparams,
 
 	initPQExpBuffer(&sql);
 
-	cell = dbtables.head;
+	cell = dbtables->head;
 	do
 	{
 		const char *tabname = cell->val;
-- 
2.47.0

v31-0004-Enable-in-place-updates-for-pg_restore_relation_.patchtext/x-patch; charset=US-ASCII; name=v31-0004-Enable-in-place-updates-for-pg_restore_relation_.patchDownload
From d14127d3f2a7eec091640f69ba485ac48e8944d7 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Mon, 4 Nov 2024 14:02:57 -0500
Subject: [PATCH v31 04/11] Enable in-place updates for
 pg_restore_relation_stats.

This matches the behavior of the ANALYZE command, and would avoid
bloating pg_class in an upgrade situation wherein
pg_restore_relation_stats would be called for nearly every relation in
the database.
---
 src/backend/statistics/relation_stats.c | 72 +++++++++++++++++++------
 1 file changed, 55 insertions(+), 17 deletions(-)

diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index ed5dea2e05..939ad56fc2 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -22,6 +22,7 @@
 #include "statistics/stat_utils.h"
 #include "utils/fmgrprotos.h"
 #include "utils/syscache.h"
+#include "utils/fmgroids.h"
 
 #define DEFAULT_RELPAGES Int32GetDatum(0)
 #define DEFAULT_RELTUPLES Float4GetDatum(-1.0)
@@ -50,13 +51,14 @@ static struct StatsArgInfo relarginfo[] =
 	[NUM_RELATION_STATS_ARGS] = {0}
 };
 
-static bool relation_statistics_update(FunctionCallInfo fcinfo, int elevel);
+static bool relation_statistics_update(FunctionCallInfo fcinfo, int elevel,
+									   bool inplace);
 
 /*
  * Internal function for modifying statistics for a relation.
  */
 static bool
-relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
+relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
 {
 	Oid			reloid;
 	Relation	crel;
@@ -68,6 +70,7 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	int			ncols = 0;
 	TupleDesc	tupdesc;
 	bool		result = true;
+	void	   *inplace_state;
 
 	stats_check_required_arg(fcinfo, relarginfo, RELATION_ARG);
 	reloid = PG_GETARG_OID(RELATION_ARG);
@@ -81,7 +84,20 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	crel = table_open(RelationRelationId, RowExclusiveLock);
 
 	tupdesc = RelationGetDescr(crel);
-	ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(reloid));
+	if (inplace)
+	{
+		ScanKeyData key[1];
+
+		ctup = NULL;
+
+		ScanKeyInit(&key[0], Anum_pg_class_oid, BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(reloid));
+		systable_inplace_update_begin(crel, ClassOidIndexId, true, NULL, 1, key,
+									  &ctup, &inplace_state);
+	}
+	else
+		ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(reloid));
+
 	if (!HeapTupleIsValid(ctup))
 	{
 		ereport(elevel,
@@ -112,8 +128,13 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 		}
 		else if (relpages != pgcform->relpages)
 		{
-			replaces[ncols] = Anum_pg_class_relpages;
-			values[ncols] = Int32GetDatum(relpages);
+			if (inplace)
+				pgcform->relpages = relpages;
+			else
+			{
+				replaces[ncols] = Anum_pg_class_relpages;
+				values[ncols] = Int32GetDatum(relpages);
+			}
 			ncols++;
 		}
 	}
@@ -131,8 +152,13 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 		}
 		else if (reltuples != pgcform->reltuples)
 		{
-			replaces[ncols] = Anum_pg_class_reltuples;
-			values[ncols] = Float4GetDatum(reltuples);
+			if (inplace)
+				pgcform->reltuples = reltuples;
+			else
+			{
+				replaces[ncols] = Anum_pg_class_reltuples;
+				values[ncols] = Float4GetDatum(reltuples);
+			}
 			ncols++;
 		}
 
@@ -151,8 +177,13 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 		}
 		else if (relallvisible != pgcform->relallvisible)
 		{
-			replaces[ncols] = Anum_pg_class_relallvisible;
-			values[ncols] = Int32GetDatum(relallvisible);
+			if (inplace)
+				pgcform->relallvisible = relallvisible;
+			else
+			{
+				replaces[ncols] = Anum_pg_class_relallvisible;
+				values[ncols] = Int32GetDatum(relallvisible);
+			}
 			ncols++;
 		}
 	}
@@ -160,13 +191,20 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	/* only update pg_class if there is a meaningful change */
 	if (ncols > 0)
 	{
-		HeapTuple	newtup;
+		if (inplace)
+			systable_inplace_update_finish(inplace_state, ctup);
+		else
+		{
+			HeapTuple	newtup;
 
-		newtup = heap_modify_tuple_by_cols(ctup, tupdesc, ncols, replaces, values,
-										   nulls);
-		CatalogTupleUpdate(crel, &newtup->t_self, newtup);
-		heap_freetuple(newtup);
+			newtup = heap_modify_tuple_by_cols(ctup, tupdesc, ncols, replaces, values,
+											nulls);
+			CatalogTupleUpdate(crel, &newtup->t_self, newtup);
+			heap_freetuple(newtup);
+		}
 	}
+	else if (inplace)
+		systable_inplace_update_cancel(inplace_state);
 
 	/* release the lock, consistent with vac_update_relstats() */
 	table_close(crel, RowExclusiveLock);
@@ -182,7 +220,7 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 Datum
 pg_set_relation_stats(PG_FUNCTION_ARGS)
 {
-	relation_statistics_update(fcinfo, ERROR);
+	relation_statistics_update(fcinfo, ERROR, false);
 	PG_RETURN_VOID();
 }
 
@@ -206,7 +244,7 @@ pg_clear_relation_stats(PG_FUNCTION_ARGS)
 	newfcinfo->args[3].value = DEFAULT_RELALLVISIBLE;
 	newfcinfo->args[3].isnull = false;
 
-	relation_statistics_update(newfcinfo, ERROR);
+	relation_statistics_update(newfcinfo, ERROR, false);
 	PG_RETURN_VOID();
 }
 
@@ -224,7 +262,7 @@ pg_restore_relation_stats(PG_FUNCTION_ARGS)
 										  relarginfo, WARNING))
 		result = false;
 
-	if (!relation_statistics_update(positional_fcinfo, WARNING))
+	if (!relation_statistics_update(positional_fcinfo, WARNING, true))
 		result = false;
 
 	PG_RETURN_BOOL(result);
-- 
2.47.0

v31-0002-Remove-schemaOnly-dataOnly-from-dump-restore-opt.patchtext/x-patch; charset=US-ASCII; name=v31-0002-Remove-schemaOnly-dataOnly-from-dump-restore-opt.patchDownload
From 3b7e12709e219aeb77383395c3cf793c1b82818b Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Fri, 1 Nov 2024 11:56:34 -0400
Subject: [PATCH v31 02/11] Remove schemaOnly, dataOnly from dump/restore
 options structures.

The user-facing flags still exist, but the results of those flags are
already resolved into dumpSchema and dumpData.

All logic about which objects to dump which previously used schemaOnly
and dataOnly in the negative (ex. we should dump large objects because
schemaOnly is NOT set), which would have gotten unweildly when a third
option (statisticsOnly) was added.
---
 src/bin/pg_dump/pg_backup.h          |  4 ----
 src/bin/pg_dump/pg_backup_archiver.c | 26 +++++++++++++-------------
 src/bin/pg_dump/pg_dump.c            | 26 ++++++++++++++------------
 src/bin/pg_dump/pg_restore.c         | 15 +++++++++------
 4 files changed, 36 insertions(+), 35 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 9bd3266a63..f74e848714 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -116,8 +116,6 @@ typedef struct _restoreOptions
 	int			strict_names;
 
 	const char *filename;
-	int			dataOnly;
-	int			schemaOnly;
 	int			dumpSections;
 	int			verbose;
 	int			aclsSkip;
@@ -171,8 +169,6 @@ typedef struct _dumpOptions
 	int			binary_upgrade;
 
 	/* various user-settable parameters */
-	bool		schemaOnly;
-	bool		dataOnly;
 	int			dumpSections;	/* bitmask of chosen sections */
 	bool		aclsSkip;
 	const char *lockWaitTimeout;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 8c20c263c4..32c8e588f5 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -165,8 +165,8 @@ dumpOptionsFromRestoreOptions(RestoreOptions *ropt)
 	dopt->cparams.username = ropt->cparams.username ? pg_strdup(ropt->cparams.username) : NULL;
 	dopt->cparams.promptPassword = ropt->cparams.promptPassword;
 	dopt->outputClean = ropt->dropSchema;
-	dopt->dataOnly = ropt->dataOnly;
-	dopt->schemaOnly = ropt->schemaOnly;
+	dopt->dumpData = ropt->dumpData;
+	dopt->dumpSchema = ropt->dumpSchema;
 	dopt->if_exists = ropt->if_exists;
 	dopt->column_inserts = ropt->column_inserts;
 	dopt->dumpSections = ropt->dumpSections;
@@ -419,12 +419,12 @@ RestoreArchive(Archive *AHX)
 	 * Work out if we have an implied data-only restore. This can happen if
 	 * the dump was data only or if the user has used a toc list to exclude
 	 * all of the schema data. All we do is look for schema entries - if none
-	 * are found then we set the dataOnly flag.
+	 * are found then we set the data-only flag.
 	 *
 	 * We could scan for wanted TABLE entries, but that is not the same as
-	 * dataOnly. At this stage, it seems unnecessary (6-Mar-2001).
+	 * data-only. At this stage, it seems unnecessary (6-Mar-2001).
 	 */
-	if (!ropt->dataOnly)
+	if (ropt->dumpSchema)
 	{
 		int			impliedDataOnly = 1;
 
@@ -438,7 +438,7 @@ RestoreArchive(Archive *AHX)
 		}
 		if (impliedDataOnly)
 		{
-			ropt->dataOnly = impliedDataOnly;
+			ropt->dumpSchema = false;
 			pg_log_info("implied data-only restore");
 		}
 	}
@@ -824,7 +824,7 @@ restore_toc_entry(ArchiveHandle *AH, TocEntry *te, bool is_parallel)
 	/* Dump any relevant dump warnings to stderr */
 	if (!ropt->suppressDumpWarnings && strcmp(te->desc, "WARNING") == 0)
 	{
-		if (!ropt->dataOnly && te->defn != NULL && strlen(te->defn) != 0)
+		if (ropt->dumpSchema && te->defn != NULL && strlen(te->defn) != 0)
 			pg_log_warning("warning from original dump file: %s", te->defn);
 		else if (te->copyStmt != NULL && strlen(te->copyStmt) != 0)
 			pg_log_warning("warning from original dump file: %s", te->copyStmt);
@@ -1090,7 +1090,7 @@ _disableTriggersIfNecessary(ArchiveHandle *AH, TocEntry *te)
 	RestoreOptions *ropt = AH->public.ropt;
 
 	/* This hack is only needed in a data-only restore */
-	if (!ropt->dataOnly || !ropt->disable_triggers)
+	if (ropt->dumpSchema || !ropt->disable_triggers)
 		return;
 
 	pg_log_info("disabling triggers for %s", te->tag);
@@ -1116,7 +1116,7 @@ _enableTriggersIfNecessary(ArchiveHandle *AH, TocEntry *te)
 	RestoreOptions *ropt = AH->public.ropt;
 
 	/* This hack is only needed in a data-only restore */
-	if (!ropt->dataOnly || !ropt->disable_triggers)
+	if (ropt->dumpSchema || !ropt->disable_triggers)
 		return;
 
 	pg_log_info("enabling triggers for %s", te->tag);
@@ -3148,12 +3148,12 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 		return 0;
 
 	/* Mask it if we only want schema */
-	if (ropt->schemaOnly)
+	if (!ropt->dumpData)
 	{
 		/*
-		 * The sequence_data option overrides schemaOnly for SEQUENCE SET.
+		 * The sequence_data option overrides schema-only for SEQUENCE SET.
 		 *
-		 * In binary-upgrade mode, even with schemaOnly set, we do not mask
+		 * In binary-upgrade mode, even with schema-only set, we do not mask
 		 * out large objects.  (Only large object definitions, comments and
 		 * other metadata should be generated in binary-upgrade mode, not the
 		 * actual data, but that need not concern us here.)
@@ -3172,7 +3172,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	}
 
 	/* Mask it if we only want data */
-	if (ropt->dataOnly)
+	if (!ropt->dumpSchema)
 		res = res & REQ_DATA;
 
 	return res;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 7ce1c7a9ce..46e1da1856 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -427,6 +427,8 @@ main(int argc, char **argv)
 	char	   *compression_algorithm_str = "none";
 	char	   *error_detail = NULL;
 	bool		user_compression_defined = false;
+	bool		data_only = false;
+	bool		schema_only = false;
 	DataDirSyncMethod sync_method = DATA_DIR_SYNC_METHOD_FSYNC;
 
 	static DumpOptions dopt;
@@ -543,7 +545,7 @@ main(int argc, char **argv)
 		switch (c)
 		{
 			case 'a':			/* Dump data only */
-				dopt.dataOnly = true;
+				data_only = true;
 				break;
 
 			case 'b':			/* Dump LOs */
@@ -616,7 +618,7 @@ main(int argc, char **argv)
 				break;
 
 			case 's':			/* dump schema only */
-				dopt.schemaOnly = true;
+				schema_only = true;
 				break;
 
 			case 'S':			/* Username for superuser in plain text output */
@@ -780,24 +782,24 @@ main(int argc, char **argv)
 	if (dopt.binary_upgrade)
 		dopt.sequence_data = 1;
 
-	if (dopt.dataOnly && dopt.schemaOnly)
+	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
 
-	if (dopt.schemaOnly && foreign_servers_include_patterns.head != NULL)
+	if (schema_only && foreign_servers_include_patterns.head != NULL)
 		pg_fatal("options -s/--schema-only and --include-foreign-data cannot be used together");
 
 	if (numWorkers > 1 && foreign_servers_include_patterns.head != NULL)
 		pg_fatal("option --include-foreign-data is not supported with parallel backup");
 
-	if (dopt.dataOnly && dopt.outputClean)
+	if (data_only && dopt.outputClean)
 		pg_fatal("options -c/--clean and -a/--data-only cannot be used together");
 
 	if (dopt.if_exists && !dopt.outputClean)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
 	/* set derivative flags */
-	dopt.dumpSchema = (!dopt.dataOnly);
-	dopt.dumpData = (!dopt.schemaOnly);
+	dopt.dumpSchema = (!data_only);
+	dopt.dumpData = (!schema_only);
 
 	/*
 	 * --inserts are already implied above if --column-inserts or
@@ -1095,8 +1097,8 @@ main(int argc, char **argv)
 	ropt->cparams.username = dopt.cparams.username ? pg_strdup(dopt.cparams.username) : NULL;
 	ropt->cparams.promptPassword = dopt.cparams.promptPassword;
 	ropt->dropSchema = dopt.outputClean;
-	ropt->dataOnly = dopt.dataOnly;
-	ropt->schemaOnly = dopt.schemaOnly;
+	ropt->dumpData = dopt.dumpData;
+	ropt->dumpSchema = dopt.dumpSchema;
 	ropt->if_exists = dopt.if_exists;
 	ropt->column_inserts = dopt.column_inserts;
 	ropt->dumpSections = dopt.dumpSections;
@@ -1991,7 +1993,7 @@ selectDumpableType(TypeInfo *tyinfo, Archive *fout)
  *		Mark a default ACL as to be dumped or not
  *
  * For per-schema default ACLs, dump if the schema is to be dumped.
- * Otherwise dump if we are dumping "everything".  Note that dataOnly
+ * Otherwise dump if we are dumping "everything".  Note that data-only
  * and aclsSkip are checked separately.
  */
 static void
@@ -17666,7 +17668,7 @@ collectSequences(Archive *fout)
 	if (fout->remoteVersion < 100000)
 		return;
 	else if (fout->remoteVersion < 180000 ||
-			 (fout->dopt->schemaOnly && !fout->dopt->sequence_data))
+			 (!fout->dopt->dumpData && !fout->dopt->sequence_data))
 		query = "SELECT seqrelid, format_type(seqtypid, NULL), "
 			"seqstart, seqincrement, "
 			"seqmax, seqmin, "
@@ -18533,7 +18535,7 @@ processExtensionTables(Archive *fout, ExtensionInfo extinfo[],
 	 * objects for them, ensuring their data will be dumped even though the
 	 * tables themselves won't be.
 	 *
-	 * Note that we create TableDataInfo objects even in schemaOnly mode, ie,
+	 * Note that we create TableDataInfo objects even in schema-only mode, ie,
 	 * user data in a configuration table is treated like schema data. This
 	 * seems appropriate since system data in a config table would get
 	 * reloaded by CREATE EXTENSION.  If the extension is not listed in the
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index d2db23c75a..d3b6debcab 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -76,6 +76,9 @@ main(int argc, char **argv)
 	static int	no_subscriptions = 0;
 	static int	strict_names = 0;
 
+	bool		data_only = false;
+	bool		schema_only = false;
+
 	struct option cmdopts[] = {
 		{"clean", 0, NULL, 'c'},
 		{"create", 0, NULL, 'C'},
@@ -160,7 +163,7 @@ main(int argc, char **argv)
 		switch (c)
 		{
 			case 'a':			/* Dump data only */
-				opts->dataOnly = 1;
+				data_only = true;
 				break;
 			case 'c':			/* clean (i.e., drop) schema prior to create */
 				opts->dropSchema = 1;
@@ -236,7 +239,7 @@ main(int argc, char **argv)
 				simple_string_list_append(&opts->triggerNames, optarg);
 				break;
 			case 's':			/* dump schema only */
-				opts->schemaOnly = 1;
+				schema_only = true;
 				break;
 			case 'S':			/* Superuser username */
 				if (strlen(optarg) != 0)
@@ -339,10 +342,10 @@ main(int argc, char **argv)
 		opts->useDB = 1;
 	}
 
-	if (opts->dataOnly && opts->schemaOnly)
+	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
 
-	if (opts->dataOnly && opts->dropSchema)
+	if (data_only && opts->dropSchema)
 		pg_fatal("options -c/--clean and -a/--data-only cannot be used together");
 
 	if (opts->single_txn && opts->txn_size > 0)
@@ -360,8 +363,8 @@ main(int argc, char **argv)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
 	/* set derivative flags */
-	opts->dumpSchema = (opts->dataOnly != 1);
-	opts->dumpData = (opts->schemaOnly != 1);
+	opts->dumpSchema = (!data_only);
+	opts->dumpData = (!schema_only);
 
 	opts->disable_triggers = disable_triggers;
 	opts->enable_row_security = enable_row_security;
-- 
2.47.0

v31-0003-Enable-dumping-of-table-index-stats-in-pg_dump.patchtext/x-patch; charset=US-ASCII; name=v31-0003-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From 602ab638f701cf6cc1357babe60462d317937392 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH v31 03/11] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will generate a statement that
calls pg_set_relation_stats(), and it will generate a series of
statements that call pg_set_attribute_stats(), one per attribute. These
statements will restore the statistics of the current system onto the
destination system.

Adds the command-line options -X / --statistics-only, which are mutually
exclusive to --schema-only and --data-only.

Statistics are not dumped when --schema-only is specified, except during
a binary upgrade.

As is the pattern with pg_dump options, staistics can be disabled using
--no-statistics.

Table statistics are dumped in the data section. This is true even if
dumping stats in a binary upgrade. Index and Materialized View
statistics are dumped in the post-data section.
---
 src/bin/pg_dump/pg_backup.h          |   4 +
 src/bin/pg_dump/pg_backup_archiver.c |   5 +
 src/bin/pg_dump/pg_dump.c            | 395 ++++++++++++++++++++++++++-
 src/bin/pg_dump/pg_dump.h            |   8 +
 src/bin/pg_dump/pg_dump_sort.c       |   5 +
 src/bin/pg_dump/pg_dumpall.c         |   5 +
 src/bin/pg_dump/pg_restore.c         |  18 +-
 src/bin/pg_dump/t/001_basic.pl       |  18 ++
 doc/src/sgml/ref/pg_dump.sgml        |  36 ++-
 doc/src/sgml/ref/pg_restore.sgml     |  31 ++-
 10 files changed, 509 insertions(+), 16 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index f74e848714..b03b69ff00 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -113,6 +113,7 @@ typedef struct _restoreOptions
 	int			no_publications;	/* Skip publication entries */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
@@ -160,6 +161,7 @@ typedef struct _restoreOptions
 	/* flags derived entirely from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } RestoreOptions;
 
 typedef struct _dumpOptions
@@ -182,6 +184,7 @@ typedef struct _dumpOptions
 	int			no_security_labels;
 	int			no_publications;
 	int			no_subscriptions;
+	int			no_statistics;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
@@ -208,6 +211,7 @@ typedef struct _dumpOptions
 	/* flags derived entirely from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } DumpOptions;
 
 /*
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 32c8e588f5..4edcb4eca5 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2958,6 +2958,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's a stats dump, maybe ignore it */
+	if (ropt->no_statistics && strcmp(te->desc, "STATISTICS") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2987,6 +2991,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 46e1da1856..25df8d56cf 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -429,6 +429,7 @@ main(int argc, char **argv)
 	bool		user_compression_defined = false;
 	bool		data_only = false;
 	bool		schema_only = false;
+	bool		statistics_only = false;
 	DataDirSyncMethod sync_method = DATA_DIR_SYNC_METHOD_FSYNC;
 
 	static DumpOptions dopt;
@@ -466,6 +467,7 @@ main(int argc, char **argv)
 		{"encoding", required_argument, NULL, 'E'},
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
+		{"statistics-only", no_argument, NULL, 'X'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -494,6 +496,7 @@ main(int argc, char **argv)
 		{"no-comments", no_argument, &dopt.no_comments, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -539,7 +542,7 @@ main(int argc, char **argv)
 
 	InitDumpOptions(&dopt);
 
-	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:RsS:t:T:U:vwWxZ:",
+	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:PRsS:t:T:U:vwWxXZ:",
 							long_options, &optindex)) != -1)
 	{
 		switch (c)
@@ -613,6 +616,10 @@ main(int argc, char **argv)
 				dopt.cparams.pgport = pg_strdup(optarg);
 				break;
 
+			case 'X':			/* Dump statistics only */
+				statistics_only = true;
+				break;
+
 			case 'R':
 				/* no-op, still accepted for backwards compatibility */
 				break;
@@ -784,6 +791,13 @@ main(int argc, char **argv)
 
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (schema_only && statistics_only)
+		pg_fatal("options -s/--schema-only and -X/--statistics-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
+
+	if (statistics_only && dopt.no_statistics)
+		pg_fatal("options -X/--statistics-only and --no-statistics cannot be used together");
 
 	if (schema_only && foreign_servers_include_patterns.head != NULL)
 		pg_fatal("options -s/--schema-only and --include-foreign-data cannot be used together");
@@ -798,8 +812,20 @@ main(int argc, char **argv)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
 	/* set derivative flags */
-	dopt.dumpSchema = (!data_only);
-	dopt.dumpData = (!schema_only);
+	dopt.dumpData = data_only || (!schema_only && !statistics_only);
+	dopt.dumpSchema = schema_only || (!data_only && !statistics_only);
+
+	if (statistics_only)
+		/* stats are only thing wanted */
+		dopt.dumpStatistics = true;
+	else if (dopt.no_statistics)
+		/* stats specifically excluded */
+		dopt.dumpStatistics = false;
+	else if (dopt.binary_upgrade)
+		/* binary upgrade and not specifically excluded */
+		dopt.dumpStatistics = true;
+	else
+		dopt.dumpStatistics = (!data_only && !schema_only);
 
 	/*
 	 * --inserts are already implied above if --column-inserts or
@@ -1099,6 +1125,7 @@ main(int argc, char **argv)
 	ropt->dropSchema = dopt.outputClean;
 	ropt->dumpData = dopt.dumpData;
 	ropt->dumpSchema = dopt.dumpSchema;
+	ropt->dumpStatistics = dopt.dumpStatistics;
 	ropt->if_exists = dopt.if_exists;
 	ropt->column_inserts = dopt.column_inserts;
 	ropt->dumpSections = dopt.dumpSections;
@@ -1177,7 +1204,7 @@ help(const char *progname)
 	printf(_("  -?, --help                   show this help, then exit\n"));
 
 	printf(_("\nOptions controlling the output content:\n"));
-	printf(_("  -a, --data-only              dump only the data, not the schema\n"));
+	printf(_("  -a, --data-only              dump only the data, not the schema or statistics\n"));
 	printf(_("  -b, --large-objects          include large objects in dump\n"));
 	printf(_("  --blobs                      (same as --large-objects, deprecated)\n"));
 	printf(_("  -B, --no-large-objects       exclude large objects in dump\n"));
@@ -1190,11 +1217,12 @@ help(const char *progname)
 	printf(_("  -N, --exclude-schema=PATTERN do NOT dump the specified schema(s)\n"));
 	printf(_("  -O, --no-owner               skip restoration of object ownership in\n"
 			 "                               plain-text format\n"));
-	printf(_("  -s, --schema-only            dump only the schema, no data\n"));
+	printf(_("  -s, --schema-only            dump only the schema, no data or statistics\n"));
 	printf(_("  -S, --superuser=NAME         superuser user name to use in plain-text format\n"));
 	printf(_("  -t, --table=PATTERN          dump only the specified table(s)\n"));
 	printf(_("  -T, --exclude-table=PATTERN  do NOT dump the specified table(s)\n"));
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
+	printf(_("  -X, --statistics-only        dump only the statistics, not schema or data\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
@@ -1221,6 +1249,7 @@ help(const char *progname)
 	printf(_("  --no-comments                do not dump comments\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -6780,6 +6809,42 @@ getFuncs(Archive *fout)
 	destroyPQExpBuffer(query);
 }
 
+/*
+ * getRelationStatistics
+ *    register the statistics object as a dependent of the relation.
+ *
+ */
+static RelStatsInfo *
+getRelationStatistics(Archive *fout, DumpableObject *rel, char relkind)
+{
+	if ((relkind == RELKIND_RELATION) ||
+		(relkind == RELKIND_PARTITIONED_TABLE) ||
+		(relkind == RELKIND_INDEX) ||
+		(relkind == RELKIND_PARTITIONED_INDEX) ||
+		(relkind == RELKIND_MATVIEW))
+	{
+		RelStatsInfo	   *info = pg_malloc0(sizeof(RelStatsInfo));
+		DumpableObject	   *dobj = &info->dobj;
+
+		dobj->objType = DO_REL_STATS;
+		dobj->catId.tableoid = 0;
+		dobj->catId.oid = 0;
+		AssignDumpId(dobj);
+		dobj->dependencies = (DumpId *) pg_malloc(sizeof(DumpId));
+		dobj->dependencies[0] = rel->dumpId;
+		dobj->nDeps = 1;
+		dobj->allocDeps = 1;
+		dobj->components |= DUMP_COMPONENT_STATISTICS;
+		dobj->dump = rel->dump;
+		dobj->name = pg_strdup(rel->name);
+		dobj->namespace = rel->namespace;
+		info->relkind = relkind;
+
+		return info;
+	}
+	return NULL;
+}
+
 /*
  * getTables
  *	  read all the tables (no indexes) in the system catalogs,
@@ -7157,6 +7222,7 @@ getTables(Archive *fout, int *numTables)
 
 		/* Tables have data */
 		tblinfo[i].dobj.components |= DUMP_COMPONENT_DATA;
+		tblinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
 
 		/* Mark whether table has an ACL */
 		if (!PQgetisnull(res, i, i_relacl))
@@ -7205,6 +7271,8 @@ getTables(Archive *fout, int *numTables)
 				}
 			}
 		}
+		if (tblinfo[i].interesting)
+			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relkind);
 	}
 
 	if (query->len != 0)
@@ -7649,11 +7717,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 		for (int c = 0; c < numinds; c++, j++)
 		{
 			char		contype;
+			char		indexkind;
+			RelStatsInfo   *relstats;
 
 			indxinfo[j].dobj.objType = DO_INDEX;
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -7676,7 +7747,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			{
 				NULL, NULL
 			};
+
+			if (indxinfo[j].parentidx == 0)
+				indexkind = RELKIND_INDEX;
+			else
+				indexkind = RELKIND_PARTITIONED_INDEX;
+
 			contype = *(PQgetvalue(res, j, i_contype));
+			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, indexkind);
 
 			if (contype == 'p' || contype == 'u' || contype == 'x')
 			{
@@ -7710,6 +7788,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				constrinfo->separate = true;
 
 				indxinfo[j].indexconstraint = constrinfo->dobj.dumpId;
+				if (relstats != NULL)
+					addObjectDependency(&relstats->dobj, constrinfo->dobj.dumpId);
 			}
 			else
 			{
@@ -10297,6 +10377,296 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * Tabular description of the parameters to pg_restore_relation_stats()
+ * required, param_name, param_type
+ */
+static const char *rel_stats_arginfo[][3] = {
+	{"f", "relation", "regclass"},
+	{"f", "version", "integer"},
+	{"f", "relpages", "integer"},
+	{"f", "reltuples", "real"},
+	{"f", "relallvisible", "integer"},
+};
+
+/*
+ * Tabular description of the parameters to pg_restore_attribute_stats()
+ * required, param_name, param_type
+ */
+static const char *att_stats_arginfo[][3] = {
+	{"f", "relation", "regclass"},
+	{"f", "attname", "name"},
+	{"f", "inherited", "boolean"},
+	{"f", "version", "integer"},
+	{"f", "null_frac", "float4"},
+	{"f", "avg_width", "integer"},
+	{"f", "n_distinct", "float4"},
+	{"f", "most_common_vals", "text"},
+	{"f", "most_common_freqs", "float4[]"},
+	{"f", "histogram_bounds", "text"},
+	{"f", "correlation", "float4"},
+	{"f", "most_common_elems", "text"},
+	{"f", "most_common_elem_freqs", "float4[]"},
+	{"f", "elem_count_histogram", "float4[]"},
+	{"f", "range_length_histogram", "text"},
+	{"f", "range_empty_frac", "float4"},
+	{"f", "range_bounds_histogram", "text"},
+};
+
+/*
+ * getRelStatsExportQuery --
+ *
+ * Generate a query that will fetch all relation (e.g. pg_class)
+ * stats for a given relation.
+ */
+static void
+getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "current_setting('server_version_num') AS version, "
+						 "c.relpages, c.reltuples, c.relallvisible "
+						 "FROM pg_class c "
+						 "JOIN pg_namespace n "
+						 "ON n.oid = c.relnamespace "
+						 "WHERE n.nspname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND c.relname = ");
+	appendStringLiteralAH(query, relname, fout);
+}
+
+/*
+ * getAttStatsExportQuery --
+ *
+ * Generate a query that will fetch all attribute (e.g. pg_statistic)
+ * stats for a given relation.
+ */
+static void
+getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "s.attname,"
+						 "s.inherited,"
+						 "current_setting('server_version_num') AS version, "
+						 "s.null_frac,"
+						 "s.avg_width,"
+						 "s.n_distinct,"
+						 "s.most_common_vals,"
+						 "s.most_common_freqs,"
+						 "s.histogram_bounds,"
+						 "s.correlation,"
+						 "s.most_common_elems,"
+						 "s.most_common_elem_freqs,"
+						 "s.elem_count_histogram,");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 "s.range_length_histogram,"
+							 "s.range_empty_frac,"
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(query,
+							 "NULL AS range_length_histogram,"
+							 "NULL AS range_empty_frac,"
+							 "NULL AS range_bounds_histogram ");
+
+	appendPQExpBufferStr(query,
+						 "FROM pg_stats s "
+						 "JOIN pg_namespace n "
+						 "ON n.nspname = s.schemaname "
+						 "JOIN pg_class c "
+						 "ON c.relname = s.tablename "
+						 "AND c.relnamespace = n.oid "
+						 "WHERE s.schemaname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND s.tablename = ");
+	appendStringLiteralAH(query, relname, fout);
+	appendPQExpBufferStr(query, " ORDER BY s.attname, s.inherited");
+}
+
+
+/*
+ * appendNamedArgument --
+ *
+ * Convenience routine for constructing parameters of the form:
+ * 'paraname', 'value'::type
+ */
+static void
+appendNamedArgument(PQExpBuffer out, Archive *fout,
+					const char *argname, bool positional,
+					const char *argval, const char *argtype)
+{
+	appendPQExpBufferStr(out, "\t");
+
+	if (!positional)
+	{
+		appendStringLiteralAH(out, argname, fout);
+		appendPQExpBufferStr(out, ", ");
+	}
+
+	appendStringLiteralAH(out, argval, fout);
+	appendPQExpBuffer(out, "::%s", argtype);
+}
+
+/*
+ * appendRelStatsImport --
+ *
+ * Append a formatted pg_restore_relation_stats statement.
+ */
+static void
+appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	const char *sep = "";
+
+	if (PQntuples(res) == 0)
+		return;
+
+	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
+
+	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
+	{
+		bool		positional = (rel_stats_arginfo[argno][0][0] == 't');
+		const char *argname = rel_stats_arginfo[argno][1];
+		const char *argtype = rel_stats_arginfo[argno][2];
+		int			fieldno = PQfnumber(res, argname);
+
+		if (fieldno < 0)
+			pg_fatal("relation stats export query missing field '%s'",
+					 argname);
+
+		if (PQgetisnull(res, 0, fieldno))
+		{
+			if (!positional)
+				pg_fatal("relation stats export query unexpected NULL in '%s'",
+						 argname);
+			else
+				continue;
+		}
+
+		appendPQExpBufferStr(out, sep);
+		appendNamedArgument(out, fout, argname, positional,
+							PQgetvalue(res, 0, fieldno), argtype);
+
+		sep = ",\n";
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+/*
+ * appendAttStatsImport --
+ *
+ * Append a series of formatted pg_restore_attribute_stats statements.
+ */
+static void
+appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	{
+		const char *sep = "";
+
+		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+		for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
+		{
+			bool		positional = (att_stats_arginfo[argno][0][0] == 't');
+			const char *argname = att_stats_arginfo[argno][1];
+			const char *argtype = att_stats_arginfo[argno][2];
+			int			fieldno = PQfnumber(res, argname);
+
+			if (fieldno < 0)
+				pg_fatal("attribute stats export query missing field '%s'",
+						 argname);
+
+			if (PQgetisnull(res, rownum, fieldno))
+			{
+				if (positional)
+					pg_fatal("attribute stats export query unexpected NULL in '%s'",
+							 argname);
+				else
+					continue;
+			}
+
+			appendPQExpBufferStr(out, sep);
+			appendNamedArgument(out, fout, argname, positional,
+								PQgetvalue(res, rownum, fieldno), argtype);
+			sep = ",\n";
+		}
+		appendPQExpBufferStr(out, "\n);\n");
+	}
+}
+
+/*
+ * get statistics dump section, which depends on the parent object type
+ *
+ * objects created in SECTION_PRE_DATA have stats in SECTION_DATA
+ * objects created in SECTION_POST_DATA have stats in SECTION_POST_DATA
+ */
+static teSection
+statisticsDumpSection(const RelStatsInfo *rsinfo)
+{
+
+	if ((rsinfo->relkind == RELKIND_MATVIEW) ||
+		(rsinfo->relkind == RELKIND_INDEX) ||
+		(rsinfo->relkind == RELKIND_PARTITIONED_INDEX))
+			return SECTION_POST_DATA;
+
+	return SECTION_DATA;
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	PGresult	   *res;
+	PQExpBuffer		query;
+	PQExpBuffer		out;
+	PQExpBuffer		tag;
+	DumpableObject *dobj = (DumpableObject *) &rsinfo->dobj;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", "STATISTICS DATA", fmtId(dobj->name));
+
+	query = createPQExpBuffer();
+	out = createPQExpBuffer();
+
+	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendRelStatsImport(out, fout, res);
+	PQclear(res);
+
+	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendAttStatsImport(out, fout, res);
+	PQclear(res);
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATISTICS DATA",
+							  .section = statisticsDumpSection(rsinfo),
+							  .createStmt = out->data,
+							  .deps = dobj->dependencies,
+							  .nDeps = dobj->nDeps));
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(out);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -10745,6 +11115,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_SUBSCRIPTION_REL:
 			dumpSubscriptionTable(fout, (const SubRelInfo *) dobj);
 			break;
+		case DO_REL_STATS:
+			dumpRelationStats(fout, (const RelStatsInfo *) dobj);
+			break;
 		case DO_PRE_DATA_BOUNDARY:
 		case DO_POST_DATA_BOUNDARY:
 			/* never dumped, nothing to do */
@@ -17173,6 +17546,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 		free(indstatvalsarray);
 	}
 
+	/* Comments and stats share same .dep */
+
 	/* Dump Index Comments */
 	if (indxinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
 		dumpComment(fout, "INDEX", qindxname,
@@ -18960,6 +19335,16 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 				/* must come after the pre-data boundary */
 				addObjectDependency(dobj, preDataBound->dumpId);
 				break;
+			case DO_REL_STATS:
+				/* stats section varies by parent object type, DATA or POST */
+				if (statisticsDumpSection((RelStatsInfo *) dobj) == SECTION_DATA)
+				{
+					addObjectDependency(dobj, preDataBound->dumpId);
+					addObjectDependency(postDataBound, dobj->dumpId);
+				}
+				else
+					addObjectDependency(dobj, postDataBound->dumpId);
+				break;
 		}
 	}
 }
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index d65f558565..ef555e9178 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -84,6 +84,7 @@ typedef enum
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION,
 	DO_SUBSCRIPTION_REL,		/* see note for SubRelInfo */
+	DO_REL_STATS,
 } DumpableObjectType;
 
 /*
@@ -101,6 +102,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
@@ -421,6 +423,12 @@ typedef struct _indexAttachInfo
 	IndxInfo   *partitionIdx;	/* link to index on partition */
 } IndexAttachInfo;
 
+typedef struct _relStatsInfo
+{
+	DumpableObject	dobj;
+	char			relkind;		/* 'r', 'v', 'c', etc */
+} RelStatsInfo;
+
 typedef struct _statsExtInfo
 {
 	DumpableObject dobj;
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 3c8f2eb808..989d20aa27 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -1500,6 +1500,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "POST-DATA BOUNDARY  (ID %d)",
 					 obj->dumpId);
 			return;
+		case DO_REL_STATS:
+			snprintf(buf, bufsize,
+					 "RELATION STATISTICS FOR %s  (ID %d OID %u)",
+					 obj->name, obj->dumpId, obj->catId.oid);
+			return;
 	}
 	/* shouldn't get here */
 	snprintf(buf, bufsize,
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index e3ad8fb295..1799a03ff1 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -103,6 +103,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -172,6 +173,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -451,6 +453,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -666,6 +670,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index d3b6debcab..f8f16facc1 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -63,6 +63,7 @@ main(int argc, char **argv)
 	int			numWorkers = 1;
 	Archive    *AH;
 	char	   *inputFileSpec;
+	bool		statistics_only = false;
 	static int	disable_triggers = 0;
 	static int	enable_row_security = 0;
 	static int	if_exists = 0;
@@ -74,6 +75,7 @@ main(int argc, char **argv)
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
 	static int	no_subscriptions = 0;
+	static int  no_statistics = 0;
 	static int	strict_names = 0;
 
 	bool		data_only = false;
@@ -109,6 +111,7 @@ main(int argc, char **argv)
 		{"username", 1, NULL, 'U'},
 		{"verbose", 0, NULL, 'v'},
 		{"single-transaction", 0, NULL, '1'},
+		{"statistics-only", no_argument, NULL, 'P'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -129,6 +132,7 @@ main(int argc, char **argv)
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
+		{"no-statistics", no_argument, &no_statistics, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -272,6 +276,10 @@ main(int argc, char **argv)
 				opts->aclsSkip = 1;
 				break;
 
+			case 'X':			/* Restore statistics only */
+				statistics_only = true;
+				break;
+
 			case '1':			/* Restore data in a single transaction */
 				opts->single_txn = true;
 				opts->exit_on_error = true;
@@ -344,6 +352,10 @@ main(int argc, char **argv)
 
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
+	if (schema_only && statistics_only)
+		pg_fatal("options -s/--schema-only and -X/--statistics-only cannot be used together");
 
 	if (data_only && opts->dropSchema)
 		pg_fatal("options -c/--clean and -a/--data-only cannot be used together");
@@ -363,8 +375,9 @@ main(int argc, char **argv)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
 	/* set derivative flags */
-	opts->dumpSchema = (!data_only);
-	opts->dumpData = (!schema_only);
+	opts->dumpSchema = (!data_only && !statistics_only);
+	opts->dumpData = (!schema_only && !statistics_only);
+	opts->dumpStatistics = (!no_statistics && !data_only && !schema_only);
 
 	opts->disable_triggers = disable_triggers;
 	opts->enable_row_security = enable_row_security;
@@ -376,6 +389,7 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
diff --git a/src/bin/pg_dump/t/001_basic.pl b/src/bin/pg_dump/t/001_basic.pl
index b9d13a0e1d..76ebf15f55 100644
--- a/src/bin/pg_dump/t/001_basic.pl
+++ b/src/bin/pg_dump/t/001_basic.pl
@@ -50,12 +50,30 @@ command_fails_like(
 	'pg_dump: options -s/--schema-only and -a/--data-only cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '-s', '-X' ],
+	qr/\Qpg_dump: error: options -s\/--schema-only and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: error: options -s/--schema-only and -X/--statistics-only cannot be used together'
+);
+
+command_fails_like(
+	[ 'pg_dump', '-a', '-X' ],
+	qr/\Qpg_dump: error: options -a\/--data-only and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: error: options -a/--data-only and -X/--statistics-only cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-s', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: options -s\/--schema-only and --include-foreign-data cannot be used together\E/,
 	'pg_dump: options -s/--schema-only and --include-foreign-data cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '--statistics-only', '--no-statistics' ],
+	qr/\Qpg_dump: error: options -X\/--statistics-only and --no-statistics cannot be used together\E/,
+	'pg_dump: options -X\/--statistics-only and --no-statistics cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-j2', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: option --include-foreign-data is not supported with parallel backup\E/,
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index ffc29b04fb..99f25e40c0 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -123,7 +123,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Dump only the data, not the schema (data definitions).
+        Dump only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are dumped.
        </para>
 
@@ -141,8 +141,9 @@ PostgreSQL documentation
       <listitem>
        <para>
         Include large objects in the dump.  This is the default behavior
-        except when <option>--schema</option>, <option>--table</option>, or
-        <option>--schema-only</option> is specified.  The <option>-b</option>
+        except when <option>--schema</option>, <option>--table</option>,
+        <option>--schema-only</option>, or
+        <option>--statistics-only</option> is specified.  The <option>-b</option>
         switch is therefore only useful to add large objects to dumps
         where a specific schema or table has been requested.  Note that
         large objects are considered data and therefore will be included when
@@ -516,10 +517,11 @@ PostgreSQL documentation
       <term><option>--schema-only</option></term>
       <listitem>
        <para>
-        Dump only the object definitions (schema), not data.
+        Dump only the object definitions (schema), not data or statistics.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive to <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
         <option>--section=pre-data --section=post-data</option>.
@@ -652,6 +654,18 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Dump only the statistics, not the schema (data definitions) or data.
+        Statistics for tables, materialized views, and indexes are dumped.
+       </para>
+
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-Z <replaceable class="parameter">level</replaceable></option></term>
       <term><option>-Z <replaceable class="parameter">method</replaceable></option>[:<replaceable>detail</replaceable>]</term>
@@ -833,7 +847,8 @@ PostgreSQL documentation
         though you do not need the data in it.
        </para>
        <para>
-        To exclude data for all tables in the database, see <option>--schema-only</option>.
+        To exclude data for all tables in the database, see <option>--schema-only</option>
+        or <option>--statistics-only</option>.
        </para>
       </listitem>
      </varlistentry>
@@ -1098,6 +1113,15 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index b8b27e1719..ff1441a243 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -94,7 +94,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Restore only the data, not the schema (data definitions).
+        Restore only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are restored,
         if present in the archive.
        </para>
@@ -483,10 +483,11 @@ PostgreSQL documentation
         to the extent that schema entries are present in the archive.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive of <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
-        <option>--section=pre-data --section=post-data</option>.
+        <option>--section=pre-data --section=post-data --no-statistics</option>.
        </para>
        <para>
         (Do not confuse this with the <option>--schema</option> option, which
@@ -599,6 +600,20 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Restore only the statistics, not schema or data.
+       </para>
+       <para>
+        (Do not confuse this with the <option>--schema</option> option, which
+        uses the word <quote>schema</quote> in a different meaning.)
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-1</option></term>
       <term><option>--single-transaction</option></term>
@@ -723,6 +738,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore statistics, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
-- 
2.47.0

v31-0005-Enable-pg_clear_relation_stats-to-handle-differe.patchtext/x-patch; charset=US-ASCII; name=v31-0005-Enable-pg_clear_relation_stats-to-handle-differe.patchDownload
From 4df47c7d28880a3cde91b941f9abf13606807950 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Mon, 4 Nov 2024 16:21:39 -0500
Subject: [PATCH v31 05/11] Enable pg_clear_relation_stats to handle different
 default relpages.

If it comes to pass that relpages has the default of -1.0 for
partitioned tables (and indexes), then this patch will handle that.
---
 src/backend/statistics/relation_stats.c | 47 +++++++++++++------------
 1 file changed, 24 insertions(+), 23 deletions(-)

diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index 939ad56fc2..1aab63f5d8 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -19,15 +19,12 @@
 
 #include "access/heapam.h"
 #include "catalog/indexing.h"
+#include "catalog/pg_class_d.h"
 #include "statistics/stat_utils.h"
 #include "utils/fmgrprotos.h"
 #include "utils/syscache.h"
 #include "utils/fmgroids.h"
 
-#define DEFAULT_RELPAGES Int32GetDatum(0)
-#define DEFAULT_RELTUPLES Float4GetDatum(-1.0)
-#define DEFAULT_RELALLVISIBLE Int32GetDatum(0)
-
 /*
  * Positional argument numbers, names, and types for
  * relation_statistics_update().
@@ -51,14 +48,11 @@ static struct StatsArgInfo relarginfo[] =
 	[NUM_RELATION_STATS_ARGS] = {0}
 };
 
-static bool relation_statistics_update(FunctionCallInfo fcinfo, int elevel,
-									   bool inplace);
-
 /*
  * Internal function for modifying statistics for a relation.
  */
 static bool
-relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
+relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace, bool clear)
 {
 	Oid			reloid;
 	Relation	crel;
@@ -110,9 +104,19 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
 	pgcform = (Form_pg_class) GETSTRUCT(ctup);
 
 	/* relpages */
-	if (!PG_ARGISNULL(RELPAGES_ARG))
+	if (!PG_ARGISNULL(RELPAGES_ARG) || clear)
 	{
-		int32		relpages = PG_GETARG_INT32(RELPAGES_ARG);
+		int32		relpages;
+
+		if (clear)
+			/* relpages default varies by relkind */
+			if ((crel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) ||
+				(crel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+				relpages = -1;
+			else
+				relpages = 0;
+		else
+			relpages = PG_GETARG_INT32(RELPAGES_ARG);
 
 		/*
 		 * Partitioned tables may have relpages=-1. Note: for relations with
@@ -139,9 +143,9 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
 		}
 	}
 
-	if (!PG_ARGISNULL(RELTUPLES_ARG))
+	if (!PG_ARGISNULL(RELTUPLES_ARG) || clear)
 	{
-		float		reltuples = PG_GETARG_FLOAT4(RELTUPLES_ARG);
+		float		reltuples = (clear) ? -1.0 : PG_GETARG_FLOAT4(RELTUPLES_ARG);
 
 		if (reltuples < -1.0)
 		{
@@ -164,9 +168,9 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
 
 	}
 
-	if (!PG_ARGISNULL(RELALLVISIBLE_ARG))
+	if (!PG_ARGISNULL(RELALLVISIBLE_ARG) || clear)
 	{
-		int32		relallvisible = PG_GETARG_INT32(RELALLVISIBLE_ARG);
+		int32		relallvisible = (clear) ? 0 : PG_GETARG_INT32(RELALLVISIBLE_ARG);
 
 		if (relallvisible < 0)
 		{
@@ -220,7 +224,7 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
 Datum
 pg_set_relation_stats(PG_FUNCTION_ARGS)
 {
-	relation_statistics_update(fcinfo, ERROR, false);
+	relation_statistics_update(fcinfo, ERROR, false, false);
 	PG_RETURN_VOID();
 }
 
@@ -237,14 +241,11 @@ pg_clear_relation_stats(PG_FUNCTION_ARGS)
 
 	newfcinfo->args[0].value = PG_GETARG_OID(0);
 	newfcinfo->args[0].isnull = PG_ARGISNULL(0);
-	newfcinfo->args[1].value = DEFAULT_RELPAGES;
-	newfcinfo->args[1].isnull = false;
-	newfcinfo->args[2].value = DEFAULT_RELTUPLES;
-	newfcinfo->args[2].isnull = false;
-	newfcinfo->args[3].value = DEFAULT_RELALLVISIBLE;
-	newfcinfo->args[3].isnull = false;
+	newfcinfo->args[1].isnull = true;
+	newfcinfo->args[2].isnull = true;
+	newfcinfo->args[3].isnull = true;
 
-	relation_statistics_update(newfcinfo, ERROR, false);
+	relation_statistics_update(newfcinfo, ERROR, false, true);
 	PG_RETURN_VOID();
 }
 
@@ -262,7 +263,7 @@ pg_restore_relation_stats(PG_FUNCTION_ARGS)
 										  relarginfo, WARNING))
 		result = false;
 
-	if (!relation_statistics_update(positional_fcinfo, WARNING, true))
+	if (!relation_statistics_update(positional_fcinfo, WARNING, true, false))
 		result = false;
 
 	PG_RETURN_BOOL(result);
-- 
2.47.0

v31-0007-split-out-print_processing_notice.patchtext/x-patch; charset=US-ASCII; name=v31-0007-split-out-print_processing_notice.patchDownload
From 4675a4b96ac1e716ffb9ffad3e8cc89631a311a6 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 6 Nov 2024 15:46:51 -0500
Subject: [PATCH v31 07/11] split out print_processing_notice

---
 src/bin/scripts/vacuumdb.c | 41 +++++++++++++++++++++++---------------
 1 file changed, 25 insertions(+), 16 deletions(-)

diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index 7b97a9428a..e9946f79b2 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -535,6 +535,30 @@ check_conn_options(PGconn *conn, vacuumingOptions *vacopts)
 }
 
 
+/*
+ * print the processing notice for a database.
+ */
+static void
+print_processing_notice(PGconn *conn, int stage, const char *progname, bool quiet)
+{
+	const char *stage_messages[] = {
+		gettext_noop("Generating minimal optimizer statistics (1 target)"),
+		gettext_noop("Generating medium optimizer statistics (10 targets)"),
+		gettext_noop("Generating default (full) optimizer statistics")
+	};
+
+	if (!quiet)
+	{
+		if (stage != ANALYZE_NO_STAGE)
+			printf(_("%s: processing database \"%s\": %s\n"),
+				   progname, PQdb(conn), _(stage_messages[stage]));
+		else
+			printf(_("%s: vacuuming database \"%s\"\n"),
+				   progname, PQdb(conn));
+		fflush(stdout);
+	}
+}
+
 /*
  * vacuum_one_database
  *
@@ -574,11 +598,6 @@ vacuum_one_database(ConnParams *cparams,
 		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
 		"RESET default_statistics_target;"
 	};
-	const char *stage_messages[] = {
-		gettext_noop("Generating minimal optimizer statistics (1 target)"),
-		gettext_noop("Generating medium optimizer statistics (10 targets)"),
-		gettext_noop("Generating default (full) optimizer statistics")
-	};
 
 	Assert(stage == ANALYZE_NO_STAGE ||
 		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
@@ -586,17 +605,7 @@ vacuum_one_database(ConnParams *cparams,
 	conn = connectDatabase(cparams, progname, echo, false, true);
 
 	check_conn_options(conn, vacopts);
-
-	if (!quiet)
-	{
-		if (stage != ANALYZE_NO_STAGE)
-			printf(_("%s: processing database \"%s\": %s\n"),
-				   progname, PQdb(conn), _(stage_messages[stage]));
-		else
-			printf(_("%s: vacuuming database \"%s\"\n"),
-				   progname, PQdb(conn));
-		fflush(stdout);
-	}
+	print_processing_notice(conn, stage, progname, quiet);
 
 	/*
 	 * Prepare the list of tables to process by querying the catalogs.
-- 
2.47.0

v31-0010-Add-issues_sql_unlike-opposite-of-issues_sql_lik.patchtext/x-patch; charset=US-ASCII; name=v31-0010-Add-issues_sql_unlike-opposite-of-issues_sql_lik.patchDownload
From e6edf4dcf4f513037645690f79d7141945c73ccc Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 7 Nov 2024 18:06:52 -0500
Subject: [PATCH v31 10/11] Add issues_sql_unlike, opposite of issues_sql_like

This is the same as issues_sql_like(), but the specified text is
prohibited from being in the output rather than required.

This became necessary to test that a command-line filter was in fact
filtering out certain output that a prior test required.
---
 src/test/perl/PostgreSQL/Test/Cluster.pm | 25 ++++++++++++++++++++++++
 1 file changed, 25 insertions(+)

diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index e5526c7565..1ac087912a 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -2793,6 +2793,31 @@ sub issues_sql_like
 }
 
 =pod
+=item $node->issues_sql_unlike(cmd, prohibited_sql, test_name)
+
+Run a command on the node, then verify that $prohibited_sql does not appear
+in the server log file.
+
+=cut
+
+sub issues_sql_unlike
+{
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+	my ($self, $cmd, $prohibited_sql, $test_name) = @_;
+
+	local %ENV = $self->_get_env();
+
+	my $log_location = -s $self->logfile;
+
+	my $result = PostgreSQL::Test::Utils::run_log($cmd);
+	ok($result, "@$cmd exit code 0");
+	my $log =
+	  PostgreSQL::Test::Utils::slurp_file($self->logfile, $log_location);
+	unlike($log, $prohibited_sql, "$test_name: SQL found in server log");
+	return;
+}
+
 
 =item $node->log_content()
 
-- 
2.47.0

v31-0011-Add-force-analyze-to-vacuumdb.patchtext/x-patch; charset=US-ASCII; name=v31-0011-Add-force-analyze-to-vacuumdb.patchDownload
From 28995d82325992c9e344bbe1176a3b0c0a827bff Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Fri, 8 Nov 2024 12:27:50 -0500
Subject: [PATCH v31 11/11] Add --force-analyze to vacuumdb.

The vacuumdb options of --analyze-in-stages and --analyze-only are often
used after a restore from a dump or a pg_upgrade to quickly rebuild
stats on a databse.

However, now that stats are imported in most (but not all) cases,
running either of these commands will be at least partially redundant,
and will overwrite the stats that were just imported, which is a big
POLA violation.

We could add a new option such as --analyze-missing-in-stages, but that
wouldn't help the userbase that grown accustomed to running
--analyze-in-stages after an upgrade.

The least-bad option to handle the situation is to change the behavior
of --analyze-only and --analyze-in-stages to only analyze tables which
were missing stats before the vacuumdb started, but offer the
--force-analyze flag to restore the old behavior for those who truly
wanted it.
---
 src/bin/scripts/t/100_vacuumdb.pl |  6 +-
 src/bin/scripts/vacuumdb.c        | 91 ++++++++++++++++++++++++-------
 doc/src/sgml/ref/vacuumdb.sgml    | 48 ++++++++++++++++
 3 files changed, 125 insertions(+), 20 deletions(-)

diff --git a/src/bin/scripts/t/100_vacuumdb.pl b/src/bin/scripts/t/100_vacuumdb.pl
index 1a2bcb4959..2d669391fe 100644
--- a/src/bin/scripts/t/100_vacuumdb.pl
+++ b/src/bin/scripts/t/100_vacuumdb.pl
@@ -127,9 +127,13 @@ $node->issues_sql_like(
 	qr/statement: VACUUM \(SKIP_DATABASE_STATS, ANALYZE\) public.vactable\(a, b\);/,
 	'vacuumdb --analyze with complete column list');
 $node->issues_sql_like(
+	[ 'vacuumdb', '--analyze-only', '--force-analyze', '--table', 'vactable(b)', 'postgres' ],
+	qr/statement: ANALYZE public.vactable\(b\);/,
+	'vacuumdb --analyze-only --force-analyze with partial column list');
+$node->issues_sql_unlike(
 	[ 'vacuumdb', '--analyze-only', '--table', 'vactable(b)', 'postgres' ],
 	qr/statement: ANALYZE public.vactable\(b\);/,
-	'vacuumdb --analyze-only with partial column list');
+	'vacuumdb --analyze-only --force-analyze with partial column list skipping vacuumed tables');
 $node->command_checks_all(
 	[ 'vacuumdb', '--analyze', '--table', 'vacview', 'postgres' ],
 	0,
diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index b13f3c4224..1aa5c46af5 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -25,6 +25,7 @@
 #include "fe_utils/query_utils.h"
 #include "fe_utils/simple_list.h"
 #include "fe_utils/string_utils.h"
+#include "pqexpbuffer.h"
 
 
 /* vacuum options controlled by user flags */
@@ -47,6 +48,8 @@ typedef struct vacuumingOptions
 	bool		process_main;
 	bool		process_toast;
 	bool		skip_database_stats;
+	bool		analyze_in_stages;
+	bool		force_analyze;
 	char	   *buffer_usage_limit;
 } vacuumingOptions;
 
@@ -75,7 +78,6 @@ static void vacuum_one_database(ConnParams *cparams,
 
 static void vacuum_all_databases(ConnParams *cparams,
 								 vacuumingOptions *vacopts,
-								 bool analyze_in_stages,
 								 SimpleStringList *objects,
 								 int concurrentCons,
 								 const char *progname, bool echo, bool quiet);
@@ -140,6 +142,7 @@ main(int argc, char *argv[])
 		{"no-process-toast", no_argument, NULL, 11},
 		{"no-process-main", no_argument, NULL, 12},
 		{"buffer-usage-limit", required_argument, NULL, 13},
+		{"force-analyze", no_argument, NULL, 14},
 		{NULL, 0, NULL, 0}
 	};
 
@@ -156,7 +159,6 @@ main(int argc, char *argv[])
 	bool		echo = false;
 	bool		quiet = false;
 	vacuumingOptions vacopts;
-	bool		analyze_in_stages = false;
 	SimpleStringList objects = {NULL, NULL};
 	int			concurrentCons = 1;
 	int			tbl_count = 0;
@@ -170,6 +172,8 @@ main(int argc, char *argv[])
 	vacopts.do_truncate = true;
 	vacopts.process_main = true;
 	vacopts.process_toast = true;
+	vacopts.force_analyze = false;
+	vacopts.analyze_in_stages = false;
 
 	pg_logging_init(argv[0]);
 	progname = get_progname(argv[0]);
@@ -251,7 +255,7 @@ main(int argc, char *argv[])
 				maintenance_db = pg_strdup(optarg);
 				break;
 			case 3:
-				analyze_in_stages = vacopts.analyze_only = true;
+				vacopts.analyze_in_stages = vacopts.analyze_only = true;
 				break;
 			case 4:
 				vacopts.disable_page_skipping = true;
@@ -287,6 +291,9 @@ main(int argc, char *argv[])
 			case 13:
 				vacopts.buffer_usage_limit = escape_quotes(optarg);
 				break;
+			case 14:
+				vacopts.force_analyze = true;
+				break;
 			default:
 				/* getopt_long already emitted a complaint */
 				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -372,6 +379,14 @@ main(int argc, char *argv[])
 		pg_fatal("cannot use the \"%s\" option with the \"%s\" option",
 				 "buffer-usage-limit", "full");
 
+	/*
+	 * --force-analyze is only valid when used with --analyze-only, -analyze,
+	 * or --analyze-in-stages
+	 */
+	if (vacopts.force_analyze && !vacopts.analyze_only && !vacopts.analyze_in_stages)
+		pg_fatal("can only use the \"%s\" option with \"%s\" or \"%s\"",
+				 "--force-analyze", "-Z/--analyze-only", "--analyze-in-stages");
+
 	/* fill cparams except for dbname, which is set below */
 	cparams.pghost = host;
 	cparams.pgport = port;
@@ -390,7 +405,6 @@ main(int argc, char *argv[])
 		cparams.dbname = maintenance_db;
 
 		vacuum_all_databases(&cparams, &vacopts,
-							 analyze_in_stages,
 							 &objects,
 							 concurrentCons,
 							 progname, echo, quiet);
@@ -413,20 +427,27 @@ main(int argc, char *argv[])
 		}
 
 		cparams.dbname = dbname;
-		stage = (analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
+		stage = (vacopts.analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
 
 		conn = connectDatabase(&cparams, progname, echo, false, true);
 		check_conn_options(conn, &vacopts);
 		print_processing_notice(conn, stage, progname, quiet);
 		found_objects = generate_catalog_list(conn, &vacopts, &objects, echo, &ntup);
+		vacuum_one_database(&cparams, &vacopts, stage,
+							&objects,
+							concurrentCons,
+							conn,
+							found_objects,
+							ntup,
+							progname, echo, quiet);
 
-		if (analyze_in_stages)
+		if (stage != ANALYZE_NO_STAGE)
 		{
-			for (stage = 0; stage < ANALYZE_NUM_STAGES; stage++)
+			for (stage = 1; stage < ANALYZE_NUM_STAGES; stage++)
 			{
 				/* the last pass disconnected the conn */
-				if (stage > 0)
-					conn = connectDatabase(&cparams, progname, echo, false, true);
+				conn = connectDatabase(&cparams, progname, echo, false, true);
+				print_processing_notice(conn, stage, progname, quiet);
 
 				vacuum_one_database(&cparams, &vacopts, stage,
 									&objects,
@@ -437,14 +458,6 @@ main(int argc, char *argv[])
 									progname, echo, quiet);
 			}
 		}
-		else
-			vacuum_one_database(&cparams, &vacopts, stage,
-								&objects,
-								concurrentCons,
-								conn,
-								found_objects,
-								ntup,
-								progname, echo, quiet);
 	}
 
 	exit(0);
@@ -763,6 +776,47 @@ generate_catalog_list(PGconn *conn,
 						  vacopts->min_mxid_age);
 	}
 
+	/*
+	 * If this query is for an analyze-only or analyze-in-stages, two
+	 * upgrade-centric operations, and force-analyze is NOT set, then
+	 * exclude any relations that already have their full compliment
+	 * of attribute stats and extended stats.
+	 *
+	 *
+	 */
+	if ((vacopts->analyze_only || vacopts->analyze_in_stages) &&
+		!vacopts->force_analyze)
+	{
+		/*
+		 * The pg_class in question has no pg_statistic rows representing
+		 * user-visible columns that lack a corresponding pg_statitic row.
+		 * Currently no differentiation is made for whether the
+		 * pg_statistic.stainherit is true or false.
+		 */
+		appendPQExpBufferStr(&catalog_query,
+							 " AND NOT EXISTS (\n"
+							 " SELECT NULL FROM pg_catalog.pg_attribute AS a\n"
+							 " WHERE a.attrelid OPERATOR(pg_catalog.=) c.oid\n"
+							 " AND a.attnum OPERATOR(pg_catalog.>) 0 AND NOT a.attisdropped\n"
+							 " AND NOT EXISTS (\n"
+							 " SELECT NULL FROM pg_catalog.pg_statistic AS s\n"
+							 " WHERE s.starelid OPERATOR(pg_catalog.=) c.oid\n"
+							 " AND s.staattnum OPERATOR(pg_catalog.=) a.attnum))\n");
+
+		/*
+		 * The pg_class entry has no pg_statistic_ext rows that lack a corresponding
+		 * pg_statistic_ext_data row. Currently no differentiation is made for whether
+		 * pg_statistic_exta_data.stxdinherit is true or false.
+		 */
+		appendPQExpBufferStr(&catalog_query,
+							 " AND NOT EXISTS (\n"
+							 " SELECT NULL FROM pg_catalog.pg_statistic_ext AS e\n"
+							 " WHERE e.stxrelid OPERATOR(pg_catalog.=) c.oid\n"
+							 " AND NOT EXISTS (\n"
+							 " SELECT NULL FROM pg_catalog.pg_statistic_ext_data AS d\n"
+							 " WHERE d.stxoid OPERATOR(pg_catalog.=) e.oid))\n");
+	}
+
 	/*
 	 * Execute the catalog query.  We use the default search_path for this
 	 * query for consistency with table lookups done elsewhere by the user.
@@ -959,7 +1013,6 @@ finish:
 static void
 vacuum_all_databases(ConnParams *cparams,
 					 vacuumingOptions *vacopts,
-					 bool analyze_in_stages,
 					 SimpleStringList *objects,
 					 int concurrentCons,
 					 const char *progname, bool echo, bool quiet)
@@ -972,7 +1025,7 @@ vacuum_all_databases(ConnParams *cparams,
 	SimpleStringList  **found_objects;
 	int				   *num_tuples;
 
-	stage = (analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
+	stage = (vacopts->analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
 
 	conn = connectMaintenanceDatabase(cparams, progname, echo);
 	result = executeQuery(conn,
diff --git a/doc/src/sgml/ref/vacuumdb.sgml b/doc/src/sgml/ref/vacuumdb.sgml
index 66fccb30a2..00ec927606 100644
--- a/doc/src/sgml/ref/vacuumdb.sgml
+++ b/doc/src/sgml/ref/vacuumdb.sgml
@@ -425,6 +425,12 @@ PostgreSQL documentation
        <para>
         Only calculate statistics for use by the optimizer (no vacuum).
        </para>
+       <para>
+        By default, this operation excludes relations that already have
+        statistics generated. If the option <option>--force-analyze</option>
+        is also specified, then relations with existing stastistics are not
+        excluded.
+       </para>
       </listitem>
      </varlistentry>
 
@@ -439,6 +445,47 @@ PostgreSQL documentation
         to produce usable statistics faster, and subsequent stages build the
         full statistics.
        </para>
+       <para>
+        This option is intended to be run after a <command>pg_upgrade</command>
+        to generate statistics for relations that have no stistatics or incomplete
+        statistics (such as those with extended statistics objects, which are not
+        imported on upgrade).
+       </para>
+       <para>
+        If the option <option>--force-analyze</option> is also specified, then
+        relations with existing stastistics are not excluded.
+        This option is only useful to analyze a database that currently has
+        no statistics or has wholly incorrect ones, such as if it is newly
+        populated from a restored dump.
+        Be aware that running with this option combinationin a database with
+        existing statistics may cause the query optimizer choices to become
+        transiently worse due to the low statistics targets of the early
+        stages.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--force-analyze</option></term>
+      <listitem>
+       <para>
+        This option can only be used if either <option>--analyze-only</option>
+        or <option>--analyze-in-stages</option> is specified. It modifies those
+        options to not filter out relations that already have statistics.
+       </para>
+       <para>
+
+        Only calculate statistics for use by the optimizer (no vacuum),
+        like <option>--analyze-only</option>.  Run three
+        stages of analyze; the first stage uses the lowest possible statistics
+        target (see <xref linkend="guc-default-statistics-target"/>)
+        to produce usable statistics faster, and subsequent stages build the
+        full statistics.
+       </para>
+
+       <para>
+        This option was created
+       </para>
 
        <para>
         This option is only useful to analyze a database that currently has
@@ -452,6 +499,7 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+
      <varlistentry>
        <term><option>-?</option></term>
        <term><option>--help</option></term>
-- 
2.47.0

#220Heikki Linnakangas
hlinnaka@iki.fi
In reply to: Jeff Davis (#211)
Re: Statistics Import and Export

On 23/10/2024 01:27, Jeff Davis wrote:

I've taken most of Jeff's work, reincorporated it into roughly the
same patch structure as before, and am posting it now.

I committed 0001-0004 with significant revision.

This just caught my eye:

postgres=# select pg_set_attribute_stats('foo', 'xmin', false, 1);
pg_set_attribute_stats
------------------------

(1 row)

We should probably not allow that, because you cannot ANALYZE system
columns:

postgres=# analyze foo (xmin);
ERROR: column "xmin" of relation "foo" does not exist

--
Heikki Linnakangas
Neon (https://neon.tech)

#221Corey Huinker
corey.huinker@gmail.com
In reply to: Heikki Linnakangas (#220)
Re: Statistics Import and Export

We should probably not allow that, because you cannot ANALYZE system
columns:

Makes sense, and the fix is changing a single character (unless we think it
warrants a test case).

#222Michael Paquier
michael@paquier.xyz
In reply to: Corey Huinker (#221)
Re: Statistics Import and Export

On Wed, Nov 13, 2024 at 01:04:32AM -0500, Corey Huinker wrote:

Makes sense, and the fix is changing a single character (unless we think it
warrants a test case).

I'd suggest to add a small test case, actually. Like all the other
tests of stats_import.sql, the error happens quickly meaning that such
negative test is cheap to run while being useful to keep a track of.
--
Michael

#223Bruce Momjian
bruce@momjian.us
In reply to: Corey Huinker (#219)
Re: Statistics Import and Export

On Fri, Nov 8, 2024 at 01:25:21PM -0500, Corey Huinker wrote:

WHAT IS NOT DONE - EXTENDED STATISTICS

It is a general consensus in the community that "nobody uses extended
statistics", though I've had difficulty getting actual figures to back this
up, even from my own employer. Surveying several vendors at PgConf.EU, the
highest estimate was that at most 1% of their customers used extended
statistics, though more probably should. This reinforces my belief that a
feature that would eliminate a major pain point in upgrades for 99% of
customers shouldn't be held back by the fact that the other 1% only have a
reduced hassle.

However, having relation and attribute statistics carry over on major
version upgrades presents a slight problem: running vacuumdb
--analyze-in-stages after such an upgrade is completely unnecessary for
those without extended statistics, and would actually result in _worse_
statistics for the database until the last stage is complete. Granted,
we've had great difficulty getting users to know that vacuumdb is a thing
that should be run, but word has slowly spread through our own
documentation and those "This one simple trick will make your postgres go
fast post-upgrade" blog posts. Those posts will continue to lurk in search
results long after this feature goes into release, and it would be a rude
surprise to users to find out that the extra work they put in to learn
about a feature that helped their upgrade in 17 was suddenly detrimental
(albeit temporarily) in 18. We should never punish people for only being a
little-bit current in their knowledge. Moreover, this surprise would
persist even after we add extended statistics import function
functionality.

I presented this problem to several people at PgConf.EU, and the consensus
least-bad solution was that vacuumdb should filter out tables that are not
missing any statistics when using options --analyze, --analyze-only, and
--analyze-in-stages, with an additional flag for now called --force-analyze
to restore the un-filtered functionality. This gives the outcome tree:

1. Users who do not have extended statistics and do not use (or not even
know about) vacuumdb will be blissfully unaware, and will get better
post-upgrade performance.
2. Users who do not have extended statistics but use vacuumdb
--analyze-in-stages will be pleasantly surprised that the vacuumdb run is
almost a no-op, and completes quickly. Those who are surprised by this and
re-run vacuumdb --analyze-in-stages will get another no-op.
3. Users who have extended statistics and use vacuumdb --analyze-in-stages
will get a quicker vacuumdb run, as only the tables with extended stats
will pass the filter. Subsequent re-runs of vacuumdb --analyze-in-stages
would be the no-op.
4. Users who have extended statistics and don't use vacuumdb will still get
better performance than they would have without any stats imported.

In case anyone is curious, I'm defining "missing stats" as a table/matview
with any of the following:

1. A table with an attribute that lacks a corresponding pg_statistic row.
2. A table with an index with an expression attribute that lacks a
corresponding pg_statistic row (non-expression attributes just borrow the
pg_statistic row from the table's attribute).
3. A table with at least one extended statistic that does not have a
corresponding pg_statistic_ext_data row.

Note that none of these criteria are concerned with the substance of the
statistics (ex. pg_statistic row should have mcv stats but does not),
merely their row-existence. 

Some rejected alternative solutions were:

1. Adding a new option --analyze-missing-stats. While simple, few people
would learn about it, knowledge of it would be drowned out by the
aforementioned sea of existing blog posts.
2. Adding --analyze-missing-stats and making --analyze-in-stages fail with
an error message educating the user about --analyze-missing-stats. Users
might not see the error, existing tooling wouldn't be able to act on the
error, and there are legitimate non-upgrade uses of --analyze-in-stages.

MAIN CONCERN GOING FORWARD

This change to vacuumdb will require some reworking of the
vacuum_one_database() function so that the list of tables analyzed is
preserved across the stages, as subsequent stages runs won't be able to
detect which tables were previously missing stats.

You seem to be optimizing for people using pg_upgrade, and for people
upgrading to PG 18, without adequately considering people using vacuumdb
in non-pg_upgrade situations, and people using PG 19+. Let me explain.

First, I see little concern here for how people who use --analyze and
--analyze-only independent of pg_upgrade will be affected by this.
While I recommend people decrease vacuum and analyze threshold during
non-peak periods:

https://momjian.us/main/blogs/pgblog/2017.html#January_3_2017

some people might just regenerate all statistics during non-peak periods
using these options. You can perhaps argue that --analyze-in-stages
would only be used by pg_upgrade so maybe that can be adjusted more
easily.

Second, the API for what --analyze and --analyze-only do will be very
confusing for people running, e.g., PG 20, because the average user
reading the option name will not guess it only adds missing statistics.

I think you need to rethink your approach and just accept that a mention
of the new preserving statistic behavior of pg_upgrade, and the new
vacuumdb API required, will be sufficient. In summary, I think you need
a new --compute-missing-statistics-only that can be combined with
--analyze, --analyze-only, and --analyze-in-stages to compute only
missing statistics, and document it in the PG 18 release notes.

Frankly, we have a similar problem with partitioned tables:

https://www.postgresql.org/docs/current/sql-analyze.html

For partitioned tables, ANALYZE gathers statistics by sampling
rows from all partitions; in addition, it will recurse into
each partition and update its statistics. Each leaf partition
is analyzed only once, even with multi-level partitioning. No
statistics are collected for only the parent table (without data
from its partitions), because with partitioning it's guaranteed
to be empty.

--> The autovacuum daemon does not process partitioned tables, nor
does it process inheritance parents if only the children are ever
modified. It is usually necessary to periodically run a manual
ANALYZE to keep the statistics of the table hierarchy up to date.

Now, you can say partitioned table statistics are not as important as
extended statistics, but that fact remains that we have these two odd
cases where special work must be done to generate statistics.

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

When a patient asks the doctor, "Am I going to die?", he means
"Am I going to die soon?"

#224Wetmore, Matthew  (CTR)
Matthew.Wetmore@mdlive.com
In reply to: Bruce Momjian (#223)
1 attachment(s)
Re: Statistics Import and Export

Sorry to chime in with a dumb question:

How would/could this effect tables that have the vacuum and analyze scale_factors different from the rest of db via the ALTE RTABLE statement?

(I do this a lot)

ALTER TABLE your_schema.your_table SET (autovacuum_enabled,autovacuum_analyze_scale_factor,autovacuum_vacuum_scale_factor);

Just wanted to mention

Thanks Bruce,

/matt

--

[MDLIVE]<https://www.mdlive.com/&gt;
Matt Wetmore
Data Engineer
m. +1-415-416-9738

From: Bruce Momjian <bruce@momjian.us>
Date: Monday, November 18, 2024 at 11:48 AM
To: Corey Huinker <corey.huinker@gmail.com>
Cc: Jeff Davis <pgsql@j-davis.com>, jian he <jian.universality@gmail.com>, Matthias van de Meent <boekewurm+postgres@gmail.com>, Tom Lane <tgl@sss.pgh.pa.us>, Nathan Bossart <nathandbossart@gmail.com>, Magnus Hagander <magnus@hagander.net>, Stephen Frost <sfrost@snowman.net>, Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>, Peter Smith <smithpb2250@gmail.com>, PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>, "alvherre@alvh.no-ip.org" <alvherre@alvh.no-ip.org>
Subject: Re: Statistics Import and Export

On Fri, Nov 8, 2024 at 01: 25: 21PM -0500, Corey Huinker wrote: > WHAT IS NOT DONE - EXTENDED STATISTICS > > It is a general consensus in the community that "nobody uses extended > statistics", though I've had difficulty getting actual

On Fri, Nov 8, 2024 at 01:25:21PM -0500, Corey Huinker wrote:

WHAT IS NOT DONE - EXTENDED STATISTICS

It is a general consensus in the community that "nobody uses extended

statistics", though I've had difficulty getting actual figures to back this

up, even from my own employer. Surveying several vendors at PgConf.EU, the

highest estimate was that at most 1% of their customers used extended

statistics, though more probably should. This reinforces my belief that a

feature that would eliminate a major pain point in upgrades for 99% of

customers shouldn't be held back by the fact that the other 1% only have a

reduced hassle.

However, having relation and attribute statistics carry over on major

version upgrades presents a slight problem: running vacuumdb

--analyze-in-stages after such an upgrade is completely unnecessary for

those without extended statistics, and would actually result in _worse_

statistics for the database until the last stage is complete. Granted,

we've had great difficulty getting users to know that vacuumdb is a thing

that should be run, but word has slowly spread through our own

documentation and those "This one simple trick will make your postgres go

fast post-upgrade" blog posts. Those posts will continue to lurk in search

results long after this feature goes into release, and it would be a rude

surprise to users to find out that the extra work they put in to learn

about a feature that helped their upgrade in 17 was suddenly detrimental

(albeit temporarily) in 18. We should never punish people for only being a

little-bit current in their knowledge. Moreover, this surprise would

persist even after we add extended statistics import function

functionality.

I presented this problem to several people at PgConf.EU, and the consensus

least-bad solution was that vacuumdb should filter out tables that are not

missing any statistics when using options --analyze, --analyze-only, and

--analyze-in-stages, with an additional flag for now called --force-analyze

to restore the un-filtered functionality. This gives the outcome tree:

1. Users who do not have extended statistics and do not use (or not even

know about) vacuumdb will be blissfully unaware, and will get better

post-upgrade performance.

2. Users who do not have extended statistics but use vacuumdb

--analyze-in-stages will be pleasantly surprised that the vacuumdb run is

almost a no-op, and completes quickly. Those who are surprised by this and

re-run vacuumdb --analyze-in-stages will get another no-op.

3. Users who have extended statistics and use vacuumdb --analyze-in-stages

will get a quicker vacuumdb run, as only the tables with extended stats

will pass the filter. Subsequent re-runs of vacuumdb --analyze-in-stages

would be the no-op.

4. Users who have extended statistics and don't use vacuumdb will still get

better performance than they would have without any stats imported.

In case anyone is curious, I'm defining "missing stats" as a table/matview

with any of the following:

1. A table with an attribute that lacks a corresponding pg_statistic row.

2. A table with an index with an expression attribute that lacks a

corresponding pg_statistic row (non-expression attributes just borrow the

pg_statistic row from the table's attribute).

3. A table with at least one extended statistic that does not have a

corresponding pg_statistic_ext_data row.

Note that none of these criteria are concerned with the substance of the

statistics (ex. pg_statistic row should have mcv stats but does not),

merely their row-existence.

Some rejected alternative solutions were:

1. Adding a new option --analyze-missing-stats. While simple, few people

would learn about it, knowledge of it would be drowned out by the

aforementioned sea of existing blog posts.

2. Adding --analyze-missing-stats and making --analyze-in-stages fail with

an error message educating the user about --analyze-missing-stats. Users

might not see the error, existing tooling wouldn't be able to act on the

error, and there are legitimate non-upgrade uses of --analyze-in-stages.

MAIN CONCERN GOING FORWARD

This change to vacuumdb will require some reworking of the

vacuum_one_database() function so that the list of tables analyzed is

preserved across the stages, as subsequent stages runs won't be able to

detect which tables were previously missing stats.

You seem to be optimizing for people using pg_upgrade, and for people

upgrading to PG 18, without adequately considering people using vacuumdb

in non-pg_upgrade situations, and people using PG 19+. Let me explain.

First, I see little concern here for how people who use --analyze and

--analyze-only independent of pg_upgrade will be affected by this.

While I recommend people decrease vacuum and analyze threshold during

non-peak periods:

https://urldefense.com/v3/__https://momjian.us/main/blogs/pgblog/2017.html*January_3_2017__;Iw!!JtyhRs6NQr98nj80!5p5jBUgKYWVIwaCN-IBjzU3lWjh76cjhyOBvdbX0A7yWbineV7Ax1atOztVc1CUn_MTpqVGWkyGKDrCfXFTNfw$&lt;https://urldefense.com/v3/__https:/momjian.us/main/blogs/pgblog/2017.html*January_3_2017__;Iw!!JtyhRs6NQr98nj80!5p5jBUgKYWVIwaCN-IBjzU3lWjh76cjhyOBvdbX0A7yWbineV7Ax1atOztVc1CUn_MTpqVGWkyGKDrCfXFTNfw$&gt;

some people might just regenerate all statistics during non-peak periods

using these options. You can perhaps argue that --analyze-in-stages

would only be used by pg_upgrade so maybe that can be adjusted more

easily.

Second, the API for what --analyze and --analyze-only do will be very

confusing for people running, e.g., PG 20, because the average user

reading the option name will not guess it only adds missing statistics.

I think you need to rethink your approach and just accept that a mention

of the new preserving statistic behavior of pg_upgrade, and the new

vacuumdb API required, will be sufficient. In summary, I think you need

a new --compute-missing-statistics-only that can be combined with

--analyze, --analyze-only, and --analyze-in-stages to compute only

missing statistics, and document it in the PG 18 release notes.

Frankly, we have a similar problem with partitioned tables:

https://urldefense.com/v3/__https://www.postgresql.org/docs/current/sql-analyze.html__;!!JtyhRs6NQr98nj80!5p5jBUgKYWVIwaCN-IBjzU3lWjh76cjhyOBvdbX0A7yWbineV7Ax1atOztVc1CUn_MTpqVGWkyGKDrAcfiLyFQ$&lt;https://urldefense.com/v3/__https:/www.postgresql.org/docs/current/sql-analyze.html__;!!JtyhRs6NQr98nj80!5p5jBUgKYWVIwaCN-IBjzU3lWjh76cjhyOBvdbX0A7yWbineV7Ax1atOztVc1CUn_MTpqVGWkyGKDrAcfiLyFQ$&gt;

For partitioned tables, ANALYZE gathers statistics by sampling

rows from all partitions; in addition, it will recurse into

each partition and update its statistics. Each leaf partition

is analyzed only once, even with multi-level partitioning. No

statistics are collected for only the parent table (without data

from its partitions), because with partitioning it's guaranteed

to be empty.

--> The autovacuum daemon does not process partitioned tables, nor

does it process inheritance parents if only the children are ever

modified. It is usually necessary to periodically run a manual

ANALYZE to keep the statistics of the table hierarchy up to date.

Now, you can say partitioned table statistics are not as important as

extended statistics, but that fact remains that we have these two odd

cases where special work must be done to generate statistics.

--

Bruce Momjian <bruce@momjian.us> https://urldefense.com/v3/__https://momjian.us__;!!JtyhRs6NQr98nj80!5p5jBUgKYWVIwaCN-IBjzU3lWjh76cjhyOBvdbX0A7yWbineV7Ax1atOztVc1CUn_MTpqVGWkyGKDrCf9HVZng$&lt;https://urldefense.com/v3/__https:/momjian.us__;!!JtyhRs6NQr98nj80!5p5jBUgKYWVIwaCN-IBjzU3lWjh76cjhyOBvdbX0A7yWbineV7Ax1atOztVc1CUn_MTpqVGWkyGKDrCf9HVZng$&gt;

EDB https://urldefense.com/v3/__https://enterprisedb.com__;!!JtyhRs6NQr98nj80!5p5jBUgKYWVIwaCN-IBjzU3lWjh76cjhyOBvdbX0A7yWbineV7Ax1atOztVc1CUn_MTpqVGWkyGKDrBvicktqQ$&lt;https://urldefense.com/v3/__https:/enterprisedb.com__;!!JtyhRs6NQr98nj80!5p5jBUgKYWVIwaCN-IBjzU3lWjh76cjhyOBvdbX0A7yWbineV7Ax1atOztVc1CUn_MTpqVGWkyGKDrBvicktqQ$&gt;

When a patient asks the doctor, "Am I going to die?", he means

"Am I going to die soon?"

Attachments:

image001.pngimage/png; name=image001.pngDownload
#225Bruce Momjian
bruce@momjian.us
In reply to: Wetmore, Matthew (CTR) (#224)
Re: Statistics Import and Export

On Mon, Nov 18, 2024 at 08:06:24PM +0000, Wetmore, Matthew (CTR) wrote:

Sorry to chime in with a dumb question:

How would/could this effect tables that have the vacuum and analyze
scale_factors different from the rest of db via the ALTE RTABLE statement?

(I do this a lot)

ALTER TABLE your_schema.your_table SET
(autovacuum_enabled,autovacuum_analyze_scale_factor,autovacuum_vacuum_scale_factor);

Just wanted to mention

I don't think it would affect it since those control autovacuum, and we
are talking about manual vacuum/analyze.

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

When a patient asks the doctor, "Am I going to die?", he means
"Am I going to die soon?"

#226Corey Huinker
corey.huinker@gmail.com
In reply to: Bruce Momjian (#225)
Re: Statistics Import and Export

On Mon, Nov 18, 2024 at 3:32 PM Bruce Momjian <bruce@momjian.us> wrote:

How would/could this effect tables that have the vacuum and analyze
scale_factors different from the rest of db via the ALTE RTABLE

statement?

(I do this a lot)

I don't think it would affect it since those control autovacuum, and we
are talking about manual vacuum/analyze.

Correct. The patchset is about carrying over the gathered statistics as-is
from the previous major version. Per-table configurations for how to
collect statistics would be unchanged.

#227Corey Huinker
corey.huinker@gmail.com
In reply to: Bruce Momjian (#223)
14 attachment(s)
Re: Statistics Import and Export

On Mon, Nov 18, 2024 at 2:47 PM Bruce Momjian <bruce@momjian.us> wrote:

On Fri, Nov 8, 2024 at 01:25:21PM -0500, Corey Huinker wrote:

WHAT IS NOT DONE - EXTENDED STATISTICS

It is a general consensus in the community that "nobody uses extended
statistics", though I've had difficulty getting actual figures to

back this

up, even from my own employer. Surveying several vendors at

PgConf.EU, the

highest estimate was that at most 1% of their customers used extended
statistics, though more probably should. This reinforces my belief

that a

feature that would eliminate a major pain point in upgrades for 99%

of

customers shouldn't be held back by the fact that the other 1% only

have a

reduced hassle.

However, having relation and attribute statistics carry over on major
version upgrades presents a slight problem: running vacuumdb
--analyze-in-stages after such an upgrade is completely unnecessary

for

those without extended statistics, and would actually result in

_worse_

statistics for the database until the last stage is complete.

Granted,

we've had great difficulty getting users to know that vacuumdb is a

thing

that should be run, but word has slowly spread through our own
documentation and those "This one simple trick will make your

postgres go

fast post-upgrade" blog posts. Those posts will continue to lurk in

search

results long after this feature goes into release, and it would be a

rude

surprise to users to find out that the extra work they put in to

learn

about a feature that helped their upgrade in 17 was suddenly

detrimental

(albeit temporarily) in 18. We should never punish people for only

being a

little-bit current in their knowledge. Moreover, this surprise would
persist even after we add extended statistics import function
functionality.

I presented this problem to several people at PgConf.EU, and the

consensus

least-bad solution was that vacuumdb should filter out tables that

are not

missing any statistics when using options --analyze, --analyze-only,

and

--analyze-in-stages, with an additional flag for now called

--force-analyze

to restore the un-filtered functionality. This gives the outcome

tree:

1. Users who do not have extended statistics and do not use (or not

even

know about) vacuumdb will be blissfully unaware, and will get better
post-upgrade performance.
2. Users who do not have extended statistics but use vacuumdb
--analyze-in-stages will be pleasantly surprised that the vacuumdb

run is

almost a no-op, and completes quickly. Those who are surprised by

this and

re-run vacuumdb --analyze-in-stages will get another no-op.
3. Users who have extended statistics and use vacuumdb

--analyze-in-stages

will get a quicker vacuumdb run, as only the tables with extended

stats

will pass the filter. Subsequent re-runs of vacuumdb

--analyze-in-stages

would be the no-op.
4. Users who have extended statistics and don't use vacuumdb will

still get

better performance than they would have without any stats imported.

In case anyone is curious, I'm defining "missing stats" as a

table/matview

with any of the following:

1. A table with an attribute that lacks a corresponding pg_statistic

row.

2. A table with an index with an expression attribute that lacks a
corresponding pg_statistic row (non-expression attributes just

borrow the

pg_statistic row from the table's attribute).
3. A table with at least one extended statistic that does not have a
corresponding pg_statistic_ext_data row.

Note that none of these criteria are concerned with the substance of

the

statistics (ex. pg_statistic row should have mcv stats but does not),
merely their row-existence.

Some rejected alternative solutions were:

1. Adding a new option --analyze-missing-stats. While simple, few

people

would learn about it, knowledge of it would be drowned out by the
aforementioned sea of existing blog posts.
2. Adding --analyze-missing-stats and making --analyze-in-stages

fail with

an error message educating the user about --analyze-missing-stats.

Users

might not see the error, existing tooling wouldn't be able to act on

the

error, and there are legitimate non-upgrade uses of

--analyze-in-stages.

MAIN CONCERN GOING FORWARD

This change to vacuumdb will require some reworking of the
vacuum_one_database() function so that the list of tables analyzed is
preserved across the stages, as subsequent stages runs won't be able

to

detect which tables were previously missing stats.

You seem to be optimizing for people using pg_upgrade, and for people
upgrading to PG 18, without adequately considering people using vacuumdb
in non-pg_upgrade situations, and people using PG 19+. Let me explain.

This was a concern as I was polling people.

A person using vacuumdb in a non-upgrade situation is, to my limited
imagination, one of three types:

1. A person who views vacuumdb as a worthwhile janitorial task for
downtimes.
2. A person who wants stats on a lot of recently created tables.
3. A person who wants better stats on a lot of recently (re)populated
tables.

The first group would not be using --analyze-in-stages or --analyze-only,
because the vacuuming is a big part of it. They will be unaffected.

The second group will be pleasantly surprised to learn that they no longer
need to specify a subset of tables, as any table missing stats will get
picked up.

The third group would be surprised that their operation completed so
quickly, check the docs, add in --force-analyze to their script, and re-run.

First, I see little concern here for how people who use --analyze and
--analyze-only independent of pg_upgrade will be affected by this.
While I recommend people decrease vacuum and analyze threshold during
non-peak periods:

https://momjian.us/main/blogs/pgblog/2017.html#January_3_2017

some people might just regenerate all statistics during non-peak periods
using these options. You can perhaps argue that --analyze-in-stages
would only be used by pg_upgrade so maybe that can be adjusted more
easily.

I, personally, would be fine if this only modified --analyze-in-stages, as
it already carries the warning:

"This option is only useful to analyze a database that currently has no
statistics or has wholly incorrect ones, such as if it is newly populated
from a restored dump or by pg_upgrade. Be aware that running with this
option in a database with existing statistics may cause the query optimizer
choices to become transiently worse due to the low statistics targets of
the early stages."

But others felt that --analyze-only should be in the mix as well.

No one advocated for changing the behavior of options that involve actual
vacuuming.

Second, the API for what --analyze and --analyze-only do will be very
confusing for people running, e.g., PG 20, because the average user
reading the option name will not guess it only adds missing statistics.

I think you need to rethink your approach and just accept that a mention
of the new preserving statistic behavior of pg_upgrade, and the new
vacuumdb API required, will be sufficient. In summary, I think you need
a new --compute-missing-statistics-only that can be combined with
--analyze, --analyze-only, and --analyze-in-stages to compute only
missing statistics, and document it in the PG 18 release notes.

A --missing-only/--analyze-missing-in-stages option was my first idea, and
it's definitely cleaner, but as I stated in the rejected ideas section
above, when I reached out to others at PgConf.EU there was pretty general
consensus that few people would actually read our documentation, and the
few that have in the past are unlikely to read it again to discover the new
option, and those people would have a negative impact of using
--analyze-in-stages, effectively punishing them for having once read the
documentation (or a blog post) but not re-read it prior to upgrade.

So, to add non-pg_upgrade users to the outcome tree in my email from
2024-11-04:

5. Users who use vacuumdb in a non-upgrade situation and do not use either
--analyze-in-stages or --analyze-only will be completely unaffected.
6. Users who use vacuumdb in a non-upgrade situation with either
--analyze-in-stages or --analyze-only set will find that the operation
skips tables that already have stats, and will have to add --force-analyze
to restore previous behavior.

That's not a great surprise for group 6, but I have to believe that group
is smaller than group 5, and it's definitely smaller than the group of
users that need to upgrade.

To conclude, there's no great option here, and I went with what seemed to
be the least-bad option in terms of user impact. I'm not locked in on any
one solution, and I really hope somebody can come up with one that we all
like.

Attached is a re-basing of the existing patchset, plus 3 more additions:

0012 - disallows setting stats on system columns per Heikki's observation

0013 - Consolidates two attribute cache lookups into one

0014 - Adds a --no-data option to pg_dump/pg_restore. This will be
particularly useful in diagnostic situations where a user wants to analyze
query plans in a sandbox without having to load all the data into the
sandbox. Instead, they can just bring over the schema and statistics and
run their experiments. This patch needs a regression test but I thought I'd
put it out there early as a way of showing the value of the stats import
functions outside of upgrades.

Show quoted text

Attachments:

v32-0003-Enable-dumping-of-table-index-stats-in-pg_dump.patchtext/x-patch; charset=US-ASCII; name=v32-0003-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From 9655cf5c78db2f4516e09175868597557f3e5b33 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH v32 03/14] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will generate a statement that
calls pg_set_relation_stats(), and it will generate a series of
statements that call pg_set_attribute_stats(), one per attribute. These
statements will restore the statistics of the current system onto the
destination system.

Adds the command-line options -X / --statistics-only, which are mutually
exclusive to --schema-only and --data-only.

Statistics are not dumped when --schema-only is specified, except during
a binary upgrade.

As is the pattern with pg_dump options, staistics can be disabled using
--no-statistics.

Table statistics are dumped in the data section. This is true even if
dumping stats in a binary upgrade. Index and Materialized View
statistics are dumped in the post-data section.
---
 src/bin/pg_dump/pg_backup.h          |   4 +
 src/bin/pg_dump/pg_backup_archiver.c |   5 +
 src/bin/pg_dump/pg_dump.c            | 395 ++++++++++++++++++++++++++-
 src/bin/pg_dump/pg_dump.h            |   8 +
 src/bin/pg_dump/pg_dump_sort.c       |   5 +
 src/bin/pg_dump/pg_dumpall.c         |   5 +
 src/bin/pg_dump/pg_restore.c         |  18 +-
 src/bin/pg_dump/t/001_basic.pl       |  18 ++
 doc/src/sgml/ref/pg_dump.sgml        |  36 ++-
 doc/src/sgml/ref/pg_restore.sgml     |  31 ++-
 10 files changed, 509 insertions(+), 16 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index f74e848714..b03b69ff00 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -113,6 +113,7 @@ typedef struct _restoreOptions
 	int			no_publications;	/* Skip publication entries */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
@@ -160,6 +161,7 @@ typedef struct _restoreOptions
 	/* flags derived entirely from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } RestoreOptions;
 
 typedef struct _dumpOptions
@@ -182,6 +184,7 @@ typedef struct _dumpOptions
 	int			no_security_labels;
 	int			no_publications;
 	int			no_subscriptions;
+	int			no_statistics;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
@@ -208,6 +211,7 @@ typedef struct _dumpOptions
 	/* flags derived entirely from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } DumpOptions;
 
 /*
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 32c8e588f5..4edcb4eca5 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2958,6 +2958,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's a stats dump, maybe ignore it */
+	if (ropt->no_statistics && strcmp(te->desc, "STATISTICS") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2987,6 +2991,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 46e1da1856..25df8d56cf 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -429,6 +429,7 @@ main(int argc, char **argv)
 	bool		user_compression_defined = false;
 	bool		data_only = false;
 	bool		schema_only = false;
+	bool		statistics_only = false;
 	DataDirSyncMethod sync_method = DATA_DIR_SYNC_METHOD_FSYNC;
 
 	static DumpOptions dopt;
@@ -466,6 +467,7 @@ main(int argc, char **argv)
 		{"encoding", required_argument, NULL, 'E'},
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
+		{"statistics-only", no_argument, NULL, 'X'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -494,6 +496,7 @@ main(int argc, char **argv)
 		{"no-comments", no_argument, &dopt.no_comments, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -539,7 +542,7 @@ main(int argc, char **argv)
 
 	InitDumpOptions(&dopt);
 
-	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:RsS:t:T:U:vwWxZ:",
+	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:PRsS:t:T:U:vwWxXZ:",
 							long_options, &optindex)) != -1)
 	{
 		switch (c)
@@ -613,6 +616,10 @@ main(int argc, char **argv)
 				dopt.cparams.pgport = pg_strdup(optarg);
 				break;
 
+			case 'X':			/* Dump statistics only */
+				statistics_only = true;
+				break;
+
 			case 'R':
 				/* no-op, still accepted for backwards compatibility */
 				break;
@@ -784,6 +791,13 @@ main(int argc, char **argv)
 
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (schema_only && statistics_only)
+		pg_fatal("options -s/--schema-only and -X/--statistics-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
+
+	if (statistics_only && dopt.no_statistics)
+		pg_fatal("options -X/--statistics-only and --no-statistics cannot be used together");
 
 	if (schema_only && foreign_servers_include_patterns.head != NULL)
 		pg_fatal("options -s/--schema-only and --include-foreign-data cannot be used together");
@@ -798,8 +812,20 @@ main(int argc, char **argv)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
 	/* set derivative flags */
-	dopt.dumpSchema = (!data_only);
-	dopt.dumpData = (!schema_only);
+	dopt.dumpData = data_only || (!schema_only && !statistics_only);
+	dopt.dumpSchema = schema_only || (!data_only && !statistics_only);
+
+	if (statistics_only)
+		/* stats are only thing wanted */
+		dopt.dumpStatistics = true;
+	else if (dopt.no_statistics)
+		/* stats specifically excluded */
+		dopt.dumpStatistics = false;
+	else if (dopt.binary_upgrade)
+		/* binary upgrade and not specifically excluded */
+		dopt.dumpStatistics = true;
+	else
+		dopt.dumpStatistics = (!data_only && !schema_only);
 
 	/*
 	 * --inserts are already implied above if --column-inserts or
@@ -1099,6 +1125,7 @@ main(int argc, char **argv)
 	ropt->dropSchema = dopt.outputClean;
 	ropt->dumpData = dopt.dumpData;
 	ropt->dumpSchema = dopt.dumpSchema;
+	ropt->dumpStatistics = dopt.dumpStatistics;
 	ropt->if_exists = dopt.if_exists;
 	ropt->column_inserts = dopt.column_inserts;
 	ropt->dumpSections = dopt.dumpSections;
@@ -1177,7 +1204,7 @@ help(const char *progname)
 	printf(_("  -?, --help                   show this help, then exit\n"));
 
 	printf(_("\nOptions controlling the output content:\n"));
-	printf(_("  -a, --data-only              dump only the data, not the schema\n"));
+	printf(_("  -a, --data-only              dump only the data, not the schema or statistics\n"));
 	printf(_("  -b, --large-objects          include large objects in dump\n"));
 	printf(_("  --blobs                      (same as --large-objects, deprecated)\n"));
 	printf(_("  -B, --no-large-objects       exclude large objects in dump\n"));
@@ -1190,11 +1217,12 @@ help(const char *progname)
 	printf(_("  -N, --exclude-schema=PATTERN do NOT dump the specified schema(s)\n"));
 	printf(_("  -O, --no-owner               skip restoration of object ownership in\n"
 			 "                               plain-text format\n"));
-	printf(_("  -s, --schema-only            dump only the schema, no data\n"));
+	printf(_("  -s, --schema-only            dump only the schema, no data or statistics\n"));
 	printf(_("  -S, --superuser=NAME         superuser user name to use in plain-text format\n"));
 	printf(_("  -t, --table=PATTERN          dump only the specified table(s)\n"));
 	printf(_("  -T, --exclude-table=PATTERN  do NOT dump the specified table(s)\n"));
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
+	printf(_("  -X, --statistics-only        dump only the statistics, not schema or data\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
@@ -1221,6 +1249,7 @@ help(const char *progname)
 	printf(_("  --no-comments                do not dump comments\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -6780,6 +6809,42 @@ getFuncs(Archive *fout)
 	destroyPQExpBuffer(query);
 }
 
+/*
+ * getRelationStatistics
+ *    register the statistics object as a dependent of the relation.
+ *
+ */
+static RelStatsInfo *
+getRelationStatistics(Archive *fout, DumpableObject *rel, char relkind)
+{
+	if ((relkind == RELKIND_RELATION) ||
+		(relkind == RELKIND_PARTITIONED_TABLE) ||
+		(relkind == RELKIND_INDEX) ||
+		(relkind == RELKIND_PARTITIONED_INDEX) ||
+		(relkind == RELKIND_MATVIEW))
+	{
+		RelStatsInfo	   *info = pg_malloc0(sizeof(RelStatsInfo));
+		DumpableObject	   *dobj = &info->dobj;
+
+		dobj->objType = DO_REL_STATS;
+		dobj->catId.tableoid = 0;
+		dobj->catId.oid = 0;
+		AssignDumpId(dobj);
+		dobj->dependencies = (DumpId *) pg_malloc(sizeof(DumpId));
+		dobj->dependencies[0] = rel->dumpId;
+		dobj->nDeps = 1;
+		dobj->allocDeps = 1;
+		dobj->components |= DUMP_COMPONENT_STATISTICS;
+		dobj->dump = rel->dump;
+		dobj->name = pg_strdup(rel->name);
+		dobj->namespace = rel->namespace;
+		info->relkind = relkind;
+
+		return info;
+	}
+	return NULL;
+}
+
 /*
  * getTables
  *	  read all the tables (no indexes) in the system catalogs,
@@ -7157,6 +7222,7 @@ getTables(Archive *fout, int *numTables)
 
 		/* Tables have data */
 		tblinfo[i].dobj.components |= DUMP_COMPONENT_DATA;
+		tblinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
 
 		/* Mark whether table has an ACL */
 		if (!PQgetisnull(res, i, i_relacl))
@@ -7205,6 +7271,8 @@ getTables(Archive *fout, int *numTables)
 				}
 			}
 		}
+		if (tblinfo[i].interesting)
+			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relkind);
 	}
 
 	if (query->len != 0)
@@ -7649,11 +7717,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 		for (int c = 0; c < numinds; c++, j++)
 		{
 			char		contype;
+			char		indexkind;
+			RelStatsInfo   *relstats;
 
 			indxinfo[j].dobj.objType = DO_INDEX;
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -7676,7 +7747,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			{
 				NULL, NULL
 			};
+
+			if (indxinfo[j].parentidx == 0)
+				indexkind = RELKIND_INDEX;
+			else
+				indexkind = RELKIND_PARTITIONED_INDEX;
+
 			contype = *(PQgetvalue(res, j, i_contype));
+			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, indexkind);
 
 			if (contype == 'p' || contype == 'u' || contype == 'x')
 			{
@@ -7710,6 +7788,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				constrinfo->separate = true;
 
 				indxinfo[j].indexconstraint = constrinfo->dobj.dumpId;
+				if (relstats != NULL)
+					addObjectDependency(&relstats->dobj, constrinfo->dobj.dumpId);
 			}
 			else
 			{
@@ -10297,6 +10377,296 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * Tabular description of the parameters to pg_restore_relation_stats()
+ * required, param_name, param_type
+ */
+static const char *rel_stats_arginfo[][3] = {
+	{"f", "relation", "regclass"},
+	{"f", "version", "integer"},
+	{"f", "relpages", "integer"},
+	{"f", "reltuples", "real"},
+	{"f", "relallvisible", "integer"},
+};
+
+/*
+ * Tabular description of the parameters to pg_restore_attribute_stats()
+ * required, param_name, param_type
+ */
+static const char *att_stats_arginfo[][3] = {
+	{"f", "relation", "regclass"},
+	{"f", "attname", "name"},
+	{"f", "inherited", "boolean"},
+	{"f", "version", "integer"},
+	{"f", "null_frac", "float4"},
+	{"f", "avg_width", "integer"},
+	{"f", "n_distinct", "float4"},
+	{"f", "most_common_vals", "text"},
+	{"f", "most_common_freqs", "float4[]"},
+	{"f", "histogram_bounds", "text"},
+	{"f", "correlation", "float4"},
+	{"f", "most_common_elems", "text"},
+	{"f", "most_common_elem_freqs", "float4[]"},
+	{"f", "elem_count_histogram", "float4[]"},
+	{"f", "range_length_histogram", "text"},
+	{"f", "range_empty_frac", "float4"},
+	{"f", "range_bounds_histogram", "text"},
+};
+
+/*
+ * getRelStatsExportQuery --
+ *
+ * Generate a query that will fetch all relation (e.g. pg_class)
+ * stats for a given relation.
+ */
+static void
+getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "current_setting('server_version_num') AS version, "
+						 "c.relpages, c.reltuples, c.relallvisible "
+						 "FROM pg_class c "
+						 "JOIN pg_namespace n "
+						 "ON n.oid = c.relnamespace "
+						 "WHERE n.nspname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND c.relname = ");
+	appendStringLiteralAH(query, relname, fout);
+}
+
+/*
+ * getAttStatsExportQuery --
+ *
+ * Generate a query that will fetch all attribute (e.g. pg_statistic)
+ * stats for a given relation.
+ */
+static void
+getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "s.attname,"
+						 "s.inherited,"
+						 "current_setting('server_version_num') AS version, "
+						 "s.null_frac,"
+						 "s.avg_width,"
+						 "s.n_distinct,"
+						 "s.most_common_vals,"
+						 "s.most_common_freqs,"
+						 "s.histogram_bounds,"
+						 "s.correlation,"
+						 "s.most_common_elems,"
+						 "s.most_common_elem_freqs,"
+						 "s.elem_count_histogram,");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 "s.range_length_histogram,"
+							 "s.range_empty_frac,"
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(query,
+							 "NULL AS range_length_histogram,"
+							 "NULL AS range_empty_frac,"
+							 "NULL AS range_bounds_histogram ");
+
+	appendPQExpBufferStr(query,
+						 "FROM pg_stats s "
+						 "JOIN pg_namespace n "
+						 "ON n.nspname = s.schemaname "
+						 "JOIN pg_class c "
+						 "ON c.relname = s.tablename "
+						 "AND c.relnamespace = n.oid "
+						 "WHERE s.schemaname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND s.tablename = ");
+	appendStringLiteralAH(query, relname, fout);
+	appendPQExpBufferStr(query, " ORDER BY s.attname, s.inherited");
+}
+
+
+/*
+ * appendNamedArgument --
+ *
+ * Convenience routine for constructing parameters of the form:
+ * 'paraname', 'value'::type
+ */
+static void
+appendNamedArgument(PQExpBuffer out, Archive *fout,
+					const char *argname, bool positional,
+					const char *argval, const char *argtype)
+{
+	appendPQExpBufferStr(out, "\t");
+
+	if (!positional)
+	{
+		appendStringLiteralAH(out, argname, fout);
+		appendPQExpBufferStr(out, ", ");
+	}
+
+	appendStringLiteralAH(out, argval, fout);
+	appendPQExpBuffer(out, "::%s", argtype);
+}
+
+/*
+ * appendRelStatsImport --
+ *
+ * Append a formatted pg_restore_relation_stats statement.
+ */
+static void
+appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	const char *sep = "";
+
+	if (PQntuples(res) == 0)
+		return;
+
+	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
+
+	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
+	{
+		bool		positional = (rel_stats_arginfo[argno][0][0] == 't');
+		const char *argname = rel_stats_arginfo[argno][1];
+		const char *argtype = rel_stats_arginfo[argno][2];
+		int			fieldno = PQfnumber(res, argname);
+
+		if (fieldno < 0)
+			pg_fatal("relation stats export query missing field '%s'",
+					 argname);
+
+		if (PQgetisnull(res, 0, fieldno))
+		{
+			if (!positional)
+				pg_fatal("relation stats export query unexpected NULL in '%s'",
+						 argname);
+			else
+				continue;
+		}
+
+		appendPQExpBufferStr(out, sep);
+		appendNamedArgument(out, fout, argname, positional,
+							PQgetvalue(res, 0, fieldno), argtype);
+
+		sep = ",\n";
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+/*
+ * appendAttStatsImport --
+ *
+ * Append a series of formatted pg_restore_attribute_stats statements.
+ */
+static void
+appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	{
+		const char *sep = "";
+
+		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+		for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
+		{
+			bool		positional = (att_stats_arginfo[argno][0][0] == 't');
+			const char *argname = att_stats_arginfo[argno][1];
+			const char *argtype = att_stats_arginfo[argno][2];
+			int			fieldno = PQfnumber(res, argname);
+
+			if (fieldno < 0)
+				pg_fatal("attribute stats export query missing field '%s'",
+						 argname);
+
+			if (PQgetisnull(res, rownum, fieldno))
+			{
+				if (positional)
+					pg_fatal("attribute stats export query unexpected NULL in '%s'",
+							 argname);
+				else
+					continue;
+			}
+
+			appendPQExpBufferStr(out, sep);
+			appendNamedArgument(out, fout, argname, positional,
+								PQgetvalue(res, rownum, fieldno), argtype);
+			sep = ",\n";
+		}
+		appendPQExpBufferStr(out, "\n);\n");
+	}
+}
+
+/*
+ * get statistics dump section, which depends on the parent object type
+ *
+ * objects created in SECTION_PRE_DATA have stats in SECTION_DATA
+ * objects created in SECTION_POST_DATA have stats in SECTION_POST_DATA
+ */
+static teSection
+statisticsDumpSection(const RelStatsInfo *rsinfo)
+{
+
+	if ((rsinfo->relkind == RELKIND_MATVIEW) ||
+		(rsinfo->relkind == RELKIND_INDEX) ||
+		(rsinfo->relkind == RELKIND_PARTITIONED_INDEX))
+			return SECTION_POST_DATA;
+
+	return SECTION_DATA;
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	PGresult	   *res;
+	PQExpBuffer		query;
+	PQExpBuffer		out;
+	PQExpBuffer		tag;
+	DumpableObject *dobj = (DumpableObject *) &rsinfo->dobj;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", "STATISTICS DATA", fmtId(dobj->name));
+
+	query = createPQExpBuffer();
+	out = createPQExpBuffer();
+
+	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendRelStatsImport(out, fout, res);
+	PQclear(res);
+
+	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendAttStatsImport(out, fout, res);
+	PQclear(res);
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATISTICS DATA",
+							  .section = statisticsDumpSection(rsinfo),
+							  .createStmt = out->data,
+							  .deps = dobj->dependencies,
+							  .nDeps = dobj->nDeps));
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(out);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -10745,6 +11115,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_SUBSCRIPTION_REL:
 			dumpSubscriptionTable(fout, (const SubRelInfo *) dobj);
 			break;
+		case DO_REL_STATS:
+			dumpRelationStats(fout, (const RelStatsInfo *) dobj);
+			break;
 		case DO_PRE_DATA_BOUNDARY:
 		case DO_POST_DATA_BOUNDARY:
 			/* never dumped, nothing to do */
@@ -17173,6 +17546,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 		free(indstatvalsarray);
 	}
 
+	/* Comments and stats share same .dep */
+
 	/* Dump Index Comments */
 	if (indxinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
 		dumpComment(fout, "INDEX", qindxname,
@@ -18960,6 +19335,16 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 				/* must come after the pre-data boundary */
 				addObjectDependency(dobj, preDataBound->dumpId);
 				break;
+			case DO_REL_STATS:
+				/* stats section varies by parent object type, DATA or POST */
+				if (statisticsDumpSection((RelStatsInfo *) dobj) == SECTION_DATA)
+				{
+					addObjectDependency(dobj, preDataBound->dumpId);
+					addObjectDependency(postDataBound, dobj->dumpId);
+				}
+				else
+					addObjectDependency(dobj, postDataBound->dumpId);
+				break;
 		}
 	}
 }
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index d65f558565..ef555e9178 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -84,6 +84,7 @@ typedef enum
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION,
 	DO_SUBSCRIPTION_REL,		/* see note for SubRelInfo */
+	DO_REL_STATS,
 } DumpableObjectType;
 
 /*
@@ -101,6 +102,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
@@ -421,6 +423,12 @@ typedef struct _indexAttachInfo
 	IndxInfo   *partitionIdx;	/* link to index on partition */
 } IndexAttachInfo;
 
+typedef struct _relStatsInfo
+{
+	DumpableObject	dobj;
+	char			relkind;		/* 'r', 'v', 'c', etc */
+} RelStatsInfo;
+
 typedef struct _statsExtInfo
 {
 	DumpableObject dobj;
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 3c8f2eb808..989d20aa27 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -1500,6 +1500,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "POST-DATA BOUNDARY  (ID %d)",
 					 obj->dumpId);
 			return;
+		case DO_REL_STATS:
+			snprintf(buf, bufsize,
+					 "RELATION STATISTICS FOR %s  (ID %d OID %u)",
+					 obj->name, obj->dumpId, obj->catId.oid);
+			return;
 	}
 	/* shouldn't get here */
 	snprintf(buf, bufsize,
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index e3ad8fb295..1799a03ff1 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -103,6 +103,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -172,6 +173,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -451,6 +453,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -666,6 +670,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index d3b6debcab..f8f16facc1 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -63,6 +63,7 @@ main(int argc, char **argv)
 	int			numWorkers = 1;
 	Archive    *AH;
 	char	   *inputFileSpec;
+	bool		statistics_only = false;
 	static int	disable_triggers = 0;
 	static int	enable_row_security = 0;
 	static int	if_exists = 0;
@@ -74,6 +75,7 @@ main(int argc, char **argv)
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
 	static int	no_subscriptions = 0;
+	static int  no_statistics = 0;
 	static int	strict_names = 0;
 
 	bool		data_only = false;
@@ -109,6 +111,7 @@ main(int argc, char **argv)
 		{"username", 1, NULL, 'U'},
 		{"verbose", 0, NULL, 'v'},
 		{"single-transaction", 0, NULL, '1'},
+		{"statistics-only", no_argument, NULL, 'P'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -129,6 +132,7 @@ main(int argc, char **argv)
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
+		{"no-statistics", no_argument, &no_statistics, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -272,6 +276,10 @@ main(int argc, char **argv)
 				opts->aclsSkip = 1;
 				break;
 
+			case 'X':			/* Restore statistics only */
+				statistics_only = true;
+				break;
+
 			case '1':			/* Restore data in a single transaction */
 				opts->single_txn = true;
 				opts->exit_on_error = true;
@@ -344,6 +352,10 @@ main(int argc, char **argv)
 
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
+	if (schema_only && statistics_only)
+		pg_fatal("options -s/--schema-only and -X/--statistics-only cannot be used together");
 
 	if (data_only && opts->dropSchema)
 		pg_fatal("options -c/--clean and -a/--data-only cannot be used together");
@@ -363,8 +375,9 @@ main(int argc, char **argv)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
 	/* set derivative flags */
-	opts->dumpSchema = (!data_only);
-	opts->dumpData = (!schema_only);
+	opts->dumpSchema = (!data_only && !statistics_only);
+	opts->dumpData = (!schema_only && !statistics_only);
+	opts->dumpStatistics = (!no_statistics && !data_only && !schema_only);
 
 	opts->disable_triggers = disable_triggers;
 	opts->enable_row_security = enable_row_security;
@@ -376,6 +389,7 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
diff --git a/src/bin/pg_dump/t/001_basic.pl b/src/bin/pg_dump/t/001_basic.pl
index b9d13a0e1d..76ebf15f55 100644
--- a/src/bin/pg_dump/t/001_basic.pl
+++ b/src/bin/pg_dump/t/001_basic.pl
@@ -50,12 +50,30 @@ command_fails_like(
 	'pg_dump: options -s/--schema-only and -a/--data-only cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '-s', '-X' ],
+	qr/\Qpg_dump: error: options -s\/--schema-only and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: error: options -s/--schema-only and -X/--statistics-only cannot be used together'
+);
+
+command_fails_like(
+	[ 'pg_dump', '-a', '-X' ],
+	qr/\Qpg_dump: error: options -a\/--data-only and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: error: options -a/--data-only and -X/--statistics-only cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-s', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: options -s\/--schema-only and --include-foreign-data cannot be used together\E/,
 	'pg_dump: options -s/--schema-only and --include-foreign-data cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '--statistics-only', '--no-statistics' ],
+	qr/\Qpg_dump: error: options -X\/--statistics-only and --no-statistics cannot be used together\E/,
+	'pg_dump: options -X\/--statistics-only and --no-statistics cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-j2', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: option --include-foreign-data is not supported with parallel backup\E/,
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index d66e901f51..aa4785c612 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -123,7 +123,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Dump only the data, not the schema (data definitions).
+        Dump only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are dumped.
        </para>
 
@@ -141,8 +141,9 @@ PostgreSQL documentation
       <listitem>
        <para>
         Include large objects in the dump.  This is the default behavior
-        except when <option>--schema</option>, <option>--table</option>, or
-        <option>--schema-only</option> is specified.  The <option>-b</option>
+        except when <option>--schema</option>, <option>--table</option>,
+        <option>--schema-only</option>, or
+        <option>--statistics-only</option> is specified.  The <option>-b</option>
         switch is therefore only useful to add large objects to dumps
         where a specific schema or table has been requested.  Note that
         large objects are considered data and therefore will be included when
@@ -516,10 +517,11 @@ PostgreSQL documentation
       <term><option>--schema-only</option></term>
       <listitem>
        <para>
-        Dump only the object definitions (schema), not data.
+        Dump only the object definitions (schema), not data or statistics.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive to <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
         <option>--section=pre-data --section=post-data</option>.
@@ -652,6 +654,18 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Dump only the statistics, not the schema (data definitions) or data.
+        Statistics for tables, materialized views, and indexes are dumped.
+       </para>
+
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-Z <replaceable class="parameter">level</replaceable></option></term>
       <term><option>-Z <replaceable class="parameter">method</replaceable></option>[:<replaceable>detail</replaceable>]</term>
@@ -833,7 +847,8 @@ PostgreSQL documentation
         though you do not need the data in it.
        </para>
        <para>
-        To exclude data for all tables in the database, see <option>--schema-only</option>.
+        To exclude data for all tables in the database, see <option>--schema-only</option>
+        or <option>--statistics-only</option>.
        </para>
       </listitem>
      </varlistentry>
@@ -1098,6 +1113,15 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index b8b27e1719..ff1441a243 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -94,7 +94,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Restore only the data, not the schema (data definitions).
+        Restore only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are restored,
         if present in the archive.
        </para>
@@ -483,10 +483,11 @@ PostgreSQL documentation
         to the extent that schema entries are present in the archive.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive of <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
-        <option>--section=pre-data --section=post-data</option>.
+        <option>--section=pre-data --section=post-data --no-statistics</option>.
        </para>
        <para>
         (Do not confuse this with the <option>--schema</option> option, which
@@ -599,6 +600,20 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Restore only the statistics, not schema or data.
+       </para>
+       <para>
+        (Do not confuse this with the <option>--schema</option> option, which
+        uses the word <quote>schema</quote> in a different meaning.)
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-1</option></term>
       <term><option>--single-transaction</option></term>
@@ -723,6 +738,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore statistics, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
-- 
2.47.0

v32-0004-Enable-in-place-updates-for-pg_restore_relation_.patchtext/x-patch; charset=US-ASCII; name=v32-0004-Enable-in-place-updates-for-pg_restore_relation_.patchDownload
From 747cadd6db9619634b863a395fdba1614ad92a55 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Mon, 4 Nov 2024 14:02:57 -0500
Subject: [PATCH v32 04/14] Enable in-place updates for
 pg_restore_relation_stats.

This matches the behavior of the ANALYZE command, and would avoid
bloating pg_class in an upgrade situation wherein
pg_restore_relation_stats would be called for nearly every relation in
the database.
---
 src/backend/statistics/relation_stats.c | 72 +++++++++++++++++++------
 1 file changed, 55 insertions(+), 17 deletions(-)

diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index ed5dea2e05..939ad56fc2 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -22,6 +22,7 @@
 #include "statistics/stat_utils.h"
 #include "utils/fmgrprotos.h"
 #include "utils/syscache.h"
+#include "utils/fmgroids.h"
 
 #define DEFAULT_RELPAGES Int32GetDatum(0)
 #define DEFAULT_RELTUPLES Float4GetDatum(-1.0)
@@ -50,13 +51,14 @@ static struct StatsArgInfo relarginfo[] =
 	[NUM_RELATION_STATS_ARGS] = {0}
 };
 
-static bool relation_statistics_update(FunctionCallInfo fcinfo, int elevel);
+static bool relation_statistics_update(FunctionCallInfo fcinfo, int elevel,
+									   bool inplace);
 
 /*
  * Internal function for modifying statistics for a relation.
  */
 static bool
-relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
+relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
 {
 	Oid			reloid;
 	Relation	crel;
@@ -68,6 +70,7 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	int			ncols = 0;
 	TupleDesc	tupdesc;
 	bool		result = true;
+	void	   *inplace_state;
 
 	stats_check_required_arg(fcinfo, relarginfo, RELATION_ARG);
 	reloid = PG_GETARG_OID(RELATION_ARG);
@@ -81,7 +84,20 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	crel = table_open(RelationRelationId, RowExclusiveLock);
 
 	tupdesc = RelationGetDescr(crel);
-	ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(reloid));
+	if (inplace)
+	{
+		ScanKeyData key[1];
+
+		ctup = NULL;
+
+		ScanKeyInit(&key[0], Anum_pg_class_oid, BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(reloid));
+		systable_inplace_update_begin(crel, ClassOidIndexId, true, NULL, 1, key,
+									  &ctup, &inplace_state);
+	}
+	else
+		ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(reloid));
+
 	if (!HeapTupleIsValid(ctup))
 	{
 		ereport(elevel,
@@ -112,8 +128,13 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 		}
 		else if (relpages != pgcform->relpages)
 		{
-			replaces[ncols] = Anum_pg_class_relpages;
-			values[ncols] = Int32GetDatum(relpages);
+			if (inplace)
+				pgcform->relpages = relpages;
+			else
+			{
+				replaces[ncols] = Anum_pg_class_relpages;
+				values[ncols] = Int32GetDatum(relpages);
+			}
 			ncols++;
 		}
 	}
@@ -131,8 +152,13 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 		}
 		else if (reltuples != pgcform->reltuples)
 		{
-			replaces[ncols] = Anum_pg_class_reltuples;
-			values[ncols] = Float4GetDatum(reltuples);
+			if (inplace)
+				pgcform->reltuples = reltuples;
+			else
+			{
+				replaces[ncols] = Anum_pg_class_reltuples;
+				values[ncols] = Float4GetDatum(reltuples);
+			}
 			ncols++;
 		}
 
@@ -151,8 +177,13 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 		}
 		else if (relallvisible != pgcform->relallvisible)
 		{
-			replaces[ncols] = Anum_pg_class_relallvisible;
-			values[ncols] = Int32GetDatum(relallvisible);
+			if (inplace)
+				pgcform->relallvisible = relallvisible;
+			else
+			{
+				replaces[ncols] = Anum_pg_class_relallvisible;
+				values[ncols] = Int32GetDatum(relallvisible);
+			}
 			ncols++;
 		}
 	}
@@ -160,13 +191,20 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	/* only update pg_class if there is a meaningful change */
 	if (ncols > 0)
 	{
-		HeapTuple	newtup;
+		if (inplace)
+			systable_inplace_update_finish(inplace_state, ctup);
+		else
+		{
+			HeapTuple	newtup;
 
-		newtup = heap_modify_tuple_by_cols(ctup, tupdesc, ncols, replaces, values,
-										   nulls);
-		CatalogTupleUpdate(crel, &newtup->t_self, newtup);
-		heap_freetuple(newtup);
+			newtup = heap_modify_tuple_by_cols(ctup, tupdesc, ncols, replaces, values,
+											nulls);
+			CatalogTupleUpdate(crel, &newtup->t_self, newtup);
+			heap_freetuple(newtup);
+		}
 	}
+	else if (inplace)
+		systable_inplace_update_cancel(inplace_state);
 
 	/* release the lock, consistent with vac_update_relstats() */
 	table_close(crel, RowExclusiveLock);
@@ -182,7 +220,7 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 Datum
 pg_set_relation_stats(PG_FUNCTION_ARGS)
 {
-	relation_statistics_update(fcinfo, ERROR);
+	relation_statistics_update(fcinfo, ERROR, false);
 	PG_RETURN_VOID();
 }
 
@@ -206,7 +244,7 @@ pg_clear_relation_stats(PG_FUNCTION_ARGS)
 	newfcinfo->args[3].value = DEFAULT_RELALLVISIBLE;
 	newfcinfo->args[3].isnull = false;
 
-	relation_statistics_update(newfcinfo, ERROR);
+	relation_statistics_update(newfcinfo, ERROR, false);
 	PG_RETURN_VOID();
 }
 
@@ -224,7 +262,7 @@ pg_restore_relation_stats(PG_FUNCTION_ARGS)
 										  relarginfo, WARNING))
 		result = false;
 
-	if (!relation_statistics_update(positional_fcinfo, WARNING))
+	if (!relation_statistics_update(positional_fcinfo, WARNING, true))
 		result = false;
 
 	PG_RETURN_BOOL(result);
-- 
2.47.0

v32-0002-Remove-schemaOnly-dataOnly-from-dump-restore-opt.patchtext/x-patch; charset=US-ASCII; name=v32-0002-Remove-schemaOnly-dataOnly-from-dump-restore-opt.patchDownload
From b08b2a8b571885fc5d26be9758d06cfbb75214de Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Fri, 1 Nov 2024 11:56:34 -0400
Subject: [PATCH v32 02/14] Remove schemaOnly, dataOnly from dump/restore
 options structures.

The user-facing flags still exist, but the results of those flags are
already resolved into dumpSchema and dumpData.

All logic about which objects to dump which previously used schemaOnly
and dataOnly in the negative (ex. we should dump large objects because
schemaOnly is NOT set), which would have gotten unweildly when a third
option (statisticsOnly) was added.
---
 src/bin/pg_dump/pg_backup.h          |  4 ----
 src/bin/pg_dump/pg_backup_archiver.c | 26 +++++++++++++-------------
 src/bin/pg_dump/pg_dump.c            | 26 ++++++++++++++------------
 src/bin/pg_dump/pg_restore.c         | 15 +++++++++------
 4 files changed, 36 insertions(+), 35 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 9bd3266a63..f74e848714 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -116,8 +116,6 @@ typedef struct _restoreOptions
 	int			strict_names;
 
 	const char *filename;
-	int			dataOnly;
-	int			schemaOnly;
 	int			dumpSections;
 	int			verbose;
 	int			aclsSkip;
@@ -171,8 +169,6 @@ typedef struct _dumpOptions
 	int			binary_upgrade;
 
 	/* various user-settable parameters */
-	bool		schemaOnly;
-	bool		dataOnly;
 	int			dumpSections;	/* bitmask of chosen sections */
 	bool		aclsSkip;
 	const char *lockWaitTimeout;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 8c20c263c4..32c8e588f5 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -165,8 +165,8 @@ dumpOptionsFromRestoreOptions(RestoreOptions *ropt)
 	dopt->cparams.username = ropt->cparams.username ? pg_strdup(ropt->cparams.username) : NULL;
 	dopt->cparams.promptPassword = ropt->cparams.promptPassword;
 	dopt->outputClean = ropt->dropSchema;
-	dopt->dataOnly = ropt->dataOnly;
-	dopt->schemaOnly = ropt->schemaOnly;
+	dopt->dumpData = ropt->dumpData;
+	dopt->dumpSchema = ropt->dumpSchema;
 	dopt->if_exists = ropt->if_exists;
 	dopt->column_inserts = ropt->column_inserts;
 	dopt->dumpSections = ropt->dumpSections;
@@ -419,12 +419,12 @@ RestoreArchive(Archive *AHX)
 	 * Work out if we have an implied data-only restore. This can happen if
 	 * the dump was data only or if the user has used a toc list to exclude
 	 * all of the schema data. All we do is look for schema entries - if none
-	 * are found then we set the dataOnly flag.
+	 * are found then we set the data-only flag.
 	 *
 	 * We could scan for wanted TABLE entries, but that is not the same as
-	 * dataOnly. At this stage, it seems unnecessary (6-Mar-2001).
+	 * data-only. At this stage, it seems unnecessary (6-Mar-2001).
 	 */
-	if (!ropt->dataOnly)
+	if (ropt->dumpSchema)
 	{
 		int			impliedDataOnly = 1;
 
@@ -438,7 +438,7 @@ RestoreArchive(Archive *AHX)
 		}
 		if (impliedDataOnly)
 		{
-			ropt->dataOnly = impliedDataOnly;
+			ropt->dumpSchema = false;
 			pg_log_info("implied data-only restore");
 		}
 	}
@@ -824,7 +824,7 @@ restore_toc_entry(ArchiveHandle *AH, TocEntry *te, bool is_parallel)
 	/* Dump any relevant dump warnings to stderr */
 	if (!ropt->suppressDumpWarnings && strcmp(te->desc, "WARNING") == 0)
 	{
-		if (!ropt->dataOnly && te->defn != NULL && strlen(te->defn) != 0)
+		if (ropt->dumpSchema && te->defn != NULL && strlen(te->defn) != 0)
 			pg_log_warning("warning from original dump file: %s", te->defn);
 		else if (te->copyStmt != NULL && strlen(te->copyStmt) != 0)
 			pg_log_warning("warning from original dump file: %s", te->copyStmt);
@@ -1090,7 +1090,7 @@ _disableTriggersIfNecessary(ArchiveHandle *AH, TocEntry *te)
 	RestoreOptions *ropt = AH->public.ropt;
 
 	/* This hack is only needed in a data-only restore */
-	if (!ropt->dataOnly || !ropt->disable_triggers)
+	if (ropt->dumpSchema || !ropt->disable_triggers)
 		return;
 
 	pg_log_info("disabling triggers for %s", te->tag);
@@ -1116,7 +1116,7 @@ _enableTriggersIfNecessary(ArchiveHandle *AH, TocEntry *te)
 	RestoreOptions *ropt = AH->public.ropt;
 
 	/* This hack is only needed in a data-only restore */
-	if (!ropt->dataOnly || !ropt->disable_triggers)
+	if (ropt->dumpSchema || !ropt->disable_triggers)
 		return;
 
 	pg_log_info("enabling triggers for %s", te->tag);
@@ -3148,12 +3148,12 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 		return 0;
 
 	/* Mask it if we only want schema */
-	if (ropt->schemaOnly)
+	if (!ropt->dumpData)
 	{
 		/*
-		 * The sequence_data option overrides schemaOnly for SEQUENCE SET.
+		 * The sequence_data option overrides schema-only for SEQUENCE SET.
 		 *
-		 * In binary-upgrade mode, even with schemaOnly set, we do not mask
+		 * In binary-upgrade mode, even with schema-only set, we do not mask
 		 * out large objects.  (Only large object definitions, comments and
 		 * other metadata should be generated in binary-upgrade mode, not the
 		 * actual data, but that need not concern us here.)
@@ -3172,7 +3172,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	}
 
 	/* Mask it if we only want data */
-	if (ropt->dataOnly)
+	if (!ropt->dumpSchema)
 		res = res & REQ_DATA;
 
 	return res;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 7ce1c7a9ce..46e1da1856 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -427,6 +427,8 @@ main(int argc, char **argv)
 	char	   *compression_algorithm_str = "none";
 	char	   *error_detail = NULL;
 	bool		user_compression_defined = false;
+	bool		data_only = false;
+	bool		schema_only = false;
 	DataDirSyncMethod sync_method = DATA_DIR_SYNC_METHOD_FSYNC;
 
 	static DumpOptions dopt;
@@ -543,7 +545,7 @@ main(int argc, char **argv)
 		switch (c)
 		{
 			case 'a':			/* Dump data only */
-				dopt.dataOnly = true;
+				data_only = true;
 				break;
 
 			case 'b':			/* Dump LOs */
@@ -616,7 +618,7 @@ main(int argc, char **argv)
 				break;
 
 			case 's':			/* dump schema only */
-				dopt.schemaOnly = true;
+				schema_only = true;
 				break;
 
 			case 'S':			/* Username for superuser in plain text output */
@@ -780,24 +782,24 @@ main(int argc, char **argv)
 	if (dopt.binary_upgrade)
 		dopt.sequence_data = 1;
 
-	if (dopt.dataOnly && dopt.schemaOnly)
+	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
 
-	if (dopt.schemaOnly && foreign_servers_include_patterns.head != NULL)
+	if (schema_only && foreign_servers_include_patterns.head != NULL)
 		pg_fatal("options -s/--schema-only and --include-foreign-data cannot be used together");
 
 	if (numWorkers > 1 && foreign_servers_include_patterns.head != NULL)
 		pg_fatal("option --include-foreign-data is not supported with parallel backup");
 
-	if (dopt.dataOnly && dopt.outputClean)
+	if (data_only && dopt.outputClean)
 		pg_fatal("options -c/--clean and -a/--data-only cannot be used together");
 
 	if (dopt.if_exists && !dopt.outputClean)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
 	/* set derivative flags */
-	dopt.dumpSchema = (!dopt.dataOnly);
-	dopt.dumpData = (!dopt.schemaOnly);
+	dopt.dumpSchema = (!data_only);
+	dopt.dumpData = (!schema_only);
 
 	/*
 	 * --inserts are already implied above if --column-inserts or
@@ -1095,8 +1097,8 @@ main(int argc, char **argv)
 	ropt->cparams.username = dopt.cparams.username ? pg_strdup(dopt.cparams.username) : NULL;
 	ropt->cparams.promptPassword = dopt.cparams.promptPassword;
 	ropt->dropSchema = dopt.outputClean;
-	ropt->dataOnly = dopt.dataOnly;
-	ropt->schemaOnly = dopt.schemaOnly;
+	ropt->dumpData = dopt.dumpData;
+	ropt->dumpSchema = dopt.dumpSchema;
 	ropt->if_exists = dopt.if_exists;
 	ropt->column_inserts = dopt.column_inserts;
 	ropt->dumpSections = dopt.dumpSections;
@@ -1991,7 +1993,7 @@ selectDumpableType(TypeInfo *tyinfo, Archive *fout)
  *		Mark a default ACL as to be dumped or not
  *
  * For per-schema default ACLs, dump if the schema is to be dumped.
- * Otherwise dump if we are dumping "everything".  Note that dataOnly
+ * Otherwise dump if we are dumping "everything".  Note that data-only
  * and aclsSkip are checked separately.
  */
 static void
@@ -17666,7 +17668,7 @@ collectSequences(Archive *fout)
 	if (fout->remoteVersion < 100000)
 		return;
 	else if (fout->remoteVersion < 180000 ||
-			 (fout->dopt->schemaOnly && !fout->dopt->sequence_data))
+			 (!fout->dopt->dumpData && !fout->dopt->sequence_data))
 		query = "SELECT seqrelid, format_type(seqtypid, NULL), "
 			"seqstart, seqincrement, "
 			"seqmax, seqmin, "
@@ -18533,7 +18535,7 @@ processExtensionTables(Archive *fout, ExtensionInfo extinfo[],
 	 * objects for them, ensuring their data will be dumped even though the
 	 * tables themselves won't be.
 	 *
-	 * Note that we create TableDataInfo objects even in schemaOnly mode, ie,
+	 * Note that we create TableDataInfo objects even in schema-only mode, ie,
 	 * user data in a configuration table is treated like schema data. This
 	 * seems appropriate since system data in a config table would get
 	 * reloaded by CREATE EXTENSION.  If the extension is not listed in the
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index d2db23c75a..d3b6debcab 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -76,6 +76,9 @@ main(int argc, char **argv)
 	static int	no_subscriptions = 0;
 	static int	strict_names = 0;
 
+	bool		data_only = false;
+	bool		schema_only = false;
+
 	struct option cmdopts[] = {
 		{"clean", 0, NULL, 'c'},
 		{"create", 0, NULL, 'C'},
@@ -160,7 +163,7 @@ main(int argc, char **argv)
 		switch (c)
 		{
 			case 'a':			/* Dump data only */
-				opts->dataOnly = 1;
+				data_only = true;
 				break;
 			case 'c':			/* clean (i.e., drop) schema prior to create */
 				opts->dropSchema = 1;
@@ -236,7 +239,7 @@ main(int argc, char **argv)
 				simple_string_list_append(&opts->triggerNames, optarg);
 				break;
 			case 's':			/* dump schema only */
-				opts->schemaOnly = 1;
+				schema_only = true;
 				break;
 			case 'S':			/* Superuser username */
 				if (strlen(optarg) != 0)
@@ -339,10 +342,10 @@ main(int argc, char **argv)
 		opts->useDB = 1;
 	}
 
-	if (opts->dataOnly && opts->schemaOnly)
+	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
 
-	if (opts->dataOnly && opts->dropSchema)
+	if (data_only && opts->dropSchema)
 		pg_fatal("options -c/--clean and -a/--data-only cannot be used together");
 
 	if (opts->single_txn && opts->txn_size > 0)
@@ -360,8 +363,8 @@ main(int argc, char **argv)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
 	/* set derivative flags */
-	opts->dumpSchema = (opts->dataOnly != 1);
-	opts->dumpData = (opts->schemaOnly != 1);
+	opts->dumpSchema = (!data_only);
+	opts->dumpData = (!schema_only);
 
 	opts->disable_triggers = disable_triggers;
 	opts->enable_row_security = enable_row_security;
-- 
2.47.0

v32-0005-Enable-pg_clear_relation_stats-to-handle-differe.patchtext/x-patch; charset=US-ASCII; name=v32-0005-Enable-pg_clear_relation_stats-to-handle-differe.patchDownload
From c8513985b8bd62fd37be3314443a6a6fe8791dee Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Mon, 4 Nov 2024 16:21:39 -0500
Subject: [PATCH v32 05/14] Enable pg_clear_relation_stats to handle different
 default relpages.

If it comes to pass that relpages has the default of -1.0 for
partitioned tables (and indexes), then this patch will handle that.
---
 src/backend/statistics/relation_stats.c | 47 +++++++++++++------------
 1 file changed, 24 insertions(+), 23 deletions(-)

diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index 939ad56fc2..1aab63f5d8 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -19,15 +19,12 @@
 
 #include "access/heapam.h"
 #include "catalog/indexing.h"
+#include "catalog/pg_class_d.h"
 #include "statistics/stat_utils.h"
 #include "utils/fmgrprotos.h"
 #include "utils/syscache.h"
 #include "utils/fmgroids.h"
 
-#define DEFAULT_RELPAGES Int32GetDatum(0)
-#define DEFAULT_RELTUPLES Float4GetDatum(-1.0)
-#define DEFAULT_RELALLVISIBLE Int32GetDatum(0)
-
 /*
  * Positional argument numbers, names, and types for
  * relation_statistics_update().
@@ -51,14 +48,11 @@ static struct StatsArgInfo relarginfo[] =
 	[NUM_RELATION_STATS_ARGS] = {0}
 };
 
-static bool relation_statistics_update(FunctionCallInfo fcinfo, int elevel,
-									   bool inplace);
-
 /*
  * Internal function for modifying statistics for a relation.
  */
 static bool
-relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
+relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace, bool clear)
 {
 	Oid			reloid;
 	Relation	crel;
@@ -110,9 +104,19 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
 	pgcform = (Form_pg_class) GETSTRUCT(ctup);
 
 	/* relpages */
-	if (!PG_ARGISNULL(RELPAGES_ARG))
+	if (!PG_ARGISNULL(RELPAGES_ARG) || clear)
 	{
-		int32		relpages = PG_GETARG_INT32(RELPAGES_ARG);
+		int32		relpages;
+
+		if (clear)
+			/* relpages default varies by relkind */
+			if ((crel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) ||
+				(crel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+				relpages = -1;
+			else
+				relpages = 0;
+		else
+			relpages = PG_GETARG_INT32(RELPAGES_ARG);
 
 		/*
 		 * Partitioned tables may have relpages=-1. Note: for relations with
@@ -139,9 +143,9 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
 		}
 	}
 
-	if (!PG_ARGISNULL(RELTUPLES_ARG))
+	if (!PG_ARGISNULL(RELTUPLES_ARG) || clear)
 	{
-		float		reltuples = PG_GETARG_FLOAT4(RELTUPLES_ARG);
+		float		reltuples = (clear) ? -1.0 : PG_GETARG_FLOAT4(RELTUPLES_ARG);
 
 		if (reltuples < -1.0)
 		{
@@ -164,9 +168,9 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
 
 	}
 
-	if (!PG_ARGISNULL(RELALLVISIBLE_ARG))
+	if (!PG_ARGISNULL(RELALLVISIBLE_ARG) || clear)
 	{
-		int32		relallvisible = PG_GETARG_INT32(RELALLVISIBLE_ARG);
+		int32		relallvisible = (clear) ? 0 : PG_GETARG_INT32(RELALLVISIBLE_ARG);
 
 		if (relallvisible < 0)
 		{
@@ -220,7 +224,7 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
 Datum
 pg_set_relation_stats(PG_FUNCTION_ARGS)
 {
-	relation_statistics_update(fcinfo, ERROR, false);
+	relation_statistics_update(fcinfo, ERROR, false, false);
 	PG_RETURN_VOID();
 }
 
@@ -237,14 +241,11 @@ pg_clear_relation_stats(PG_FUNCTION_ARGS)
 
 	newfcinfo->args[0].value = PG_GETARG_OID(0);
 	newfcinfo->args[0].isnull = PG_ARGISNULL(0);
-	newfcinfo->args[1].value = DEFAULT_RELPAGES;
-	newfcinfo->args[1].isnull = false;
-	newfcinfo->args[2].value = DEFAULT_RELTUPLES;
-	newfcinfo->args[2].isnull = false;
-	newfcinfo->args[3].value = DEFAULT_RELALLVISIBLE;
-	newfcinfo->args[3].isnull = false;
+	newfcinfo->args[1].isnull = true;
+	newfcinfo->args[2].isnull = true;
+	newfcinfo->args[3].isnull = true;
 
-	relation_statistics_update(newfcinfo, ERROR, false);
+	relation_statistics_update(newfcinfo, ERROR, false, true);
 	PG_RETURN_VOID();
 }
 
@@ -262,7 +263,7 @@ pg_restore_relation_stats(PG_FUNCTION_ARGS)
 										  relarginfo, WARNING))
 		result = false;
 
-	if (!relation_statistics_update(positional_fcinfo, WARNING, true))
+	if (!relation_statistics_update(positional_fcinfo, WARNING, true, false))
 		result = false;
 
 	PG_RETURN_BOOL(result);
-- 
2.47.0

v32-0001-Add-derivative-flags-dumpSchema-dumpData.patchtext/x-patch; charset=US-ASCII; name=v32-0001-Add-derivative-flags-dumpSchema-dumpData.patchDownload
From 7668bda66c93958cc708e736324f27cec531a821 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 4 May 2024 04:52:38 -0400
Subject: [PATCH v32 01/14] Add derivative flags dumpSchema, dumpData.

User-set flags --schema-only and --data-only are often consulted by
various operations to determine if they should be skipped or not. While
this logic works when there are only two mutually-exclusive -only
options, it will get progressively more confusing when more are added.

In anticipation of this, create the flags dumpSchema and dumpData which
are derivative of the existing options schemaOnly and dataOnly. This
allows us to restate current skip-this-section tests in terms of what is
enabled, rather than checking if the other -only mode is turned off.
---
 src/bin/pg_dump/pg_backup.h  |   8 ++
 src/bin/pg_dump/pg_dump.c    | 186 ++++++++++++++++++-----------------
 src/bin/pg_dump/pg_restore.c |   4 +
 3 files changed, 107 insertions(+), 91 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 68ae2970ad..9bd3266a63 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -158,6 +158,10 @@ typedef struct _restoreOptions
 	int			enable_row_security;
 	int			sequence_data;	/* dump sequence data even in schema-only mode */
 	int			binary_upgrade;
+
+	/* flags derived entirely from the user-settable flags */
+	bool		dumpSchema;
+	bool		dumpData;
 } RestoreOptions;
 
 typedef struct _dumpOptions
@@ -204,6 +208,10 @@ typedef struct _dumpOptions
 
 	int			sequence_data;	/* dump sequence data even in schema-only mode */
 	int			do_nothing;
+
+	/* flags derived entirely from the user-settable flags */
+	bool		dumpSchema;
+	bool		dumpData;
 } DumpOptions;
 
 /*
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a8c141b689..7ce1c7a9ce 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -795,6 +795,10 @@ main(int argc, char **argv)
 	if (dopt.if_exists && !dopt.outputClean)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
+	/* set derivative flags */
+	dopt.dumpSchema = (!dopt.dataOnly);
+	dopt.dumpData = (!dopt.schemaOnly);
+
 	/*
 	 * --inserts are already implied above if --column-inserts or
 	 * --rows-per-insert were specified.
@@ -977,7 +981,7 @@ main(int argc, char **argv)
 	 * -s means "schema only" and LOs are data, not schema, so we never
 	 * include LOs when -s is used.
 	 */
-	if (dopt.include_everything && !dopt.schemaOnly && !dopt.dontOutputLOs)
+	if (dopt.include_everything && dopt.dumpData && !dopt.dontOutputLOs)
 		dopt.outputLOs = true;
 
 	/*
@@ -991,15 +995,15 @@ main(int argc, char **argv)
 	 */
 	tblinfo = getSchemaData(fout, &numTables);
 
-	if (!dopt.schemaOnly)
+	if (dopt.dumpData)
 	{
 		getTableData(&dopt, tblinfo, numTables, 0);
 		buildMatViewRefreshDependencies(fout);
-		if (dopt.dataOnly)
+		if (!dopt.dumpSchema)
 			getTableDataFKConstraints();
 	}
 
-	if (dopt.schemaOnly && dopt.sequence_data)
+	if (!dopt.dumpData && dopt.sequence_data)
 		getTableData(&dopt, tblinfo, numTables, RELKIND_SEQUENCE);
 
 	/*
@@ -4161,8 +4165,8 @@ dumpPolicy(Archive *fout, const PolicyInfo *polinfo)
 	const char *cmd;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
@@ -4383,8 +4387,8 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo)
 	char	   *qpubname;
 	bool		first = true;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	delq = createPQExpBuffer();
@@ -4701,8 +4705,8 @@ dumpPublicationNamespace(Archive *fout, const PublicationSchemaInfo *pubsinfo)
 	PQExpBuffer query;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	tag = psprintf("%s %s", pubinfo->dobj.name, schemainfo->dobj.name);
@@ -4744,8 +4748,8 @@ dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo)
 	PQExpBuffer query;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	tag = psprintf("%s %s", pubinfo->dobj.name, tbinfo->dobj.name);
@@ -5130,8 +5134,8 @@ dumpSubscriptionTable(Archive *fout, const SubRelInfo *subrinfo)
 	PQExpBuffer query;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	Assert(fout->dopt->binary_upgrade && fout->remoteVersion >= 170000);
@@ -5204,8 +5208,8 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo)
 	int			i;
 	char		two_phase_disabled[] = {LOGICALREP_TWOPHASE_STATE_DISABLED, '\0'};
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	delq = createPQExpBuffer();
@@ -7356,8 +7360,8 @@ getPartitioningInfo(Archive *fout)
 	/* hash partitioning didn't exist before v11 */
 	if (fout->remoteVersion < 110000)
 		return;
-	/* needn't bother if schema-only dump */
-	if (fout->dopt->schemaOnly)
+	/* needn't bother if not dumping data */
+	if (!fout->dopt->dumpData)
 		return;
 
 	query = createPQExpBuffer();
@@ -9055,7 +9059,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * Now get info about column defaults.  This is skipped for a data-only
 	 * dump, as it is only needed for table schemas.
 	 */
-	if (!dopt->dataOnly && tbloids->len > 1)
+	if (dopt->dumpSchema && tbloids->len > 1)
 	{
 		AttrDefInfo *attrdefs;
 		int			numDefaults;
@@ -9185,7 +9189,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * Get info about table CHECK constraints.  This is skipped for a
 	 * data-only dump, as it is only needed for table schemas.
 	 */
-	if (!dopt->dataOnly && checkoids->len > 2)
+	if (dopt->dumpSchema && checkoids->len > 2)
 	{
 		ConstraintInfo *constrs;
 		int			numConstrs;
@@ -10199,13 +10203,13 @@ dumpCommentExtended(Archive *fout, const char *type,
 	/* Comments are schema not data ... except LO comments are data */
 	if (strcmp(type, "LARGE OBJECT") != 0)
 	{
-		if (dopt->dataOnly)
+		if (!dopt->dumpSchema)
 			return;
 	}
 	else
 	{
 		/* We do dump LO comments in binary-upgrade mode */
-		if (dopt->schemaOnly && !dopt->binary_upgrade)
+		if (!dopt->dumpData && !dopt->binary_upgrade)
 			return;
 	}
 
@@ -10312,7 +10316,7 @@ dumpTableComment(Archive *fout, const TableInfo *tbinfo,
 		return;
 
 	/* Comments are SCHEMA not data */
-	if (dopt->dataOnly)
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Search for comments associated with relation, using table */
@@ -10758,8 +10762,8 @@ dumpNamespace(Archive *fout, const NamespaceInfo *nspinfo)
 	PQExpBuffer delq;
 	char	   *qnspname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -10835,8 +10839,8 @@ dumpExtension(Archive *fout, const ExtensionInfo *extinfo)
 	PQExpBuffer delq;
 	char	   *qextname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -10960,8 +10964,8 @@ dumpType(Archive *fout, const TypeInfo *tyinfo)
 {
 	DumpOptions *dopt = fout->dopt;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Dump out in proper style */
@@ -12071,8 +12075,8 @@ dumpShellType(Archive *fout, const ShellTypeInfo *stinfo)
 	DumpOptions *dopt = fout->dopt;
 	PQExpBuffer q;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -12123,8 +12127,8 @@ dumpProcLang(Archive *fout, const ProcLangInfo *plang)
 	FuncInfo   *inlineInfo = NULL;
 	FuncInfo   *validatorInfo = NULL;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
@@ -12331,8 +12335,8 @@ dumpFunc(Archive *fout, const FuncInfo *finfo)
 	int			nconfigitems = 0;
 	const char *keyword;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -12723,8 +12727,8 @@ dumpCast(Archive *fout, const CastInfo *cast)
 	const char *sourceType;
 	const char *targetType;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Cannot dump if we don't have the cast function's info */
@@ -12829,8 +12833,8 @@ dumpTransform(Archive *fout, const TransformInfo *transform)
 	char	   *lanname;
 	const char *transformType;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Cannot dump if we don't have the transform functions' info */
@@ -12978,8 +12982,8 @@ dumpOpr(Archive *fout, const OprInfo *oprinfo)
 	char	   *oprregproc;
 	char	   *oprref;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
@@ -13265,8 +13269,8 @@ dumpAccessMethod(Archive *fout, const AccessMethodInfo *aminfo)
 	PQExpBuffer delq;
 	char	   *qamname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -13368,8 +13372,8 @@ dumpOpclass(Archive *fout, const OpclassInfo *opcinfo)
 	bool		needComma;
 	int			i;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -13639,8 +13643,8 @@ dumpOpfamily(Archive *fout, const OpfamilyInfo *opfinfo)
 	bool		needComma;
 	int			i;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -13846,8 +13850,8 @@ dumpCollation(Archive *fout, const CollInfo *collinfo)
 	const char *colllocale;
 	const char *collicurules;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -14100,8 +14104,8 @@ dumpConversion(Archive *fout, const ConvInfo *convinfo)
 	const char *conproc;
 	bool		condefault;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -14248,8 +14252,8 @@ dumpAgg(Archive *fout, const AggInfo *agginfo)
 	const char *proparallel;
 	char		defaultfinalmodify;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -14578,8 +14582,8 @@ dumpTSParser(Archive *fout, const TSParserInfo *prsinfo)
 	PQExpBuffer delq;
 	char	   *qprsname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14646,8 +14650,8 @@ dumpTSDictionary(Archive *fout, const TSDictInfo *dictinfo)
 	char	   *nspname;
 	char	   *tmplname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14722,8 +14726,8 @@ dumpTSTemplate(Archive *fout, const TSTemplateInfo *tmplinfo)
 	PQExpBuffer delq;
 	char	   *qtmplname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14788,8 +14792,8 @@ dumpTSConfig(Archive *fout, const TSConfigInfo *cfginfo)
 	int			i_tokenname;
 	int			i_dictname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14900,8 +14904,8 @@ dumpForeignDataWrapper(Archive *fout, const FdwInfo *fdwinfo)
 	PQExpBuffer delq;
 	char	   *qfdwname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -14973,8 +14977,8 @@ dumpForeignServer(Archive *fout, const ForeignServerInfo *srvinfo)
 	char	   *qsrvname;
 	char	   *fdwname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -15164,8 +15168,8 @@ dumpDefaultACL(Archive *fout, const DefaultACLInfo *daclinfo)
 	PQExpBuffer tag;
 	const char *type;
 
-	/* Do nothing in data-only dump, or if we're skipping ACLs */
-	if (dopt->dataOnly || dopt->aclsSkip)
+	/* Do nothing if not dumping schema, or if we're skipping ACLs */
+	if (!dopt->dumpSchema || dopt->aclsSkip)
 		return;
 
 	q = createPQExpBuffer();
@@ -15265,7 +15269,7 @@ dumpACL(Archive *fout, DumpId objDumpId, DumpId altDumpId,
 		return InvalidDumpId;
 
 	/* --data-only skips ACLs *except* large object ACLs */
-	if (dopt->dataOnly && strcmp(type, "LARGE OBJECT") != 0)
+	if (!dopt->dumpSchema && strcmp(type, "LARGE OBJECT") != 0)
 		return InvalidDumpId;
 
 	sql = createPQExpBuffer();
@@ -15394,13 +15398,13 @@ dumpSecLabel(Archive *fout, const char *type, const char *name,
 	 */
 	if (strcmp(type, "LARGE OBJECT") != 0)
 	{
-		if (dopt->dataOnly)
+		if (!dopt->dumpSchema)
 			return;
 	}
 	else
 	{
 		/* We do dump large object security labels in binary-upgrade mode */
-		if (dopt->schemaOnly && !dopt->binary_upgrade)
+		if (!dopt->dumpData && !dopt->binary_upgrade)
 			return;
 	}
 
@@ -15468,7 +15472,7 @@ dumpTableSecLabel(Archive *fout, const TableInfo *tbinfo, const char *reltypenam
 		return;
 
 	/* SecLabel are SCHEMA not data */
-	if (dopt->dataOnly)
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Search for comments associated with relation, using table */
@@ -15707,8 +15711,8 @@ dumpTable(Archive *fout, const TableInfo *tbinfo)
 	DumpId		tableAclDumpId = InvalidDumpId;
 	char	   *namecopy;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	if (tbinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
@@ -16895,8 +16899,8 @@ dumpTableAttach(Archive *fout, const TableAttachInfo *attachinfo)
 	PGresult   *res;
 	char	   *partbound;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -16967,8 +16971,8 @@ dumpAttrDef(Archive *fout, const AttrDefInfo *adinfo)
 	char	   *tag;
 	char	   *foreign;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/* Skip if not "separate"; it was dumped in the table's definition */
@@ -17056,8 +17060,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 	char	   *qindxname;
 	char	   *qqindxname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -17189,8 +17193,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 static void
 dumpIndexAttach(Archive *fout, const IndexAttachInfo *attachinfo)
 {
-	/* Do nothing in data-only dump */
-	if (fout->dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!fout->dopt->dumpSchema)
 		return;
 
 	if (attachinfo->partitionIdx->dobj.dump & DUMP_COMPONENT_DEFINITION)
@@ -17236,8 +17240,8 @@ dumpStatisticsExt(Archive *fout, const StatsExtInfo *statsextinfo)
 	PGresult   *res;
 	char	   *stxdef;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -17312,8 +17316,8 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
 	char	   *tag = NULL;
 	char	   *foreign;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	q = createPQExpBuffer();
@@ -18049,8 +18053,8 @@ dumpTrigger(Archive *fout, const TriggerInfo *tginfo)
 	char	   *qtabname;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -18171,8 +18175,8 @@ dumpEventTrigger(Archive *fout, const EventTriggerInfo *evtinfo)
 	PQExpBuffer delqry;
 	char	   *qevtname;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	query = createPQExpBuffer();
@@ -18262,8 +18266,8 @@ dumpRule(Archive *fout, const RuleInfo *rinfo)
 	PGresult   *res;
 	char	   *tag;
 
-	/* Do nothing in data-only dump */
-	if (dopt->dataOnly)
+	/* Do nothing if not dumping schema */
+	if (!dopt->dumpSchema)
 		return;
 
 	/*
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index f2c1020d05..d2db23c75a 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -359,6 +359,10 @@ main(int argc, char **argv)
 	if (opts->single_txn && numWorkers > 1)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
+	/* set derivative flags */
+	opts->dumpSchema = (opts->dataOnly != 1);
+	opts->dumpData = (opts->schemaOnly != 1);
+
 	opts->disable_triggers = disable_triggers;
 	opts->enable_row_security = enable_row_security;
 	opts->noDataForFailedTables = no_data_for_failed_tables;
-- 
2.47.0

v32-0006-split-out-check_conn_options.patchtext/x-patch; charset=US-ASCII; name=v32-0006-split-out-check_conn_options.patchDownload
From 588af9192c576da9b3ae2e20ece4513a0f01a4d3 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 6 Nov 2024 15:20:07 -0500
Subject: [PATCH v32 06/14] split out check_conn_options

---
 src/bin/scripts/vacuumdb.c | 103 ++++++++++++++++++++-----------------
 1 file changed, 57 insertions(+), 46 deletions(-)

diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index d07ab7d67e..7b97a9428a 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -10,6 +10,7 @@
  *-------------------------------------------------------------------------
  */
 
+#include "libpq-fe.h"
 #include "postgres_fe.h"
 
 #include <limits.h>
@@ -459,55 +460,11 @@ escape_quotes(const char *src)
 }
 
 /*
- * vacuum_one_database
- *
- * Process tables in the given database.  If the 'tables' list is empty,
- * process all tables in the database.
- *
- * Note that this function is only concerned with running exactly one stage
- * when in analyze-in-stages mode; caller must iterate on us if necessary.
- *
- * If concurrentCons is > 1, multiple connections are used to vacuum tables
- * in parallel.  In this case and if the table list is empty, we first obtain
- * a list of tables from the database.
+ * Check connection options for compatibility with the connected database.
  */
 static void
-vacuum_one_database(ConnParams *cparams,
-					vacuumingOptions *vacopts,
-					int stage,
-					SimpleStringList *objects,
-					int concurrentCons,
-					const char *progname, bool echo, bool quiet)
+check_conn_options(PGconn *conn, vacuumingOptions *vacopts)
 {
-	PQExpBufferData sql;
-	PQExpBufferData buf;
-	PQExpBufferData catalog_query;
-	PGresult   *res;
-	PGconn	   *conn;
-	SimpleStringListCell *cell;
-	ParallelSlotArray *sa;
-	SimpleStringList dbtables = {NULL, NULL};
-	int			i;
-	int			ntups;
-	bool		failed = false;
-	bool		objects_listed = false;
-	const char *initcmd;
-	const char *stage_commands[] = {
-		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
-		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
-		"RESET default_statistics_target;"
-	};
-	const char *stage_messages[] = {
-		gettext_noop("Generating minimal optimizer statistics (1 target)"),
-		gettext_noop("Generating medium optimizer statistics (10 targets)"),
-		gettext_noop("Generating default (full) optimizer statistics")
-	};
-
-	Assert(stage == ANALYZE_NO_STAGE ||
-		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
-
-	conn = connectDatabase(cparams, progname, echo, false, true);
-
 	if (vacopts->disable_page_skipping && PQserverVersion(conn) < 90600)
 	{
 		PQfinish(conn);
@@ -575,6 +532,60 @@ vacuum_one_database(ConnParams *cparams,
 
 	/* skip_database_stats is used automatically if server supports it */
 	vacopts->skip_database_stats = (PQserverVersion(conn) >= 160000);
+}
+
+
+/*
+ * vacuum_one_database
+ *
+ * Process tables in the given database.  If the 'tables' list is empty,
+ * process all tables in the database.
+ *
+ * Note that this function is only concerned with running exactly one stage
+ * when in analyze-in-stages mode; caller must iterate on us if necessary.
+ *
+ * If concurrentCons is > 1, multiple connections are used to vacuum tables
+ * in parallel.  In this case and if the table list is empty, we first obtain
+ * a list of tables from the database.
+ */
+static void
+vacuum_one_database(ConnParams *cparams,
+					vacuumingOptions *vacopts,
+					int stage,
+					SimpleStringList *objects,
+					int concurrentCons,
+					const char *progname, bool echo, bool quiet)
+{
+	PQExpBufferData sql;
+	PQExpBufferData buf;
+	PQExpBufferData catalog_query;
+	PGresult   *res;
+	PGconn	   *conn;
+	SimpleStringListCell *cell;
+	ParallelSlotArray *sa;
+	SimpleStringList dbtables = {NULL, NULL};
+	int			i;
+	int			ntups;
+	bool		failed = false;
+	bool		objects_listed = false;
+	const char *initcmd;
+	const char *stage_commands[] = {
+		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
+		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
+		"RESET default_statistics_target;"
+	};
+	const char *stage_messages[] = {
+		gettext_noop("Generating minimal optimizer statistics (1 target)"),
+		gettext_noop("Generating medium optimizer statistics (10 targets)"),
+		gettext_noop("Generating default (full) optimizer statistics")
+	};
+
+	Assert(stage == ANALYZE_NO_STAGE ||
+		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
+
+	conn = connectDatabase(cparams, progname, echo, false, true);
+
+	check_conn_options(conn, vacopts);
 
 	if (!quiet)
 	{
-- 
2.47.0

v32-0009-preserve-catalog-lists-across-staged-runs.patchtext/x-patch; charset=US-ASCII; name=v32-0009-preserve-catalog-lists-across-staged-runs.patchDownload
From 7d5c52d4bfa157e6374ef7d6d7209b0b7d933c48 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 6 Nov 2024 21:30:49 -0500
Subject: [PATCH v32 09/14] preserve catalog lists across staged runs

---
 src/bin/scripts/vacuumdb.c | 113 ++++++++++++++++++++++++++-----------
 1 file changed, 80 insertions(+), 33 deletions(-)

diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index 36f4796db0..b13f3c4224 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -68,6 +68,9 @@ static void vacuum_one_database(ConnParams *cparams,
 								int stage,
 								SimpleStringList *objects,
 								int concurrentCons,
+								PGconn *conn,
+								SimpleStringList *dbtables,
+								int ntups,
 								const char *progname, bool echo, bool quiet);
 
 static void vacuum_all_databases(ConnParams *cparams,
@@ -83,6 +86,14 @@ static void prepare_vacuum_command(PQExpBuffer sql, int serverVersion,
 static void run_vacuum_command(PGconn *conn, const char *sql, bool echo,
 							   const char *table);
 
+static void check_conn_options(PGconn *conn, vacuumingOptions *vacopts);
+
+static void
+print_processing_notice(PGconn *conn, int stage, const char *progname, bool quiet);
+
+static SimpleStringList * generate_catalog_list(PGconn *conn, vacuumingOptions *vacopts,
+												SimpleStringList *objects, bool echo, int *ntups);
+
 static void help(const char *progname);
 
 void		check_objfilter(void);
@@ -386,6 +397,11 @@ main(int argc, char *argv[])
 	}
 	else
 	{
+		PGconn	   *conn;
+		int			ntup;
+		SimpleStringList   *found_objects;
+		int			stage;
+
 		if (dbname == NULL)
 		{
 			if (getenv("PGDATABASE"))
@@ -397,25 +413,37 @@ main(int argc, char *argv[])
 		}
 
 		cparams.dbname = dbname;
+		stage = (analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
+
+		conn = connectDatabase(&cparams, progname, echo, false, true);
+		check_conn_options(conn, &vacopts);
+		print_processing_notice(conn, stage, progname, quiet);
+		found_objects = generate_catalog_list(conn, &vacopts, &objects, echo, &ntup);
 
 		if (analyze_in_stages)
 		{
-			int			stage;
-
 			for (stage = 0; stage < ANALYZE_NUM_STAGES; stage++)
 			{
-				vacuum_one_database(&cparams, &vacopts,
-									stage,
+				/* the last pass disconnected the conn */
+				if (stage > 0)
+					conn = connectDatabase(&cparams, progname, echo, false, true);
+
+				vacuum_one_database(&cparams, &vacopts, stage,
 									&objects,
 									concurrentCons,
+									conn,
+									found_objects,
+									ntup,
 									progname, echo, quiet);
 			}
 		}
 		else
-			vacuum_one_database(&cparams, &vacopts,
-								ANALYZE_NO_STAGE,
+			vacuum_one_database(&cparams, &vacopts, stage,
 								&objects,
 								concurrentCons,
+								conn,
+								found_objects,
+								ntup,
 								progname, echo, quiet);
 	}
 
@@ -791,16 +819,17 @@ vacuum_one_database(ConnParams *cparams,
 					int stage,
 					SimpleStringList *objects,
 					int concurrentCons,
+					PGconn *conn,
+					SimpleStringList *dbtables,
+					int ntups,
 					const char *progname, bool echo, bool quiet)
 {
 	PQExpBufferData sql;
-	PGconn	   *conn;
 	SimpleStringListCell *cell;
 	ParallelSlotArray *sa;
-	int			ntups;
 	bool		failed = false;
 	const char *initcmd;
-	SimpleStringList *dbtables;
+
 	const char *stage_commands[] = {
 		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
 		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
@@ -810,13 +839,6 @@ vacuum_one_database(ConnParams *cparams,
 	Assert(stage == ANALYZE_NO_STAGE ||
 		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
 
-	conn = connectDatabase(cparams, progname, echo, false, true);
-
-	check_conn_options(conn, vacopts);
-	print_processing_notice(conn, stage, progname, quiet);
-
-	dbtables = generate_catalog_list(conn, vacopts, objects, echo, &ntups);
-
 	/*
 	 * If no rows are returned, there are no matching tables, so we are done.
 	 */
@@ -928,7 +950,7 @@ finish:
 }
 
 /*
- * Vacuum/analyze all connectable databases.
+ * Vacuum/analyze all ccparams->override_dbname = PQgetvalue(result, i, 0);onnectable databases.
  *
  * In analyze-in-stages mode, we process all databases in one stage before
  * moving on to the next stage.  That ensure minimal stats are available
@@ -944,8 +966,13 @@ vacuum_all_databases(ConnParams *cparams,
 {
 	PGconn	   *conn;
 	PGresult   *result;
-	int			stage;
 	int			i;
+	int			stage;
+
+	SimpleStringList  **found_objects;
+	int				   *num_tuples;
+
+	stage = (analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
 
 	conn = connectMaintenanceDatabase(cparams, progname, echo);
 	result = executeQuery(conn,
@@ -953,7 +980,33 @@ vacuum_all_databases(ConnParams *cparams,
 						  echo);
 	PQfinish(conn);
 
-	if (analyze_in_stages)
+	/*
+	 * connect to each database, check validity of options,
+	 * build the list of found objects per database,
+	 * and run the first/only vacuum stage
+	 */
+	found_objects = palloc(PQntuples(result) * sizeof(SimpleStringList *));
+	num_tuples = palloc(PQntuples(result) * sizeof (int));
+
+	for (i = 0; i < PQntuples(result); i++)
+	{
+		cparams->override_dbname = PQgetvalue(result, i, 0);
+		conn = connectDatabase(cparams, progname, echo, false, true);
+		check_conn_options(conn, vacopts);
+		print_processing_notice(conn, stage, progname, quiet);
+		found_objects[i] = generate_catalog_list(conn, vacopts, objects, echo, &num_tuples[i]);
+
+		vacuum_one_database(cparams, vacopts,
+							stage,
+							objects,
+							concurrentCons,
+							conn,
+							found_objects[i],
+							num_tuples[i],
+							progname, echo, quiet);
+	}
+
+	if (stage != ANALYZE_NO_STAGE)
 	{
 		/*
 		 * When analyzing all databases in stages, we analyze them all in the
@@ -963,35 +1016,29 @@ vacuum_all_databases(ConnParams *cparams,
 		 * This means we establish several times as many connections, but
 		 * that's a secondary consideration.
 		 */
-		for (stage = 0; stage < ANALYZE_NUM_STAGES; stage++)
+		for (stage = 1; stage < ANALYZE_NUM_STAGES; stage++)
 		{
 			for (i = 0; i < PQntuples(result); i++)
 			{
 				cparams->override_dbname = PQgetvalue(result, i, 0);
+				conn = connectDatabase(cparams, progname, echo, false, true);
+				print_processing_notice(conn, stage, progname, quiet);
 
 				vacuum_one_database(cparams, vacopts,
 									stage,
 									objects,
 									concurrentCons,
+									conn,
+									found_objects[i],
+									num_tuples[i],
 									progname, echo, quiet);
 			}
 		}
 	}
-	else
-	{
-		for (i = 0; i < PQntuples(result); i++)
-		{
-			cparams->override_dbname = PQgetvalue(result, i, 0);
-
-			vacuum_one_database(cparams, vacopts,
-								ANALYZE_NO_STAGE,
-								objects,
-								concurrentCons,
-								progname, echo, quiet);
-		}
-	}
 
 	PQclear(result);
+	pfree(found_objects);
+	pfree(num_tuples);
 }
 
 /*
-- 
2.47.0

v32-0008-split-out-generate_catalog_list.patchtext/x-patch; charset=US-ASCII; name=v32-0008-split-out-generate_catalog_list.patchDownload
From c9c2dbc6a82946ae1de8884552810f68374ce945 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 6 Nov 2024 16:15:16 -0500
Subject: [PATCH v32 08/14] split out generate_catalog_list

---
 src/bin/scripts/vacuumdb.c | 176 +++++++++++++++++++++----------------
 1 file changed, 99 insertions(+), 77 deletions(-)

diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index e9946f79b2..36f4796db0 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -560,67 +560,38 @@ print_processing_notice(PGconn *conn, int stage, const char *progname, bool quie
 }
 
 /*
- * vacuum_one_database
- *
- * Process tables in the given database.  If the 'tables' list is empty,
- * process all tables in the database.
- *
- * Note that this function is only concerned with running exactly one stage
- * when in analyze-in-stages mode; caller must iterate on us if necessary.
- *
- * If concurrentCons is > 1, multiple connections are used to vacuum tables
- * in parallel.  In this case and if the table list is empty, we first obtain
- * a list of tables from the database.
- */
-static void
-vacuum_one_database(ConnParams *cparams,
-					vacuumingOptions *vacopts,
-					int stage,
-					SimpleStringList *objects,
-					int concurrentCons,
-					const char *progname, bool echo, bool quiet)
+	* Prepare the list of tables to process by querying the catalogs.
+	*
+	* Since we execute the constructed query with the default search_path
+	* (which could be unsafe), everything in this query MUST be fully
+	* qualified.
+	*
+	* First, build a WITH clause for the catalog query if any tables were
+	* specified, with a set of values made of relation names and their
+	* optional set of columns.  This is used to match any provided column
+	* lists with the generated qualified identifiers and to filter for the
+	* tables provided via --table.  If a listed table does not exist, the
+	* catalog query will fail.
+	*/
+static SimpleStringList *
+generate_catalog_list(PGconn *conn,
+					  vacuumingOptions *vacopts,
+					  SimpleStringList *objects,
+					  bool echo,
+					  int *ntups)
 {
-	PQExpBufferData sql;
-	PQExpBufferData buf;
 	PQExpBufferData catalog_query;
-	PGresult   *res;
-	PGconn	   *conn;
+	PQExpBufferData buf;
+	SimpleStringList *dbtables;
 	SimpleStringListCell *cell;
-	ParallelSlotArray *sa;
-	SimpleStringList dbtables = {NULL, NULL};
-	int			i;
-	int			ntups;
-	bool		failed = false;
 	bool		objects_listed = false;
-	const char *initcmd;
-	const char *stage_commands[] = {
-		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
-		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
-		"RESET default_statistics_target;"
-	};
+	PGresult   *res;
+	int			i;
 
-	Assert(stage == ANALYZE_NO_STAGE ||
-		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
+	dbtables = palloc(sizeof(SimpleStringList));
+	dbtables->head = NULL;
+	dbtables->tail = NULL;
 
-	conn = connectDatabase(cparams, progname, echo, false, true);
-
-	check_conn_options(conn, vacopts);
-	print_processing_notice(conn, stage, progname, quiet);
-
-	/*
-	 * Prepare the list of tables to process by querying the catalogs.
-	 *
-	 * Since we execute the constructed query with the default search_path
-	 * (which could be unsafe), everything in this query MUST be fully
-	 * qualified.
-	 *
-	 * First, build a WITH clause for the catalog query if any tables were
-	 * specified, with a set of values made of relation names and their
-	 * optional set of columns.  This is used to match any provided column
-	 * lists with the generated qualified identifiers and to filter for the
-	 * tables provided via --table.  If a listed table does not exist, the
-	 * catalog query will fail.
-	 */
 	initPQExpBuffer(&catalog_query);
 	for (cell = objects ? objects->head : NULL; cell; cell = cell->next)
 	{
@@ -771,40 +742,91 @@ vacuum_one_database(ConnParams *cparams,
 	appendPQExpBufferStr(&catalog_query, " ORDER BY c.relpages DESC;");
 	executeCommand(conn, "RESET search_path;", echo);
 	res = executeQuery(conn, catalog_query.data, echo);
+	*ntups = PQntuples(res);
 	termPQExpBuffer(&catalog_query);
 	PQclear(executeQuery(conn, ALWAYS_SECURE_SEARCH_PATH_SQL, echo));
 
-	/*
-	 * If no rows are returned, there are no matching tables, so we are done.
-	 */
-	ntups = PQntuples(res);
-	if (ntups == 0)
-	{
-		PQclear(res);
-		PQfinish(conn);
-		return;
-	}
-
 	/*
 	 * Build qualified identifiers for each table, including the column list
 	 * if given.
 	 */
-	initPQExpBuffer(&buf);
-	for (i = 0; i < ntups; i++)
+	if (*ntups > 0)
 	{
-		appendPQExpBufferStr(&buf,
-							 fmtQualifiedId(PQgetvalue(res, i, 1),
-											PQgetvalue(res, i, 0)));
+		initPQExpBuffer(&buf);
+		for (i = 0; i < *ntups; i++)
+		{
+			appendPQExpBufferStr(&buf,
+								fmtQualifiedId(PQgetvalue(res, i, 1),
+												PQgetvalue(res, i, 0)));
 
-		if (objects_listed && !PQgetisnull(res, i, 2))
-			appendPQExpBufferStr(&buf, PQgetvalue(res, i, 2));
+			if (objects_listed && !PQgetisnull(res, i, 2))
+				appendPQExpBufferStr(&buf, PQgetvalue(res, i, 2));
 
-		simple_string_list_append(&dbtables, buf.data);
-		resetPQExpBuffer(&buf);
+			simple_string_list_append(dbtables, buf.data);
+			resetPQExpBuffer(&buf);
+		}
+		termPQExpBuffer(&buf);
 	}
-	termPQExpBuffer(&buf);
 	PQclear(res);
 
+	return dbtables;
+}
+
+/*
+ * vacuum_one_database
+ *
+ * Process tables in the given database.  If the 'tables' list is empty,
+ * process all tables in the database.
+ *
+ * Note that this function is only concerned with running exactly one stage
+ * when in analyze-in-stages mode; caller must iterate on us if necessary.
+ *
+ * If concurrentCons is > 1, multiple connections are used to vacuum tables
+ * in parallel.  In this case and if the table list is empty, we first obtain
+ * a list of tables from the database.
+ */
+static void
+vacuum_one_database(ConnParams *cparams,
+					vacuumingOptions *vacopts,
+					int stage,
+					SimpleStringList *objects,
+					int concurrentCons,
+					const char *progname, bool echo, bool quiet)
+{
+	PQExpBufferData sql;
+	PGconn	   *conn;
+	SimpleStringListCell *cell;
+	ParallelSlotArray *sa;
+	int			ntups;
+	bool		failed = false;
+	const char *initcmd;
+	SimpleStringList *dbtables;
+	const char *stage_commands[] = {
+		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
+		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
+		"RESET default_statistics_target;"
+	};
+
+	Assert(stage == ANALYZE_NO_STAGE ||
+		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
+
+	conn = connectDatabase(cparams, progname, echo, false, true);
+
+	check_conn_options(conn, vacopts);
+	print_processing_notice(conn, stage, progname, quiet);
+
+	dbtables = generate_catalog_list(conn, vacopts, objects, echo, &ntups);
+
+	/*
+	 * If no rows are returned, there are no matching tables, so we are done.
+	 */
+	if (ntups == 0)
+	{
+		PQfinish(conn);
+		return;
+	}
+
+
 	/*
 	 * Ensure concurrentCons is sane.  If there are more connections than
 	 * vacuumable relations, we don't need to use them all.
@@ -837,7 +859,7 @@ vacuum_one_database(ConnParams *cparams,
 
 	initPQExpBuffer(&sql);
 
-	cell = dbtables.head;
+	cell = dbtables->head;
 	do
 	{
 		const char *tabname = cell->val;
-- 
2.47.0

v32-0010-Add-issues_sql_unlike-opposite-of-issues_sql_lik.patchtext/x-patch; charset=US-ASCII; name=v32-0010-Add-issues_sql_unlike-opposite-of-issues_sql_lik.patchDownload
From 7e9a2903285f9f49a5434af6c2ab4337a158883d Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 7 Nov 2024 18:06:52 -0500
Subject: [PATCH v32 10/14] Add issues_sql_unlike, opposite of issues_sql_like

This is the same as issues_sql_like(), but the specified text is
prohibited from being in the output rather than required.

This became necessary to test that a command-line filter was in fact
filtering out certain output that a prior test required.
---
 src/test/perl/PostgreSQL/Test/Cluster.pm | 25 ++++++++++++++++++++++++
 1 file changed, 25 insertions(+)

diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index 508e5e3917..1281071479 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -2799,6 +2799,31 @@ sub issues_sql_like
 }
 
 =pod
+=item $node->issues_sql_unlike(cmd, prohibited_sql, test_name)
+
+Run a command on the node, then verify that $prohibited_sql does not appear
+in the server log file.
+
+=cut
+
+sub issues_sql_unlike
+{
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+	my ($self, $cmd, $prohibited_sql, $test_name) = @_;
+
+	local %ENV = $self->_get_env();
+
+	my $log_location = -s $self->logfile;
+
+	my $result = PostgreSQL::Test::Utils::run_log($cmd);
+	ok($result, "@$cmd exit code 0");
+	my $log =
+	  PostgreSQL::Test::Utils::slurp_file($self->logfile, $log_location);
+	unlike($log, $prohibited_sql, "$test_name: SQL found in server log");
+	return;
+}
+
 
 =item $node->log_content()
 
-- 
2.47.0

v32-0007-split-out-print_processing_notice.patchtext/x-patch; charset=US-ASCII; name=v32-0007-split-out-print_processing_notice.patchDownload
From c3b2b3a148b7e8c9a5eb6fa1d5e2a4124834bcd3 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 6 Nov 2024 15:46:51 -0500
Subject: [PATCH v32 07/14] split out print_processing_notice

---
 src/bin/scripts/vacuumdb.c | 41 +++++++++++++++++++++++---------------
 1 file changed, 25 insertions(+), 16 deletions(-)

diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index 7b97a9428a..e9946f79b2 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -535,6 +535,30 @@ check_conn_options(PGconn *conn, vacuumingOptions *vacopts)
 }
 
 
+/*
+ * print the processing notice for a database.
+ */
+static void
+print_processing_notice(PGconn *conn, int stage, const char *progname, bool quiet)
+{
+	const char *stage_messages[] = {
+		gettext_noop("Generating minimal optimizer statistics (1 target)"),
+		gettext_noop("Generating medium optimizer statistics (10 targets)"),
+		gettext_noop("Generating default (full) optimizer statistics")
+	};
+
+	if (!quiet)
+	{
+		if (stage != ANALYZE_NO_STAGE)
+			printf(_("%s: processing database \"%s\": %s\n"),
+				   progname, PQdb(conn), _(stage_messages[stage]));
+		else
+			printf(_("%s: vacuuming database \"%s\"\n"),
+				   progname, PQdb(conn));
+		fflush(stdout);
+	}
+}
+
 /*
  * vacuum_one_database
  *
@@ -574,11 +598,6 @@ vacuum_one_database(ConnParams *cparams,
 		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
 		"RESET default_statistics_target;"
 	};
-	const char *stage_messages[] = {
-		gettext_noop("Generating minimal optimizer statistics (1 target)"),
-		gettext_noop("Generating medium optimizer statistics (10 targets)"),
-		gettext_noop("Generating default (full) optimizer statistics")
-	};
 
 	Assert(stage == ANALYZE_NO_STAGE ||
 		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
@@ -586,17 +605,7 @@ vacuum_one_database(ConnParams *cparams,
 	conn = connectDatabase(cparams, progname, echo, false, true);
 
 	check_conn_options(conn, vacopts);
-
-	if (!quiet)
-	{
-		if (stage != ANALYZE_NO_STAGE)
-			printf(_("%s: processing database \"%s\": %s\n"),
-				   progname, PQdb(conn), _(stage_messages[stage]));
-		else
-			printf(_("%s: vacuuming database \"%s\"\n"),
-				   progname, PQdb(conn));
-		fflush(stdout);
-	}
+	print_processing_notice(conn, stage, progname, quiet);
 
 	/*
 	 * Prepare the list of tables to process by querying the catalogs.
-- 
2.47.0

v32-0011-Add-force-analyze-to-vacuumdb.patchtext/x-patch; charset=US-ASCII; name=v32-0011-Add-force-analyze-to-vacuumdb.patchDownload
From 1db3ae91d7f08fc7385e714c45610f1093bc8bc5 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Fri, 8 Nov 2024 12:27:50 -0500
Subject: [PATCH v32 11/14] Add --force-analyze to vacuumdb.

The vacuumdb options of --analyze-in-stages and --analyze-only are often
used after a restore from a dump or a pg_upgrade to quickly rebuild
stats on a databse.

However, now that stats are imported in most (but not all) cases,
running either of these commands will be at least partially redundant,
and will overwrite the stats that were just imported, which is a big
POLA violation.

We could add a new option such as --analyze-missing-in-stages, but that
wouldn't help the userbase that grown accustomed to running
--analyze-in-stages after an upgrade.

The least-bad option to handle the situation is to change the behavior
of --analyze-only and --analyze-in-stages to only analyze tables which
were missing stats before the vacuumdb started, but offer the
--force-analyze flag to restore the old behavior for those who truly
wanted it.
---
 src/bin/scripts/t/100_vacuumdb.pl |  6 +-
 src/bin/scripts/vacuumdb.c        | 91 ++++++++++++++++++++++++-------
 doc/src/sgml/ref/vacuumdb.sgml    | 48 ++++++++++++++++
 3 files changed, 125 insertions(+), 20 deletions(-)

diff --git a/src/bin/scripts/t/100_vacuumdb.pl b/src/bin/scripts/t/100_vacuumdb.pl
index 1a2bcb4959..2d669391fe 100644
--- a/src/bin/scripts/t/100_vacuumdb.pl
+++ b/src/bin/scripts/t/100_vacuumdb.pl
@@ -127,9 +127,13 @@ $node->issues_sql_like(
 	qr/statement: VACUUM \(SKIP_DATABASE_STATS, ANALYZE\) public.vactable\(a, b\);/,
 	'vacuumdb --analyze with complete column list');
 $node->issues_sql_like(
+	[ 'vacuumdb', '--analyze-only', '--force-analyze', '--table', 'vactable(b)', 'postgres' ],
+	qr/statement: ANALYZE public.vactable\(b\);/,
+	'vacuumdb --analyze-only --force-analyze with partial column list');
+$node->issues_sql_unlike(
 	[ 'vacuumdb', '--analyze-only', '--table', 'vactable(b)', 'postgres' ],
 	qr/statement: ANALYZE public.vactable\(b\);/,
-	'vacuumdb --analyze-only with partial column list');
+	'vacuumdb --analyze-only --force-analyze with partial column list skipping vacuumed tables');
 $node->command_checks_all(
 	[ 'vacuumdb', '--analyze', '--table', 'vacview', 'postgres' ],
 	0,
diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index b13f3c4224..1aa5c46af5 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -25,6 +25,7 @@
 #include "fe_utils/query_utils.h"
 #include "fe_utils/simple_list.h"
 #include "fe_utils/string_utils.h"
+#include "pqexpbuffer.h"
 
 
 /* vacuum options controlled by user flags */
@@ -47,6 +48,8 @@ typedef struct vacuumingOptions
 	bool		process_main;
 	bool		process_toast;
 	bool		skip_database_stats;
+	bool		analyze_in_stages;
+	bool		force_analyze;
 	char	   *buffer_usage_limit;
 } vacuumingOptions;
 
@@ -75,7 +78,6 @@ static void vacuum_one_database(ConnParams *cparams,
 
 static void vacuum_all_databases(ConnParams *cparams,
 								 vacuumingOptions *vacopts,
-								 bool analyze_in_stages,
 								 SimpleStringList *objects,
 								 int concurrentCons,
 								 const char *progname, bool echo, bool quiet);
@@ -140,6 +142,7 @@ main(int argc, char *argv[])
 		{"no-process-toast", no_argument, NULL, 11},
 		{"no-process-main", no_argument, NULL, 12},
 		{"buffer-usage-limit", required_argument, NULL, 13},
+		{"force-analyze", no_argument, NULL, 14},
 		{NULL, 0, NULL, 0}
 	};
 
@@ -156,7 +159,6 @@ main(int argc, char *argv[])
 	bool		echo = false;
 	bool		quiet = false;
 	vacuumingOptions vacopts;
-	bool		analyze_in_stages = false;
 	SimpleStringList objects = {NULL, NULL};
 	int			concurrentCons = 1;
 	int			tbl_count = 0;
@@ -170,6 +172,8 @@ main(int argc, char *argv[])
 	vacopts.do_truncate = true;
 	vacopts.process_main = true;
 	vacopts.process_toast = true;
+	vacopts.force_analyze = false;
+	vacopts.analyze_in_stages = false;
 
 	pg_logging_init(argv[0]);
 	progname = get_progname(argv[0]);
@@ -251,7 +255,7 @@ main(int argc, char *argv[])
 				maintenance_db = pg_strdup(optarg);
 				break;
 			case 3:
-				analyze_in_stages = vacopts.analyze_only = true;
+				vacopts.analyze_in_stages = vacopts.analyze_only = true;
 				break;
 			case 4:
 				vacopts.disable_page_skipping = true;
@@ -287,6 +291,9 @@ main(int argc, char *argv[])
 			case 13:
 				vacopts.buffer_usage_limit = escape_quotes(optarg);
 				break;
+			case 14:
+				vacopts.force_analyze = true;
+				break;
 			default:
 				/* getopt_long already emitted a complaint */
 				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -372,6 +379,14 @@ main(int argc, char *argv[])
 		pg_fatal("cannot use the \"%s\" option with the \"%s\" option",
 				 "buffer-usage-limit", "full");
 
+	/*
+	 * --force-analyze is only valid when used with --analyze-only, -analyze,
+	 * or --analyze-in-stages
+	 */
+	if (vacopts.force_analyze && !vacopts.analyze_only && !vacopts.analyze_in_stages)
+		pg_fatal("can only use the \"%s\" option with \"%s\" or \"%s\"",
+				 "--force-analyze", "-Z/--analyze-only", "--analyze-in-stages");
+
 	/* fill cparams except for dbname, which is set below */
 	cparams.pghost = host;
 	cparams.pgport = port;
@@ -390,7 +405,6 @@ main(int argc, char *argv[])
 		cparams.dbname = maintenance_db;
 
 		vacuum_all_databases(&cparams, &vacopts,
-							 analyze_in_stages,
 							 &objects,
 							 concurrentCons,
 							 progname, echo, quiet);
@@ -413,20 +427,27 @@ main(int argc, char *argv[])
 		}
 
 		cparams.dbname = dbname;
-		stage = (analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
+		stage = (vacopts.analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
 
 		conn = connectDatabase(&cparams, progname, echo, false, true);
 		check_conn_options(conn, &vacopts);
 		print_processing_notice(conn, stage, progname, quiet);
 		found_objects = generate_catalog_list(conn, &vacopts, &objects, echo, &ntup);
+		vacuum_one_database(&cparams, &vacopts, stage,
+							&objects,
+							concurrentCons,
+							conn,
+							found_objects,
+							ntup,
+							progname, echo, quiet);
 
-		if (analyze_in_stages)
+		if (stage != ANALYZE_NO_STAGE)
 		{
-			for (stage = 0; stage < ANALYZE_NUM_STAGES; stage++)
+			for (stage = 1; stage < ANALYZE_NUM_STAGES; stage++)
 			{
 				/* the last pass disconnected the conn */
-				if (stage > 0)
-					conn = connectDatabase(&cparams, progname, echo, false, true);
+				conn = connectDatabase(&cparams, progname, echo, false, true);
+				print_processing_notice(conn, stage, progname, quiet);
 
 				vacuum_one_database(&cparams, &vacopts, stage,
 									&objects,
@@ -437,14 +458,6 @@ main(int argc, char *argv[])
 									progname, echo, quiet);
 			}
 		}
-		else
-			vacuum_one_database(&cparams, &vacopts, stage,
-								&objects,
-								concurrentCons,
-								conn,
-								found_objects,
-								ntup,
-								progname, echo, quiet);
 	}
 
 	exit(0);
@@ -763,6 +776,47 @@ generate_catalog_list(PGconn *conn,
 						  vacopts->min_mxid_age);
 	}
 
+	/*
+	 * If this query is for an analyze-only or analyze-in-stages, two
+	 * upgrade-centric operations, and force-analyze is NOT set, then
+	 * exclude any relations that already have their full compliment
+	 * of attribute stats and extended stats.
+	 *
+	 *
+	 */
+	if ((vacopts->analyze_only || vacopts->analyze_in_stages) &&
+		!vacopts->force_analyze)
+	{
+		/*
+		 * The pg_class in question has no pg_statistic rows representing
+		 * user-visible columns that lack a corresponding pg_statitic row.
+		 * Currently no differentiation is made for whether the
+		 * pg_statistic.stainherit is true or false.
+		 */
+		appendPQExpBufferStr(&catalog_query,
+							 " AND NOT EXISTS (\n"
+							 " SELECT NULL FROM pg_catalog.pg_attribute AS a\n"
+							 " WHERE a.attrelid OPERATOR(pg_catalog.=) c.oid\n"
+							 " AND a.attnum OPERATOR(pg_catalog.>) 0 AND NOT a.attisdropped\n"
+							 " AND NOT EXISTS (\n"
+							 " SELECT NULL FROM pg_catalog.pg_statistic AS s\n"
+							 " WHERE s.starelid OPERATOR(pg_catalog.=) c.oid\n"
+							 " AND s.staattnum OPERATOR(pg_catalog.=) a.attnum))\n");
+
+		/*
+		 * The pg_class entry has no pg_statistic_ext rows that lack a corresponding
+		 * pg_statistic_ext_data row. Currently no differentiation is made for whether
+		 * pg_statistic_exta_data.stxdinherit is true or false.
+		 */
+		appendPQExpBufferStr(&catalog_query,
+							 " AND NOT EXISTS (\n"
+							 " SELECT NULL FROM pg_catalog.pg_statistic_ext AS e\n"
+							 " WHERE e.stxrelid OPERATOR(pg_catalog.=) c.oid\n"
+							 " AND NOT EXISTS (\n"
+							 " SELECT NULL FROM pg_catalog.pg_statistic_ext_data AS d\n"
+							 " WHERE d.stxoid OPERATOR(pg_catalog.=) e.oid))\n");
+	}
+
 	/*
 	 * Execute the catalog query.  We use the default search_path for this
 	 * query for consistency with table lookups done elsewhere by the user.
@@ -959,7 +1013,6 @@ finish:
 static void
 vacuum_all_databases(ConnParams *cparams,
 					 vacuumingOptions *vacopts,
-					 bool analyze_in_stages,
 					 SimpleStringList *objects,
 					 int concurrentCons,
 					 const char *progname, bool echo, bool quiet)
@@ -972,7 +1025,7 @@ vacuum_all_databases(ConnParams *cparams,
 	SimpleStringList  **found_objects;
 	int				   *num_tuples;
 
-	stage = (analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
+	stage = (vacopts->analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
 
 	conn = connectMaintenanceDatabase(cparams, progname, echo);
 	result = executeQuery(conn,
diff --git a/doc/src/sgml/ref/vacuumdb.sgml b/doc/src/sgml/ref/vacuumdb.sgml
index 66fccb30a2..00ec927606 100644
--- a/doc/src/sgml/ref/vacuumdb.sgml
+++ b/doc/src/sgml/ref/vacuumdb.sgml
@@ -425,6 +425,12 @@ PostgreSQL documentation
        <para>
         Only calculate statistics for use by the optimizer (no vacuum).
        </para>
+       <para>
+        By default, this operation excludes relations that already have
+        statistics generated. If the option <option>--force-analyze</option>
+        is also specified, then relations with existing stastistics are not
+        excluded.
+       </para>
       </listitem>
      </varlistentry>
 
@@ -439,6 +445,47 @@ PostgreSQL documentation
         to produce usable statistics faster, and subsequent stages build the
         full statistics.
        </para>
+       <para>
+        This option is intended to be run after a <command>pg_upgrade</command>
+        to generate statistics for relations that have no stistatics or incomplete
+        statistics (such as those with extended statistics objects, which are not
+        imported on upgrade).
+       </para>
+       <para>
+        If the option <option>--force-analyze</option> is also specified, then
+        relations with existing stastistics are not excluded.
+        This option is only useful to analyze a database that currently has
+        no statistics or has wholly incorrect ones, such as if it is newly
+        populated from a restored dump.
+        Be aware that running with this option combinationin a database with
+        existing statistics may cause the query optimizer choices to become
+        transiently worse due to the low statistics targets of the early
+        stages.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--force-analyze</option></term>
+      <listitem>
+       <para>
+        This option can only be used if either <option>--analyze-only</option>
+        or <option>--analyze-in-stages</option> is specified. It modifies those
+        options to not filter out relations that already have statistics.
+       </para>
+       <para>
+
+        Only calculate statistics for use by the optimizer (no vacuum),
+        like <option>--analyze-only</option>.  Run three
+        stages of analyze; the first stage uses the lowest possible statistics
+        target (see <xref linkend="guc-default-statistics-target"/>)
+        to produce usable statistics faster, and subsequent stages build the
+        full statistics.
+       </para>
+
+       <para>
+        This option was created
+       </para>
 
        <para>
         This option is only useful to analyze a database that currently has
@@ -452,6 +499,7 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+
      <varlistentry>
        <term><option>-?</option></term>
        <term><option>--help</option></term>
-- 
2.47.0

v32-0012-Disallow-setting-statistics-on-system-columns.patchtext/x-patch; charset=US-ASCII; name=v32-0012-Disallow-setting-statistics-on-system-columns.patchDownload
From 936d98c474afcbc33fc85694dd25df70fdec9b5a Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 13 Nov 2024 01:00:45 -0500
Subject: [PATCH v32 12/14] Disallow setting statistics on system columns.

ANALYZE can't, so we shouldn't.
---
 src/backend/statistics/attribute_stats.c   | 6 ++++--
 src/test/regress/expected/stats_import.out | 9 +++++++++
 src/test/regress/sql/stats_import.sql      | 9 +++++++++
 3 files changed, 22 insertions(+), 2 deletions(-)

diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index 4ae0722b78..42f458c280 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -161,7 +161,8 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	stats_check_required_arg(fcinfo, attarginfo, ATTNAME_ARG);
 	attname = PG_GETARG_NAME(ATTNAME_ARG);
 	attnum = get_attnum(reloid, NameStr(*attname));
-	if (attnum == InvalidAttrNumber)
+	/* maybe use (!AttrNumberIsForUserDefinedAttr(attnum)) instead? */
+	if (attnum <= InvalidAttrNumber)
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 errmsg("column \"%s\" of relation \"%s\" does not exist",
@@ -870,7 +871,8 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 	stats_check_required_arg(fcinfo, attarginfo, ATTNAME_ARG);
 	attname = PG_GETARG_NAME(ATTNAME_ARG);
 	attnum = get_attnum(reloid, NameStr(*attname));
-	if (attnum == InvalidAttrNumber)
+	/* maybe use (!AttrNumberIsForUserDefinedAttr(attnum)) instead? */
+	if (attnum <= InvalidAttrNumber)
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 errmsg("column \"%s\" of relation \"%s\" does not exist",
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index 9186fc01ec..9da9e3cbfc 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -228,6 +228,15 @@ SELECT pg_catalog.pg_set_attribute_stats(
     avg_width => 2::integer,
     n_distinct => 0.3::real);
 ERROR:  "inherited" cannot be NULL
+-- error: system column
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'xmin'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  column "xmin" of relation "test" does not exist
 -- ok: no stakinds
 SELECT pg_catalog.pg_set_attribute_stats(
     relation => 'stats_import.test'::regclass,
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index c7d5e017d9..3c68b51bcc 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -178,6 +178,15 @@ SELECT pg_catalog.pg_set_attribute_stats(
     avg_width => 2::integer,
     n_distinct => 0.3::real);
 
+-- error: system column
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'xmin'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
 -- ok: no stakinds
 SELECT pg_catalog.pg_set_attribute_stats(
     relation => 'stats_import.test'::regclass,
-- 
2.47.0

v32-0013-Consolidate-attribute-syscache-lookups-into-one-.patchtext/x-patch; charset=US-ASCII; name=v32-0013-Consolidate-attribute-syscache-lookups-into-one-.patchDownload
From 433b3b2dc09445f68d14677d61a77e266c4f7fed Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 14 Nov 2024 03:53:33 -0500
Subject: [PATCH v32 13/14] Consolidate attribute syscache lookups into one
 call by name.

Previously we were doing one lookup by attname and one lookup by attnum,
which seems wasteful.
---
 src/backend/statistics/attribute_stats.c | 47 ++++++++++--------------
 1 file changed, 20 insertions(+), 27 deletions(-)

diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index 42f458c280..1d39281c03 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -78,8 +78,8 @@ static struct StatsArgInfo attarginfo[] =
 
 static bool attribute_statistics_update(FunctionCallInfo fcinfo, int elevel);
 static Node *get_attr_expr(Relation rel, int attnum);
-static void get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
-							   Oid *atttypid, int32 *atttypmod,
+static void get_attr_stat_type(Oid reloid, Name attname, int elevel,
+							   AttrNumber *attnum, Oid *atttypid, int32 *atttypmod,
 							   char *atttyptype, Oid *atttypcoll,
 							   Oid *eq_opr, Oid *lt_opr);
 static bool get_elem_stat_type(Oid atttypid, char atttyptype, int elevel,
@@ -160,17 +160,16 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 
 	stats_check_required_arg(fcinfo, attarginfo, ATTNAME_ARG);
 	attname = PG_GETARG_NAME(ATTNAME_ARG);
-	attnum = get_attnum(reloid, NameStr(*attname));
-	/* maybe use (!AttrNumberIsForUserDefinedAttr(attnum)) instead? */
-	if (attnum <= InvalidAttrNumber)
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_COLUMN),
-				 errmsg("column \"%s\" of relation \"%s\" does not exist",
-						NameStr(*attname), get_rel_name(reloid))));
 
 	stats_check_required_arg(fcinfo, attarginfo, INHERITED_ARG);
 	inherited = PG_GETARG_BOOL(INHERITED_ARG);
 
+	/* derive type information from attribute */
+	get_attr_stat_type(reloid, attname, elevel,
+					   &attnum, &atttypid, &atttypmod,
+					   &atttyptype, &atttypcoll,
+					   &eq_opr, &lt_opr);
+
 	/*
 	 * Check argument sanity. If some arguments are unusable, emit at elevel
 	 * and set the corresponding argument to NULL in fcinfo.
@@ -220,12 +219,6 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 		result = false;
 	}
 
-	/* derive information from attribute */
-	get_attr_stat_type(reloid, attnum, elevel,
-					   &atttypid, &atttypmod,
-					   &atttyptype, &atttypcoll,
-					   &eq_opr, &lt_opr);
-
 	/* if needed, derive element type */
 	if (do_mcelem || do_dechist)
 	{
@@ -491,8 +484,8 @@ get_attr_expr(Relation rel, int attnum)
  * Derive type information from the attribute.
  */
 static void
-get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
-				   Oid *atttypid, int32 *atttypmod,
+get_attr_stat_type(Oid reloid, Name attname, int elevel,
+				   AttrNumber *attnum, Oid *atttypid, int32 *atttypmod,
 				   char *atttyptype, Oid *atttypcoll,
 				   Oid *eq_opr, Oid *lt_opr)
 {
@@ -502,24 +495,25 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
 	Node	   *expr;
 	TypeCacheEntry *typcache;
 
-	atup = SearchSysCache2(ATTNUM, ObjectIdGetDatum(reloid),
-						   Int16GetDatum(attnum));
+	atup = SearchSysCacheAttName(reloid, NameStr(*attname));
 
-	/* Attribute not found */
+	/* Attribute not found or is dropped */
 	if (!HeapTupleIsValid(atup))
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
-				 errmsg("attribute %d of relation \"%s\" does not exist",
-						attnum, RelationGetRelationName(rel))));
+				 errmsg("column \"%s\" of relation \"%s\" does not exist",
+						NameStr(*attname), get_rel_name(reloid))));
 
 	attr = (Form_pg_attribute) GETSTRUCT(atup);
 
-	if (attr->attisdropped)
+	/* Reject system columns */
+	if (!AttrNumberIsForUserDefinedAttr(attr->attnum))
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
-				 errmsg("attribute %d of relation \"%s\" does not exist",
-						attnum, RelationGetRelationName(rel))));
+				 errmsg("column \"%s\" of relation \"%s\" does not exist",
+						NameStr(*attname), get_rel_name(reloid))));
 
+	*attnum = attr->attnum;
 	expr = get_attr_expr(rel, attr->attnum);
 
 	/*
@@ -871,8 +865,7 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 	stats_check_required_arg(fcinfo, attarginfo, ATTNAME_ARG);
 	attname = PG_GETARG_NAME(ATTNAME_ARG);
 	attnum = get_attnum(reloid, NameStr(*attname));
-	/* maybe use (!AttrNumberIsForUserDefinedAttr(attnum)) instead? */
-	if (attnum <= InvalidAttrNumber)
+	if (!AttrNumberIsForUserDefinedAttr(attnum))
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 errmsg("column \"%s\" of relation \"%s\" does not exist",
-- 
2.47.0

v32-0014-Add-no-data-option.patchtext/x-patch; charset=US-ASCII; name=v32-0014-Add-no-data-option.patchDownload
From 4e0ae8e031e9b4c753b5a0bb15b07bca87bfa530 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 14 Nov 2024 04:58:17 -0500
Subject: [PATCH v32 14/14] Add --no-data option.

This option is useful for situations where someone wishes to test
query plans from a production database without copying production data.
---
 src/bin/pg_dump/pg_backup.h          | 2 ++
 src/bin/pg_dump/pg_backup_archiver.c | 2 ++
 src/bin/pg_dump/pg_dump.c            | 7 ++++++-
 src/bin/pg_dump/pg_restore.c         | 5 ++++-
 4 files changed, 14 insertions(+), 2 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index b03b69ff00..ac19260715 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -110,6 +110,7 @@ typedef struct _restoreOptions
 	int			column_inserts;
 	int			if_exists;
 	int			no_comments;	/* Skip comments */
+	int			no_data;			/* Skip data */
 	int			no_publications;	/* Skip publication entries */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
@@ -185,6 +186,7 @@ typedef struct _dumpOptions
 	int			no_publications;
 	int			no_subscriptions;
 	int			no_statistics;
+	int			no_data;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 4edcb4eca5..a448a84667 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -184,6 +184,8 @@ dumpOptionsFromRestoreOptions(RestoreOptions *ropt)
 	dopt->no_publications = ropt->no_publications;
 	dopt->no_security_labels = ropt->no_security_labels;
 	dopt->no_subscriptions = ropt->no_subscriptions;
+	dopt->no_data = ropt->no_data;
+	dopt->no_statistics = ropt->no_statistics;
 	dopt->lockWaitTimeout = ropt->lockWaitTimeout;
 	dopt->include_everything = ropt->include_everything;
 	dopt->enable_row_security = ropt->enable_row_security;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 25df8d56cf..597a6564bc 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -494,6 +494,7 @@ main(int argc, char **argv)
 		{"strict-names", no_argument, &strict_names, 1},
 		{"use-set-session-authorization", no_argument, &dopt.use_setsessauth, 1},
 		{"no-comments", no_argument, &dopt.no_comments, 1},
+		{"no-data", no_argument, &dopt.no_data, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
 		{"no-statistics", no_argument, &dopt.no_statistics, 1},
@@ -796,6 +797,9 @@ main(int argc, char **argv)
 	if (data_only && statistics_only)
 		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
 
+	if (data_only && dopt.no_data)
+		pg_fatal("options -a/--data-only and --no-data cannot be used together");
+
 	if (statistics_only && dopt.no_statistics)
 		pg_fatal("options -X/--statistics-only and --no-statistics cannot be used together");
 
@@ -812,7 +816,7 @@ main(int argc, char **argv)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
 	/* set derivative flags */
-	dopt.dumpData = data_only || (!schema_only && !statistics_only);
+	dopt.dumpData = data_only || (!schema_only && !statistics_only && !dopt.no_data);
 	dopt.dumpSchema = schema_only || (!data_only && !statistics_only);
 
 	if (statistics_only)
@@ -1247,6 +1251,7 @@ help(const char *progname)
 	printf(_("  --inserts                    dump data as INSERT commands, rather than COPY\n"));
 	printf(_("  --load-via-partition-root    load partitions via the root table\n"));
 	printf(_("  --no-comments                do not dump comments\n"));
+	printf(_("  --no-data                    do not dump data\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
 	printf(_("  --no-statistics              do not dump statistics\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index f8f16facc1..cfb2b34e32 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -71,6 +71,7 @@ main(int argc, char **argv)
 	static int	outputNoTableAm = 0;
 	static int	outputNoTablespaces = 0;
 	static int	use_setsessauth = 0;
+	static int	no_data = 0;
 	static int	no_comments = 0;
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
@@ -133,6 +134,7 @@ main(int argc, char **argv)
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
 		{"no-statistics", no_argument, &no_statistics, 1},
+		{"no-data", no_argument, &no_data, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -376,7 +378,7 @@ main(int argc, char **argv)
 
 	/* set derivative flags */
 	opts->dumpSchema = (!data_only && !statistics_only);
-	opts->dumpData = (!schema_only && !statistics_only);
+	opts->dumpData = (!no_data && !schema_only && !statistics_only);
 	opts->dumpStatistics = (!no_statistics && !data_only && !schema_only);
 
 	opts->disable_triggers = disable_triggers;
@@ -390,6 +392,7 @@ main(int argc, char **argv)
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
 	opts->no_statistics = no_statistics;
+	opts->no_data = no_data;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
-- 
2.47.0

#228Bruce Momjian
bruce@momjian.us
In reply to: Corey Huinker (#227)
Re: Statistics Import and Export

On Mon, Nov 18, 2024 at 08:29:10PM -0500, Corey Huinker wrote:

On Mon, Nov 18, 2024 at 2:47 PM Bruce Momjian <bruce@momjian.us> wrote:
You seem to be optimizing for people using pg_upgrade, and for people
upgrading to PG 18, without adequately considering people using vacuumdb
in non-pg_upgrade situations, and people using PG 19+.  Let me explain.

This was a concern as I was polling people.

A person using vacuumdb in a non-upgrade situation is, to my limited
imagination, one of three types:

1. A person who views vacuumdb as a worthwhile janitorial task for downtimes.
2. A person who wants stats on a lot of recently created tables.
3. A person who wants better stats on a lot of recently (re)populated tables.

The first group would not be using --analyze-in-stages or --analyze-only,
because the vacuuming is a big part of it. They will be unaffected.

The second group will be pleasantly surprised to learn that they no longer need
to specify a subset of tables, as any table missing stats will get picked up.

The third group would be surprised that their operation completed so quickly,
check the docs, add in --force-analyze to their script, and re-run.

We can't design an API around who is going to be surprised. We have to
look at what the options say, what people would expect it to do, and
what it does. The reason "surprise" doesn't work in the long run is
that while PG 18 users might be surprised, PG 20 users will be confused.

First, I see little concern here for how people who use --analyze and
--analyze-only independent of pg_upgrade will be affected by this.
While I recommend people decrease vacuum and analyze threshold during
non-peak periods:

        https://momjian.us/main/blogs/pgblog/2017.html#January_3_2017

some people might just regenerate all statistics during non-peak periods
using these options.  You can perhaps argue that --analyze-in-stages
would only be used by pg_upgrade so maybe that can be adjusted more
easily.

I, personally, would be fine if this only modified --analyze-in-stages, as it
already carries the warning:

Right, but I think we would need to rename the option to clarify what it
does, e.g. --analyze-missing-in-stages. If they use
--analyze-in-stages, they will get an error, and will then need to
reference the docs to see the new option wording, or we can suggest the
new option in the error message.

But others felt that --analyze-only should be in the mix as well.

Again, with those other people not saying so in this thread, I can't
really comment on it --- I can only tell you what I have seen and others
are going to have to explain why they want such dramatic changes.

No one advocated for changing the behavior of options that involve actual
vacuuming.
 

Second, the API for what --analyze and --analyze-only do will be very
confusing for people running, e.g., PG 20, because the average user
reading the option name will not guess it only adds missing statistics.

I think you need to rethink your approach and just accept that a mention
of the new preserving statistic behavior of pg_upgrade, and the new
vacuumdb API required, will be sufficient.  In summary, I think you need
a new --compute-missing-statistics-only that can be combined with
--analyze, --analyze-only, and --analyze-in-stages to compute only
missing statistics, and document it in the PG 18 release notes.

A --missing-only/--analyze-missing-in-stages option was my first idea, and it's
definitely cleaner, but as I stated in the rejected ideas section above, when I
reached out to others at PgConf.EU there was pretty general consensus that few
people would actually read our documentation, and the few that have in the past
are unlikely to read it again to discover the new option, and those people
would have a negative impact of using --analyze-in-stages, effectively
punishing them for having once read the documentation (or a blog post) but not
re-read it prior to upgrade.

Again, you can't justify such changes based on discussions that are not
posted publicly here.

So, to add non-pg_upgrade users to the outcome tree in my email from
2024-11-04:

5. Users who use vacuumdb in a non-upgrade situation and do not use either
--analyze-in-stages or --analyze-only will be completely unaffected.
6. Users who use vacuumdb in a non-upgrade situation with either
--analyze-in-stages or --analyze-only set will find that the operation
skips tables that already have stats, and will have to add --force-analyze
to restore previous behavior.

That's not a great surprise for group 6, but I have to believe that group is
smaller than group 5, and it's definitely smaller than the group of users that
need to upgrade.

Again, a clean API is the goal, not surprise calculus.

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

When a patient asks the doctor, "Am I going to die?", he means
"Am I going to die soon?"

#229Bruce Momjian
bruce@momjian.us
In reply to: Bruce Momjian (#228)
Re: Statistics Import and Export

On Mon, Nov 18, 2024 at 08:42:35PM -0500, Bruce Momjian wrote:

On Mon, Nov 18, 2024 at 08:29:10PM -0500, Corey Huinker wrote:

That's not a great surprise for group 6, but I have to believe that group is
smaller than group 5, and it's definitely smaller than the group of users that
need to upgrade.

Again, a clean API is the goal, not surprise calculus.

Maybe I was too harsh. "Surprise calculus" is fine, but only after we
have an API that will be clearly understood by new users. We have to
assume that in the long run new users will use this API more than
existing users.

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

When a patient asks the doctor, "Am I going to die?", he means
"Am I going to die soon?"

#230Nathan Bossart
nathandbossart@gmail.com
In reply to: Bruce Momjian (#228)
Re: Statistics Import and Export

On Mon, Nov 18, 2024 at 08:42:35PM -0500, Bruce Momjian wrote:

We can't design an API around who is going to be surprised. We have to
look at what the options say, what people would expect it to do, and
what it does. The reason "surprise" doesn't work in the long run is
that while PG 18 users might be surprised, PG 20 users will be confused.

I think Bruce makes good points. I'd add that even if we did nothing at
all for vacuumdb, folks who continued to use it wouldn't benefit from the
new changes, but they also shouldn't be harmed by it, either.

I, personally, would be fine if this only modified --analyze-in-stages, as it
already carries the warning:

Right, but I think we would need to rename the option to clarify what it
does, e.g. --analyze-missing-in-stages. If they use
--analyze-in-stages, they will get an error, and will then need to
reference the docs to see the new option wording, or we can suggest the
new option in the error message.

But others felt that --analyze-only should be in the mix as well.

Again, with those other people not saying so in this thread, I can't
really comment on it --- I can only tell you what I have seen and others
are going to have to explain why they want such dramatic changes.

I don't have a strong opinion here, but I suspect that if I was creating
vacuumdb from scratch, I'd have suggested a --missing-only flag that would
only work for --analyze-only/--analyze-in-stages. That way, folks can
still regenerate statistics if they want, but we also have an answer for
folks who use pg_upgrade and have extended statistics.

--
nathan

#231Corey Huinker
corey.huinker@gmail.com
In reply to: Nathan Bossart (#230)
Re: Statistics Import and Export

I don't have a strong opinion here, but I suspect that if I was creating
vacuumdb from scratch, I'd have suggested a --missing-only flag that would
only work for --analyze-only/--analyze-in-stages. That way, folks can
still regenerate statistics if they want, but we also have an answer for
folks who use pg_upgrade and have extended statistics.

(combining responses to Bruce's para about surprise calculus and Nathan
here)

I agree that a clean API is desirable and a goal. And as I stated before, a
new flag (--analyze-missing-in-stages / --analyze-post-pgupgrade, etc) or a
flag modifier ( --missing-only ) was my first choice.

But if we're going to go that route, we have a messaging problem. We need
to reach our customers who plan to upgrade, and explain to them that the
underlying assumption behind running vacuumdb has gone away for 99% of
them, and that may be 100% in the next version, but for that 99% running
vacuumdb in the old way now actively undoes one of the major improvements
to pg_upgrade, but this one additional option keeps the benefits of the new
pg_upgrade without the drawbacks.

That, and once we have extended statistics importing on upgrade, then the
need for vacuumdb post-upgrade goes away entirely. So we'll have to
re-message the users with that news too.

I'd be in favor of this, but I have to be honest, our messaging reach is
not good, and takes years to sink in. Years in which the message will
change at least one more time. And this outreach will likely confuse users
who already weren't (and now shouldn't be) using vacuumdb. In light of
that, the big risk was that an action that some users learned to do years
ago was now actively undoing whatever gains they were supposed to get in
their upgrade downtime, and that downtime is money to them, hence the
surprise calculus.

One other possibilities we could consider:

* create a pg_stats_health_check script that lists tables missing stats,
with --fix/--fix-in-stages options, effectively replacing vacuumdb for
those purposes, and then crank up the messaging about that change. The "new
shiny" effect of a new utility that has "stats", "health", and "check" in
the name may be the search/click-bait we need to get the word out
effectively. That last sentence may sound facetious, but it isn't, it's
just accepting how search engines and eyeballs currently function. With
that in place, we can then change the vacuumdb documentation to be deter
future use in post-upgrade situations.

* move missing-stats rebuilds into pg_upgrade/pg_restore itself, and this
would give us the simpler one-time message that users should stop using
vacuumdb in upgrade situations.

* Making a concerted push to get extended stats import into v18 despite the
high-effort/low-reward nature of it, and then we can go with the simple
messaging of "Remember vacuumdb, that thing you probably weren't running
post-upgrade but should have been? Now you can stop using it!". I had
extended stats imports working back when the function took JSON input, so
it's do-able, but the difficulty lies in how to represent an array of
incomplete pg_statistic rows in a serial fashion that is cross-version
compatible.

#232Bruce Momjian
bruce@momjian.us
In reply to: Corey Huinker (#231)
Re: Statistics Import and Export

On Tue, Nov 19, 2024 at 03:47:20PM -0500, Corey Huinker wrote:

I don't have a strong opinion here, but I suspect that if I was creating
vacuumdb from scratch, I'd have suggested a --missing-only flag that would
only work for --analyze-only/--analyze-in-stages.  That way, folks can
still regenerate statistics if they want, but we also have an answer for
folks who use pg_upgrade and have extended statistics.

(combining responses to Bruce's para about surprise calculus and Nathan here)

I agree that a clean API is desirable and a goal. And as I stated before, a new
flag (--analyze-missing-in-stages / --analyze-post-pgupgrade, etc) or a flag
modifier ( --missing-only ) was my first choice.

Yes, after a clean API is designed, you can then consider surprise
calculus. This is an issue not only for this feature, but for all
Postgres changes we consider, which is why I think it is worth stating
this clearly. If I am thinking incorrectly, we can discuss that here too.

But if we're going to go that route, we have a messaging problem. We need to
reach our customers who plan to upgrade, and explain to them that the
underlying assumption behind running vacuumdb has gone away for 99% of them,
and that may be 100% in the next version, but for that 99% running vacuumdb in
the old way now actively undoes one of the major improvements to pg_upgrade,
but this one additional option keeps the benefits of the new pg_upgrade without
the drawbacks.

How much are we supposed to consider users who do not read the major
release notes? I realize we might be unrealistic to expect that from
the majority of our users, but I also don't want to contort our API to
adjust for them.

That, and once we have extended statistics importing on upgrade, then the need
for vacuumdb post-upgrade goes away entirely. So we'll have to re-message the
users with that news too.

I'd be in favor of this, but I have to be honest, our messaging reach is not
good, and takes years to sink in. Years in which the message will change at
least one more time. And this outreach will likely confuse users who already
weren't (and now shouldn't be) using vacuumdb. In light of that, the big risk
was that an action that some users learned to do years ago was now actively
undoing whatever gains they were supposed to get in their upgrade downtime, and
that downtime is money to them, hence the surprise calculus.

That is a big purpose of the major release notes. We can even list this
as an incompatibility in the sense that the procedure has changed.

One other possibilities we could consider:

* create a pg_stats_health_check script that lists tables missing stats, with
--fix/--fix-in-stages options, effectively replacing vacuumdb for those
purposes, and then crank up the messaging about that change. The "new shiny"
effect of a new utility that has "stats", "health", and "check" in the name may
be the search/click-bait we need to get the word out effectively. That last
sentence may sound facetious, but it isn't, it's just accepting how search
engines and eyeballs currently function. With that in place, we can then change
the vacuumdb documentation to be deter future use in post-upgrade situations.

We used to create a script until the functionality was added to
vacuumdb. Since 99% of users will not need to do anything after
pg_upgrade, it would make sense to output the script only for the 1% of
users who need it and tell users to run it, rather than giving
instructions that are a no-op for 99% of users.

* move missing-stats rebuilds into pg_upgrade/pg_restore itself, and this would
give us the simpler one-time message that users should stop using vacuumdb in
upgrade situations.

Uh, that would make pg_upgrade take longer for some users, which might
be confusing.

* Making a concerted push to get extended stats import into v18 despite the
high-effort/low-reward nature of it, and then we can go with the simple
messaging of "Remember vacuumdb, that thing you probably weren't running
post-upgrade but should have been? Now you can stop using it!". I had extended
stats imports working back when the function took JSON input, so it's do-able,
but the difficulty lies in how to represent an array of incomplete pg_statistic
rows in a serial fashion that is cross-version compatible.

I am not a big fan of that at this point. If we get it, we can adjust
our API at that time, but I don't want to plan on it.

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

When a patient asks the doctor, "Am I going to die?", he means
"Am I going to die soon?"

#233Bruce Momjian
bruce@momjian.us
In reply to: Bruce Momjian (#232)
Re: Statistics Import and Export

On Tue, Nov 19, 2024 at 05:40:20PM -0500, Bruce Momjian wrote:

On Tue, Nov 19, 2024 at 03:47:20PM -0500, Corey Huinker wrote:

* create a pg_stats_health_check script that lists tables missing stats, with
--fix/--fix-in-stages options, effectively replacing vacuumdb for those
purposes, and then crank up the messaging about that change. The "new shiny"
effect of a new utility that has "stats", "health", and "check" in the name may
be the search/click-bait we need to get the word out effectively. That last
sentence may sound facetious, but it isn't, it's just accepting how search
engines and eyeballs currently function. With that in place, we can then change
the vacuumdb documentation to be deter future use in post-upgrade situations.

We used to create a script until the functionality was added to
vacuumdb. Since 99% of users will not need to do anything after
pg_upgrade, it would make sense to output the script only for the 1% of
users who need it and tell users to run it, rather than giving
instructions that are a no-op for 99% of users.

One problem with the above approach is that it gives users upgrading or
loading via pg_dump no way to know which tables need analyze statistics,
right? I think that is why we ended up putting the pg_upgrade
statistics functionality in vacuumdb --analyze-in-stages.

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

When a patient asks the doctor, "Am I going to die?", he means
"Am I going to die soon?"

#234Nathan Bossart
nathandbossart@gmail.com
In reply to: Bruce Momjian (#233)
Re: Statistics Import and Export

I took another look at v32-0001 and v32-0002, and they look reasonable to
me. Unless additional feedback materializes, I'll plan on committing those
soon.

After that, it might be a good idea to take up the vacuumdb changes next,
since there's been quite a bit of recent discussion about those. I have a
fair amount of experience working on vacuumdb, so I could probably help
there, too.

--
nathan

#235Jeff Davis
pgsql@j-davis.com
In reply to: Nathan Bossart (#234)
Re: Statistics Import and Export

On Fri, 2024-11-22 at 15:09 -0600, Nathan Bossart wrote:

I took another look at v32-0001 and v32-0002, and they look
reasonable to
me.  Unless additional feedback materializes, I'll plan on committing
those
soon.

Those refactoring patches look fine to me, the only comment I have is
that they are easier to understand as a single patch.

Regards,
Jeff Davis

#236Nathan Bossart
nathandbossart@gmail.com
In reply to: Jeff Davis (#235)
Re: Statistics Import and Export

On Mon, Nov 25, 2024 at 08:11:09AM -0800, Jeff Davis wrote:

On Fri, 2024-11-22 at 15:09 -0600, Nathan Bossart wrote:

I took another look at v32-0001 and v32-0002, and they look
reasonable to
me.� Unless additional feedback materializes, I'll plan on committing
those
soon.

Those refactoring patches look fine to me, the only comment I have is
that they are easier to understand as a single patch.

Yeah, I intend to combine them when committing.

--
nathan

#237Nathan Bossart
nathandbossart@gmail.com
In reply to: Nathan Bossart (#236)
Re: Statistics Import and Export

On Mon, Nov 25, 2024 at 01:12:52PM -0600, Nathan Bossart wrote:

On Mon, Nov 25, 2024 at 08:11:09AM -0800, Jeff Davis wrote:

On Fri, 2024-11-22 at 15:09 -0600, Nathan Bossart wrote:

I took another look at v32-0001 and v32-0002, and they look
reasonable to
me.� Unless additional feedback materializes, I'll plan on committing
those
soon.

Those refactoring patches look fine to me, the only comment I have is
that they are easier to understand as a single patch.

Yeah, I intend to combine them when committing.

Committed.

--
nathan

#238Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#227)
Re: Statistics Import and Export

On Mon, 2024-11-18 at 20:29 -0500, Corey Huinker wrote:

Attached is a re-basing of the existing patchset, plus 3 more
additions:

Comments on 0003:

* If we commit 0003, is it a useful feature by itself or does it
require that we commit some or all of 0004-0014? Which of these need to
be in v18?

* Why does binary upgrade cause statistics to be dumped? Can you just
make pg_upgrade specify the appropriate set of flags?

* It looks like appendNamedArgument() is never called with
positional=true?

* It's pretty awkward to use an array of arrays of strings when an
array of structs might make more sense.

Regards,
Jeff Davis

#239Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#238)
Re: Statistics Import and Export

Comments on 0003:

* If we commit 0003, is it a useful feature by itself or does it
require that we commit some or all of 0004-0014? Which of these need to
be in v18?

Useful by itself.

0004 seems needed to me, unless we're fine with ~50% bloat in pg_class on a
new-upgraded system, or we think inplace update are on their way out.

0005 is basically theoretical, it is only needed if we change the default
relpages on partitioned tables.

0006-0011 are the vacuumdb things being debated now. I've reached out to
some of the people I spoke to at PgConf.eu to get them to chime in.

0012 is now moot as a similar patch was committed Friday.

0013 is a cleanup/optimization.

0014 is the --no-data flag, which has no meaning without 0004, but 0004 in
no way requires it.

* Why does binary upgrade cause statistics to be dumped? Can you just
make pg_upgrade specify the appropriate set of flags?

That decision goes back a ways, I tried to dig in the archives last night
but I was getting a Server Error on postgresql.org.

Today I'm coming up with
/messages/by-id/267624.1711756062@sss.pgh.pa.us
which is actually more about whether stats import should be the default
(consensus: yesyesyes), and the binary_upgrade test may have been because
binary_upgrade shuts off data section stuff, but table stats are in the
data section. Happy to review the decision.

* It looks like appendNamedArgument() is never called with
positional=true?

That is currently the case. Currently all params are called with name/value
pairs, but in the past we had leading positionals followed by the stat-y
parameters in name-value pairs. I'll be refactoring it to remove the
positonal=T/F argument, which leaves just a list of name-type pairs, and
thus probably reduces the urge to make it an array of structs.

* It's pretty awkward to use an array of arrays of strings when an
array of structs might make more sense.

That pattern was introduced here:
/messages/by-id/4afa70edab849ff16238d1100b6652404e9a4d9d.camel@j-davis.com
:)

I'll be rebasing (that's done) and refactoring 0003 to get rid of the
positional column, and moving 0014 next to 0003 because they touch the same
files.

#240Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#239)
11 attachment(s)
Re: Statistics Import and Export

I'll be rebasing (that's done) and refactoring 0003 to get rid of the
positional column, and moving 0014 next to 0003 because they touch the same
files.

As promised, rebased (as of 8fcd80258bcf43dab93d877a5de0ce3f4d2bd471)

Things have been reordered here in a mostly-priority order:

0001 - Enable dumping stats in pg_dump/pg_upgrade. Now with less dead code!
0002 - The dump --no-data option. Very handy for diagnosing customer query
plans, but not in the critical path for v18. Still, so useful that we ought
to include it.
0003 - Re-enabling in-place updates because catalog bloat bad.
0004 - Combine two syscache lookups into one. Not strictly necessary, but
the second lookup is redundant if the first one grabs what we need.
0005-0010 vacuumdb changes (still up for debate) in baby steps.
0011 - The fix for clearing relation stats if the default for relpages
becomes -1 on inherited tables (which it hasn't, yet, and might never).

Attachments:

v33-0002-Add-no-data-option.patchtext/x-patch; charset=US-ASCII; name=v33-0002-Add-no-data-option.patchDownload
From 0f616d4d44f7e29517ac348ded09da1fef1bcbd9 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 14 Nov 2024 04:58:17 -0500
Subject: [PATCH v33 02/11] Add --no-data option.

This option is useful for situations where someone wishes to test
query plans from a production database without copying production data.
---
 src/bin/pg_dump/pg_backup.h          | 2 ++
 src/bin/pg_dump/pg_backup_archiver.c | 2 ++
 src/bin/pg_dump/pg_dump.c            | 7 ++++++-
 src/bin/pg_dump/pg_restore.c         | 5 ++++-
 4 files changed, 14 insertions(+), 2 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 8fbb39d399..241855d017 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -110,6 +110,7 @@ typedef struct _restoreOptions
 	int			column_inserts;
 	int			if_exists;
 	int			no_comments;	/* Skip comments */
+	int			no_data;			/* Skip data */
 	int			no_publications;	/* Skip publication entries */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
@@ -185,6 +186,7 @@ typedef struct _dumpOptions
 	int			no_publications;
 	int			no_subscriptions;
 	int			no_statistics;
+	int			no_data;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index af8bb6bd12..b045c7aa7d 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -186,6 +186,8 @@ dumpOptionsFromRestoreOptions(RestoreOptions *ropt)
 	dopt->no_publications = ropt->no_publications;
 	dopt->no_security_labels = ropt->no_security_labels;
 	dopt->no_subscriptions = ropt->no_subscriptions;
+	dopt->no_data = ropt->no_data;
+	dopt->no_statistics = ropt->no_statistics;
 	dopt->lockWaitTimeout = ropt->lockWaitTimeout;
 	dopt->include_everything = ropt->include_everything;
 	dopt->enable_row_security = ropt->enable_row_security;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 40f75130b2..161eb80859 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -494,6 +494,7 @@ main(int argc, char **argv)
 		{"strict-names", no_argument, &strict_names, 1},
 		{"use-set-session-authorization", no_argument, &dopt.use_setsessauth, 1},
 		{"no-comments", no_argument, &dopt.no_comments, 1},
+		{"no-data", no_argument, &dopt.no_data, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
 		{"no-statistics", no_argument, &dopt.no_statistics, 1},
@@ -796,6 +797,9 @@ main(int argc, char **argv)
 	if (data_only && statistics_only)
 		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
 
+	if (data_only && dopt.no_data)
+		pg_fatal("options -a/--data-only and --no-data cannot be used together");
+
 	if (statistics_only && dopt.no_statistics)
 		pg_fatal("options -X/--statistics-only and --no-statistics cannot be used together");
 
@@ -812,7 +816,7 @@ main(int argc, char **argv)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
 	/* set derivative flags */
-	dopt.dumpData = data_only || (!schema_only && !statistics_only);
+	dopt.dumpData = data_only || (!schema_only && !statistics_only && !dopt.no_data);
 	dopt.dumpSchema = schema_only || (!data_only && !statistics_only);
 
 	if (statistics_only)
@@ -1247,6 +1251,7 @@ help(const char *progname)
 	printf(_("  --inserts                    dump data as INSERT commands, rather than COPY\n"));
 	printf(_("  --load-via-partition-root    load partitions via the root table\n"));
 	printf(_("  --no-comments                do not dump comment commands\n"));
+	printf(_("  --no-data                    do not dump data\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
 	printf(_("  --no-statistics              do not dump statistics\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 355f0439da..31c3cd32de 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -71,6 +71,7 @@ main(int argc, char **argv)
 	static int	outputNoTableAm = 0;
 	static int	outputNoTablespaces = 0;
 	static int	use_setsessauth = 0;
+	static int	no_data = 0;
 	static int	no_comments = 0;
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
@@ -132,6 +133,7 @@ main(int argc, char **argv)
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
 		{"no-statistics", no_argument, &no_statistics, 1},
+		{"no-data", no_argument, &no_data, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -375,7 +377,7 @@ main(int argc, char **argv)
 
 	/* set derivative flags */
 	opts->dumpSchema = (!data_only && !statistics_only);
-	opts->dumpData = (!schema_only && !statistics_only);
+	opts->dumpData = (!no_data && !schema_only && !statistics_only);
 	opts->dumpStatistics = (!no_statistics && !data_only && !schema_only);
 
 	opts->disable_triggers = disable_triggers;
@@ -389,6 +391,7 @@ main(int argc, char **argv)
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
 	opts->no_statistics = no_statistics;
+	opts->no_data = no_data;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
-- 
2.47.0

v33-0003-Enable-in-place-updates-for-pg_restore_relation_.patchtext/x-patch; charset=US-ASCII; name=v33-0003-Enable-in-place-updates-for-pg_restore_relation_.patchDownload
From 4538ce000c39c53ec23bfbe9c903f5e47d28f573 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Mon, 4 Nov 2024 14:02:57 -0500
Subject: [PATCH v33 03/11] Enable in-place updates for
 pg_restore_relation_stats.

This matches the behavior of the ANALYZE command, and would avoid
bloating pg_class in an upgrade situation wherein
pg_restore_relation_stats would be called for nearly every relation in
the database.
---
 src/backend/statistics/relation_stats.c | 72 +++++++++++++++++++------
 1 file changed, 55 insertions(+), 17 deletions(-)

diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index e619d5cf5b..24f646048c 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -22,6 +22,7 @@
 #include "statistics/stat_utils.h"
 #include "utils/fmgrprotos.h"
 #include "utils/syscache.h"
+#include "utils/fmgroids.h"
 
 #define DEFAULT_RELPAGES Int32GetDatum(0)
 #define DEFAULT_RELTUPLES Float4GetDatum(-1.0)
@@ -50,13 +51,14 @@ static struct StatsArgInfo relarginfo[] =
 	[NUM_RELATION_STATS_ARGS] = {0}
 };
 
-static bool relation_statistics_update(FunctionCallInfo fcinfo, int elevel);
+static bool relation_statistics_update(FunctionCallInfo fcinfo, int elevel,
+									   bool inplace);
 
 /*
  * Internal function for modifying statistics for a relation.
  */
 static bool
-relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
+relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
 {
 	Oid			reloid;
 	Relation	crel;
@@ -68,6 +70,7 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	int			ncols = 0;
 	TupleDesc	tupdesc;
 	bool		result = true;
+	void	   *inplace_state;
 
 	stats_check_required_arg(fcinfo, relarginfo, RELATION_ARG);
 	reloid = PG_GETARG_OID(RELATION_ARG);
@@ -87,7 +90,20 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	crel = table_open(RelationRelationId, RowExclusiveLock);
 
 	tupdesc = RelationGetDescr(crel);
-	ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(reloid));
+	if (inplace)
+	{
+		ScanKeyData key[1];
+
+		ctup = NULL;
+
+		ScanKeyInit(&key[0], Anum_pg_class_oid, BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(reloid));
+		systable_inplace_update_begin(crel, ClassOidIndexId, true, NULL, 1, key,
+									  &ctup, &inplace_state);
+	}
+	else
+		ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(reloid));
+
 	if (!HeapTupleIsValid(ctup))
 	{
 		ereport(elevel,
@@ -118,8 +134,13 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 		}
 		else if (relpages != pgcform->relpages)
 		{
-			replaces[ncols] = Anum_pg_class_relpages;
-			values[ncols] = Int32GetDatum(relpages);
+			if (inplace)
+				pgcform->relpages = relpages;
+			else
+			{
+				replaces[ncols] = Anum_pg_class_relpages;
+				values[ncols] = Int32GetDatum(relpages);
+			}
 			ncols++;
 		}
 	}
@@ -137,8 +158,13 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 		}
 		else if (reltuples != pgcform->reltuples)
 		{
-			replaces[ncols] = Anum_pg_class_reltuples;
-			values[ncols] = Float4GetDatum(reltuples);
+			if (inplace)
+				pgcform->reltuples = reltuples;
+			else
+			{
+				replaces[ncols] = Anum_pg_class_reltuples;
+				values[ncols] = Float4GetDatum(reltuples);
+			}
 			ncols++;
 		}
 
@@ -157,8 +183,13 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 		}
 		else if (relallvisible != pgcform->relallvisible)
 		{
-			replaces[ncols] = Anum_pg_class_relallvisible;
-			values[ncols] = Int32GetDatum(relallvisible);
+			if (inplace)
+				pgcform->relallvisible = relallvisible;
+			else
+			{
+				replaces[ncols] = Anum_pg_class_relallvisible;
+				values[ncols] = Int32GetDatum(relallvisible);
+			}
 			ncols++;
 		}
 	}
@@ -166,13 +197,20 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	/* only update pg_class if there is a meaningful change */
 	if (ncols > 0)
 	{
-		HeapTuple	newtup;
+		if (inplace)
+			systable_inplace_update_finish(inplace_state, ctup);
+		else
+		{
+			HeapTuple	newtup;
 
-		newtup = heap_modify_tuple_by_cols(ctup, tupdesc, ncols, replaces, values,
-										   nulls);
-		CatalogTupleUpdate(crel, &newtup->t_self, newtup);
-		heap_freetuple(newtup);
+			newtup = heap_modify_tuple_by_cols(ctup, tupdesc, ncols, replaces, values,
+											nulls);
+			CatalogTupleUpdate(crel, &newtup->t_self, newtup);
+			heap_freetuple(newtup);
+		}
 	}
+	else if (inplace)
+		systable_inplace_update_cancel(inplace_state);
 
 	/* release the lock, consistent with vac_update_relstats() */
 	table_close(crel, RowExclusiveLock);
@@ -188,7 +226,7 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 Datum
 pg_set_relation_stats(PG_FUNCTION_ARGS)
 {
-	relation_statistics_update(fcinfo, ERROR);
+	relation_statistics_update(fcinfo, ERROR, false);
 	PG_RETURN_VOID();
 }
 
@@ -212,7 +250,7 @@ pg_clear_relation_stats(PG_FUNCTION_ARGS)
 	newfcinfo->args[3].value = DEFAULT_RELALLVISIBLE;
 	newfcinfo->args[3].isnull = false;
 
-	relation_statistics_update(newfcinfo, ERROR);
+	relation_statistics_update(newfcinfo, ERROR, false);
 	PG_RETURN_VOID();
 }
 
@@ -230,7 +268,7 @@ pg_restore_relation_stats(PG_FUNCTION_ARGS)
 										  relarginfo, WARNING))
 		result = false;
 
-	if (!relation_statistics_update(positional_fcinfo, WARNING))
+	if (!relation_statistics_update(positional_fcinfo, WARNING, true))
 		result = false;
 
 	PG_RETURN_BOOL(result);
-- 
2.47.0

v33-0005-split-out-check_conn_options.patchtext/x-patch; charset=US-ASCII; name=v33-0005-split-out-check_conn_options.patchDownload
From 0d01cf8eece54da540f221e7d3e93279290fd573 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 6 Nov 2024 15:20:07 -0500
Subject: [PATCH v33 05/11] split out check_conn_options

---
 src/bin/scripts/vacuumdb.c | 103 ++++++++++++++++++++-----------------
 1 file changed, 57 insertions(+), 46 deletions(-)

diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index d07ab7d67e..7b97a9428a 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -10,6 +10,7 @@
  *-------------------------------------------------------------------------
  */
 
+#include "libpq-fe.h"
 #include "postgres_fe.h"
 
 #include <limits.h>
@@ -459,55 +460,11 @@ escape_quotes(const char *src)
 }
 
 /*
- * vacuum_one_database
- *
- * Process tables in the given database.  If the 'tables' list is empty,
- * process all tables in the database.
- *
- * Note that this function is only concerned with running exactly one stage
- * when in analyze-in-stages mode; caller must iterate on us if necessary.
- *
- * If concurrentCons is > 1, multiple connections are used to vacuum tables
- * in parallel.  In this case and if the table list is empty, we first obtain
- * a list of tables from the database.
+ * Check connection options for compatibility with the connected database.
  */
 static void
-vacuum_one_database(ConnParams *cparams,
-					vacuumingOptions *vacopts,
-					int stage,
-					SimpleStringList *objects,
-					int concurrentCons,
-					const char *progname, bool echo, bool quiet)
+check_conn_options(PGconn *conn, vacuumingOptions *vacopts)
 {
-	PQExpBufferData sql;
-	PQExpBufferData buf;
-	PQExpBufferData catalog_query;
-	PGresult   *res;
-	PGconn	   *conn;
-	SimpleStringListCell *cell;
-	ParallelSlotArray *sa;
-	SimpleStringList dbtables = {NULL, NULL};
-	int			i;
-	int			ntups;
-	bool		failed = false;
-	bool		objects_listed = false;
-	const char *initcmd;
-	const char *stage_commands[] = {
-		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
-		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
-		"RESET default_statistics_target;"
-	};
-	const char *stage_messages[] = {
-		gettext_noop("Generating minimal optimizer statistics (1 target)"),
-		gettext_noop("Generating medium optimizer statistics (10 targets)"),
-		gettext_noop("Generating default (full) optimizer statistics")
-	};
-
-	Assert(stage == ANALYZE_NO_STAGE ||
-		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
-
-	conn = connectDatabase(cparams, progname, echo, false, true);
-
 	if (vacopts->disable_page_skipping && PQserverVersion(conn) < 90600)
 	{
 		PQfinish(conn);
@@ -575,6 +532,60 @@ vacuum_one_database(ConnParams *cparams,
 
 	/* skip_database_stats is used automatically if server supports it */
 	vacopts->skip_database_stats = (PQserverVersion(conn) >= 160000);
+}
+
+
+/*
+ * vacuum_one_database
+ *
+ * Process tables in the given database.  If the 'tables' list is empty,
+ * process all tables in the database.
+ *
+ * Note that this function is only concerned with running exactly one stage
+ * when in analyze-in-stages mode; caller must iterate on us if necessary.
+ *
+ * If concurrentCons is > 1, multiple connections are used to vacuum tables
+ * in parallel.  In this case and if the table list is empty, we first obtain
+ * a list of tables from the database.
+ */
+static void
+vacuum_one_database(ConnParams *cparams,
+					vacuumingOptions *vacopts,
+					int stage,
+					SimpleStringList *objects,
+					int concurrentCons,
+					const char *progname, bool echo, bool quiet)
+{
+	PQExpBufferData sql;
+	PQExpBufferData buf;
+	PQExpBufferData catalog_query;
+	PGresult   *res;
+	PGconn	   *conn;
+	SimpleStringListCell *cell;
+	ParallelSlotArray *sa;
+	SimpleStringList dbtables = {NULL, NULL};
+	int			i;
+	int			ntups;
+	bool		failed = false;
+	bool		objects_listed = false;
+	const char *initcmd;
+	const char *stage_commands[] = {
+		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
+		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
+		"RESET default_statistics_target;"
+	};
+	const char *stage_messages[] = {
+		gettext_noop("Generating minimal optimizer statistics (1 target)"),
+		gettext_noop("Generating medium optimizer statistics (10 targets)"),
+		gettext_noop("Generating default (full) optimizer statistics")
+	};
+
+	Assert(stage == ANALYZE_NO_STAGE ||
+		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
+
+	conn = connectDatabase(cparams, progname, echo, false, true);
+
+	check_conn_options(conn, vacopts);
 
 	if (!quiet)
 	{
-- 
2.47.0

v33-0001-Enable-dumping-of-table-index-stats-in-pg_dump.patchtext/x-patch; charset=US-ASCII; name=v33-0001-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From 789830b90b4b4ac7edaa59555fd80b5e7c1fa5ee Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH v33 01/11] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will generate a statement that
calls pg_set_relation_stats(), and it will generate a series of
statements that call pg_set_attribute_stats(), one per attribute. These
statements will restore the statistics of the current system onto the
destination system.

Adds the command-line options -X / --statistics-only, which are mutually
exclusive to --schema-only and --data-only.

Statistics are not dumped when --schema-only is specified, except during
a binary upgrade.

As is the pattern with pg_dump options, staistics can be disabled using
--no-statistics.

Table statistics are dumped in the data section. This is true even if
dumping stats in a binary upgrade. Index and Materialized View
statistics are dumped in the post-data section.
---
 src/bin/pg_dump/pg_backup.h          |   4 +
 src/bin/pg_dump/pg_backup_archiver.c |   5 +
 src/bin/pg_dump/pg_dump.c            | 375 ++++++++++++++++++++++++++-
 src/bin/pg_dump/pg_dump.h            |   8 +
 src/bin/pg_dump/pg_dump_sort.c       |   5 +
 src/bin/pg_dump/pg_dumpall.c         |   5 +
 src/bin/pg_dump/pg_restore.c         |  18 +-
 src/bin/pg_dump/t/001_basic.pl       |  18 ++
 doc/src/sgml/ref/pg_dump.sgml        |  36 ++-
 doc/src/sgml/ref/pg_restore.sgml     |  31 ++-
 10 files changed, 489 insertions(+), 16 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index f0f19bb0b2..8fbb39d399 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -113,6 +113,7 @@ typedef struct _restoreOptions
 	int			no_publications;	/* Skip publication entries */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
@@ -160,6 +161,7 @@ typedef struct _restoreOptions
 	/* flags derived from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } RestoreOptions;
 
 typedef struct _dumpOptions
@@ -182,6 +184,7 @@ typedef struct _dumpOptions
 	int			no_security_labels;
 	int			no_publications;
 	int			no_subscriptions;
+	int			no_statistics;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
@@ -208,6 +211,7 @@ typedef struct _dumpOptions
 	/* flags derived from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } DumpOptions;
 
 /*
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 33182d5b44..af8bb6bd12 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2962,6 +2962,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's a stats dump, maybe ignore it */
+	if (ropt->no_statistics && strcmp(te->desc, "STATISTICS") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2991,6 +2995,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index add7f16c90..40f75130b2 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -430,6 +430,7 @@ main(int argc, char **argv)
 	DataDirSyncMethod sync_method = DATA_DIR_SYNC_METHOD_FSYNC;
 	bool		data_only = false;
 	bool		schema_only = false;
+	bool		statistics_only = false;
 
 	static DumpOptions dopt;
 
@@ -466,6 +467,7 @@ main(int argc, char **argv)
 		{"encoding", required_argument, NULL, 'E'},
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
+		{"statistics-only", no_argument, NULL, 'X'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -494,6 +496,7 @@ main(int argc, char **argv)
 		{"no-comments", no_argument, &dopt.no_comments, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -539,7 +542,7 @@ main(int argc, char **argv)
 
 	InitDumpOptions(&dopt);
 
-	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:RsS:t:T:U:vwWxZ:",
+	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:PRsS:t:T:U:vwWxXZ:",
 							long_options, &optindex)) != -1)
 	{
 		switch (c)
@@ -613,6 +616,10 @@ main(int argc, char **argv)
 				dopt.cparams.pgport = pg_strdup(optarg);
 				break;
 
+			case 'X':			/* Dump statistics only */
+				statistics_only = true;
+				break;
+
 			case 'R':
 				/* no-op, still accepted for backwards compatibility */
 				break;
@@ -784,6 +791,13 @@ main(int argc, char **argv)
 
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (schema_only && statistics_only)
+		pg_fatal("options -s/--schema-only and -X/--statistics-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
+
+	if (statistics_only && dopt.no_statistics)
+		pg_fatal("options -X/--statistics-only and --no-statistics cannot be used together");
 
 	if (schema_only && foreign_servers_include_patterns.head != NULL)
 		pg_fatal("options -s/--schema-only and --include-foreign-data cannot be used together");
@@ -798,8 +812,20 @@ main(int argc, char **argv)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
 	/* set derivative flags */
-	dopt.dumpSchema = (!data_only);
-	dopt.dumpData = (!schema_only);
+	dopt.dumpData = data_only || (!schema_only && !statistics_only);
+	dopt.dumpSchema = schema_only || (!data_only && !statistics_only);
+
+	if (statistics_only)
+		/* stats are only thing wanted */
+		dopt.dumpStatistics = true;
+	else if (dopt.no_statistics)
+		/* stats specifically excluded */
+		dopt.dumpStatistics = false;
+	else if (dopt.binary_upgrade)
+		/* binary upgrade and not specifically excluded */
+		dopt.dumpStatistics = true;
+	else
+		dopt.dumpStatistics = (!data_only && !schema_only);
 
 	/*
 	 * --inserts are already implied above if --column-inserts or
@@ -1099,6 +1125,7 @@ main(int argc, char **argv)
 	ropt->dropSchema = dopt.outputClean;
 	ropt->dumpData = dopt.dumpData;
 	ropt->dumpSchema = dopt.dumpSchema;
+	ropt->dumpStatistics = dopt.dumpStatistics;
 	ropt->if_exists = dopt.if_exists;
 	ropt->column_inserts = dopt.column_inserts;
 	ropt->dumpSections = dopt.dumpSections;
@@ -1177,7 +1204,7 @@ help(const char *progname)
 	printf(_("  -?, --help                   show this help, then exit\n"));
 
 	printf(_("\nOptions controlling the output content:\n"));
-	printf(_("  -a, --data-only              dump only the data, not the schema\n"));
+	printf(_("  -a, --data-only              dump only the data, not the schema or statistics\n"));
 	printf(_("  -b, --large-objects          include large objects in dump\n"));
 	printf(_("  --blobs                      (same as --large-objects, deprecated)\n"));
 	printf(_("  -B, --no-large-objects       exclude large objects in dump\n"));
@@ -1190,11 +1217,12 @@ help(const char *progname)
 	printf(_("  -N, --exclude-schema=PATTERN do NOT dump the specified schema(s)\n"));
 	printf(_("  -O, --no-owner               skip restoration of object ownership in\n"
 			 "                               plain-text format\n"));
-	printf(_("  -s, --schema-only            dump only the schema, no data\n"));
+	printf(_("  -s, --schema-only            dump only the schema, no data or statistics\n"));
 	printf(_("  -S, --superuser=NAME         superuser user name to use in plain-text format\n"));
 	printf(_("  -t, --table=PATTERN          dump only the specified table(s)\n"));
 	printf(_("  -T, --exclude-table=PATTERN  do NOT dump the specified table(s)\n"));
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
+	printf(_("  -X, --statistics-only        dump only the statistics, not schema or data\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
@@ -1221,6 +1249,7 @@ help(const char *progname)
 	printf(_("  --no-comments                do not dump comment commands\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -6780,6 +6809,42 @@ getFuncs(Archive *fout)
 	destroyPQExpBuffer(query);
 }
 
+/*
+ * getRelationStatistics
+ *    register the statistics object as a dependent of the relation.
+ *
+ */
+static RelStatsInfo *
+getRelationStatistics(Archive *fout, DumpableObject *rel, char relkind)
+{
+	if ((relkind == RELKIND_RELATION) ||
+		(relkind == RELKIND_PARTITIONED_TABLE) ||
+		(relkind == RELKIND_INDEX) ||
+		(relkind == RELKIND_PARTITIONED_INDEX) ||
+		(relkind == RELKIND_MATVIEW))
+	{
+		RelStatsInfo	   *info = pg_malloc0(sizeof(RelStatsInfo));
+		DumpableObject	   *dobj = &info->dobj;
+
+		dobj->objType = DO_REL_STATS;
+		dobj->catId.tableoid = 0;
+		dobj->catId.oid = 0;
+		AssignDumpId(dobj);
+		dobj->dependencies = (DumpId *) pg_malloc(sizeof(DumpId));
+		dobj->dependencies[0] = rel->dumpId;
+		dobj->nDeps = 1;
+		dobj->allocDeps = 1;
+		dobj->components |= DUMP_COMPONENT_STATISTICS;
+		dobj->dump = rel->dump;
+		dobj->name = pg_strdup(rel->name);
+		dobj->namespace = rel->namespace;
+		info->relkind = relkind;
+
+		return info;
+	}
+	return NULL;
+}
+
 /*
  * getTables
  *	  read all the tables (no indexes) in the system catalogs,
@@ -7157,6 +7222,7 @@ getTables(Archive *fout, int *numTables)
 
 		/* Tables have data */
 		tblinfo[i].dobj.components |= DUMP_COMPONENT_DATA;
+		tblinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
 
 		/* Mark whether table has an ACL */
 		if (!PQgetisnull(res, i, i_relacl))
@@ -7205,6 +7271,8 @@ getTables(Archive *fout, int *numTables)
 				}
 			}
 		}
+		if (tblinfo[i].interesting)
+			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relkind);
 	}
 
 	if (query->len != 0)
@@ -7649,11 +7717,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 		for (int c = 0; c < numinds; c++, j++)
 		{
 			char		contype;
+			char		indexkind;
+			RelStatsInfo   *relstats;
 
 			indxinfo[j].dobj.objType = DO_INDEX;
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -7676,7 +7747,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			{
 				NULL, NULL
 			};
+
+			if (indxinfo[j].parentidx == 0)
+				indexkind = RELKIND_INDEX;
+			else
+				indexkind = RELKIND_PARTITIONED_INDEX;
+
 			contype = *(PQgetvalue(res, j, i_contype));
+			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, indexkind);
 
 			if (contype == 'p' || contype == 'u' || contype == 'x')
 			{
@@ -7710,6 +7788,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				constrinfo->separate = true;
 
 				indxinfo[j].indexconstraint = constrinfo->dobj.dumpId;
+				if (relstats != NULL)
+					addObjectDependency(&relstats->dobj, constrinfo->dobj.dumpId);
 			}
 			else
 			{
@@ -10297,6 +10377,276 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * Tabular description of the parameters to pg_restore_relation_stats()
+ * param_name, param_type
+ */
+static const char *rel_stats_arginfo[][2] = {
+	{"relation", "regclass"},
+	{"version", "integer"},
+	{"relpages", "integer"},
+	{"reltuples", "real"},
+	{"relallvisible", "integer"},
+};
+
+/*
+ * Tabular description of the parameters to pg_restore_attribute_stats()
+ * param_name, param_type
+ */
+static const char *att_stats_arginfo[][2] = {
+	{"relation", "regclass"},
+	{"attname", "name"},
+	{"inherited", "boolean"},
+	{"version", "integer"},
+	{"null_frac", "float4"},
+	{"avg_width", "integer"},
+	{"n_distinct", "float4"},
+	{"most_common_vals", "text"},
+	{"most_common_freqs", "float4[]"},
+	{"histogram_bounds", "text"},
+	{"correlation", "float4"},
+	{"most_common_elems", "text"},
+	{"most_common_elem_freqs", "float4[]"},
+	{"elem_count_histogram", "float4[]"},
+	{"range_length_histogram", "text"},
+	{"range_empty_frac", "float4"},
+	{"range_bounds_histogram", "text"},
+};
+
+/*
+ * getRelStatsExportQuery --
+ *
+ * Generate a query that will fetch all relation (e.g. pg_class)
+ * stats for a given relation.
+ */
+static void
+getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "current_setting('server_version_num') AS version, "
+						 "c.relpages, c.reltuples, c.relallvisible "
+						 "FROM pg_class c "
+						 "JOIN pg_namespace n "
+						 "ON n.oid = c.relnamespace "
+						 "WHERE n.nspname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND c.relname = ");
+	appendStringLiteralAH(query, relname, fout);
+}
+
+/*
+ * getAttStatsExportQuery --
+ *
+ * Generate a query that will fetch all attribute (e.g. pg_statistic)
+ * stats for a given relation.
+ */
+static void
+getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "s.attname,"
+						 "s.inherited,"
+						 "current_setting('server_version_num') AS version, "
+						 "s.null_frac,"
+						 "s.avg_width,"
+						 "s.n_distinct,"
+						 "s.most_common_vals,"
+						 "s.most_common_freqs,"
+						 "s.histogram_bounds,"
+						 "s.correlation,"
+						 "s.most_common_elems,"
+						 "s.most_common_elem_freqs,"
+						 "s.elem_count_histogram,");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 "s.range_length_histogram,"
+							 "s.range_empty_frac,"
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(query,
+							 "NULL AS range_length_histogram,"
+							 "NULL AS range_empty_frac,"
+							 "NULL AS range_bounds_histogram ");
+
+	appendPQExpBufferStr(query,
+						 "FROM pg_stats s "
+						 "JOIN pg_namespace n "
+						 "ON n.nspname = s.schemaname "
+						 "JOIN pg_class c "
+						 "ON c.relname = s.tablename "
+						 "AND c.relnamespace = n.oid "
+						 "WHERE s.schemaname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND s.tablename = ");
+	appendStringLiteralAH(query, relname, fout);
+	appendPQExpBufferStr(query, " ORDER BY s.attname, s.inherited");
+}
+
+
+/*
+ * appendNamedArgument --
+ *
+ * Convenience routine for constructing parameters of the form:
+ * 'paraname', 'value'::type
+ */
+static void
+appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
+					const char *argval, const char *argtype)
+{
+	appendPQExpBufferStr(out, "\t");
+
+	appendStringLiteralAH(out, argname, fout);
+	appendPQExpBufferStr(out, ", ");
+
+	appendStringLiteralAH(out, argval, fout);
+	appendPQExpBuffer(out, "::%s", argtype);
+}
+
+/*
+ * appendRelStatsImport --
+ *
+ * Append a formatted pg_restore_relation_stats statement.
+ */
+static void
+appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	const char *sep = "";
+
+	if (PQntuples(res) == 0)
+		return;
+
+	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
+
+	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
+	{
+		const char *argname = rel_stats_arginfo[argno][0];
+		const char *argtype = rel_stats_arginfo[argno][1];
+		int			fieldno = PQfnumber(res, argname);
+
+		if (fieldno < 0)
+			pg_fatal("relation stats export query missing field '%s'",
+					 argname);
+
+		if (PQgetisnull(res, 0, fieldno))
+			continue;
+
+		appendPQExpBufferStr(out, sep);
+		appendNamedArgument(out, fout, argname, PQgetvalue(res, 0, fieldno), argtype);
+
+		sep = ",\n";
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+/*
+ * appendAttStatsImport --
+ *
+ * Append a series of formatted pg_restore_attribute_stats statements.
+ */
+static void
+appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	{
+		const char *sep = "";
+
+		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+		for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
+		{
+			const char *argname = att_stats_arginfo[argno][0];
+			const char *argtype = att_stats_arginfo[argno][1];
+			int			fieldno = PQfnumber(res, argname);
+
+			if (fieldno < 0)
+				pg_fatal("attribute stats export query missing field '%s'",
+						 argname);
+
+			if (PQgetisnull(res, rownum, fieldno))
+				continue;
+
+			appendPQExpBufferStr(out, sep);
+			appendNamedArgument(out, fout, argname, PQgetvalue(res, rownum, fieldno), argtype);
+			sep = ",\n";
+		}
+		appendPQExpBufferStr(out, "\n);\n");
+	}
+}
+
+/*
+ * get statistics dump section, which depends on the parent object type
+ *
+ * objects created in SECTION_PRE_DATA have stats in SECTION_DATA
+ * objects created in SECTION_POST_DATA have stats in SECTION_POST_DATA
+ */
+static teSection
+statisticsDumpSection(const RelStatsInfo *rsinfo)
+{
+
+	if ((rsinfo->relkind == RELKIND_MATVIEW) ||
+		(rsinfo->relkind == RELKIND_INDEX) ||
+		(rsinfo->relkind == RELKIND_PARTITIONED_INDEX))
+			return SECTION_POST_DATA;
+
+	return SECTION_DATA;
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	PGresult	   *res;
+	PQExpBuffer		query;
+	PQExpBuffer		out;
+	PQExpBuffer		tag;
+	DumpableObject *dobj = (DumpableObject *) &rsinfo->dobj;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", "STATISTICS DATA", fmtId(dobj->name));
+
+	query = createPQExpBuffer();
+	out = createPQExpBuffer();
+
+	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendRelStatsImport(out, fout, res);
+	PQclear(res);
+
+	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendAttStatsImport(out, fout, res);
+	PQclear(res);
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATISTICS DATA",
+							  .section = statisticsDumpSection(rsinfo),
+							  .createStmt = out->data,
+							  .deps = dobj->dependencies,
+							  .nDeps = dobj->nDeps));
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(out);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -10745,6 +11095,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_SUBSCRIPTION_REL:
 			dumpSubscriptionTable(fout, (const SubRelInfo *) dobj);
 			break;
+		case DO_REL_STATS:
+			dumpRelationStats(fout, (const RelStatsInfo *) dobj);
+			break;
 		case DO_PRE_DATA_BOUNDARY:
 		case DO_POST_DATA_BOUNDARY:
 			/* never dumped, nothing to do */
@@ -17173,6 +17526,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 		free(indstatvalsarray);
 	}
 
+	/* Comments and stats share same .dep */
+
 	/* Dump Index Comments */
 	if (indxinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
 		dumpComment(fout, "INDEX", qindxname,
@@ -18960,6 +19315,16 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 				/* must come after the pre-data boundary */
 				addObjectDependency(dobj, preDataBound->dumpId);
 				break;
+			case DO_REL_STATS:
+				/* stats section varies by parent object type, DATA or POST */
+				if (statisticsDumpSection((RelStatsInfo *) dobj) == SECTION_DATA)
+				{
+					addObjectDependency(dobj, preDataBound->dumpId);
+					addObjectDependency(postDataBound, dobj->dumpId);
+				}
+				else
+					addObjectDependency(dobj, postDataBound->dumpId);
+				break;
 		}
 	}
 }
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index d65f558565..ef555e9178 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -84,6 +84,7 @@ typedef enum
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION,
 	DO_SUBSCRIPTION_REL,		/* see note for SubRelInfo */
+	DO_REL_STATS,
 } DumpableObjectType;
 
 /*
@@ -101,6 +102,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
@@ -421,6 +423,12 @@ typedef struct _indexAttachInfo
 	IndxInfo   *partitionIdx;	/* link to index on partition */
 } IndexAttachInfo;
 
+typedef struct _relStatsInfo
+{
+	DumpableObject	dobj;
+	char			relkind;		/* 'r', 'v', 'c', etc */
+} RelStatsInfo;
+
 typedef struct _statsExtInfo
 {
 	DumpableObject dobj;
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 3c8f2eb808..989d20aa27 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -1500,6 +1500,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "POST-DATA BOUNDARY  (ID %d)",
 					 obj->dumpId);
 			return;
+		case DO_REL_STATS:
+			snprintf(buf, bufsize,
+					 "RELATION STATISTICS FOR %s  (ID %d OID %u)",
+					 obj->name, obj->dumpId, obj->catId.oid);
+			return;
 	}
 	/* shouldn't get here */
 	snprintf(buf, bufsize,
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 9a04e51c81..62e2766c09 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -103,6 +103,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -172,6 +173,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -451,6 +453,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -666,6 +670,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 88ae39d938..355f0439da 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -63,6 +63,7 @@ main(int argc, char **argv)
 	int			numWorkers = 1;
 	Archive    *AH;
 	char	   *inputFileSpec;
+	bool		statistics_only = false;
 	static int	disable_triggers = 0;
 	static int	enable_row_security = 0;
 	static int	if_exists = 0;
@@ -74,6 +75,7 @@ main(int argc, char **argv)
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
 	static int	no_subscriptions = 0;
+	static int  no_statistics = 0;
 	static int	strict_names = 0;
 	bool		data_only = false;
 	bool		schema_only = false;
@@ -108,6 +110,7 @@ main(int argc, char **argv)
 		{"username", 1, NULL, 'U'},
 		{"verbose", 0, NULL, 'v'},
 		{"single-transaction", 0, NULL, '1'},
+		{"statistics-only", no_argument, NULL, 'P'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -128,6 +131,7 @@ main(int argc, char **argv)
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
+		{"no-statistics", no_argument, &no_statistics, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -271,6 +275,10 @@ main(int argc, char **argv)
 				opts->aclsSkip = 1;
 				break;
 
+			case 'X':			/* Restore statistics only */
+				statistics_only = true;
+				break;
+
 			case '1':			/* Restore data in a single transaction */
 				opts->single_txn = true;
 				opts->exit_on_error = true;
@@ -343,6 +351,10 @@ main(int argc, char **argv)
 
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
+	if (schema_only && statistics_only)
+		pg_fatal("options -s/--schema-only and -X/--statistics-only cannot be used together");
 
 	if (data_only && opts->dropSchema)
 		pg_fatal("options -c/--clean and -a/--data-only cannot be used together");
@@ -362,8 +374,9 @@ main(int argc, char **argv)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
 	/* set derivative flags */
-	opts->dumpSchema = (!data_only);
-	opts->dumpData = (!schema_only);
+	opts->dumpSchema = (!data_only && !statistics_only);
+	opts->dumpData = (!schema_only && !statistics_only);
+	opts->dumpStatistics = (!no_statistics && !data_only && !schema_only);
 
 	opts->disable_triggers = disable_triggers;
 	opts->enable_row_security = enable_row_security;
@@ -375,6 +388,7 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
diff --git a/src/bin/pg_dump/t/001_basic.pl b/src/bin/pg_dump/t/001_basic.pl
index b9d13a0e1d..76ebf15f55 100644
--- a/src/bin/pg_dump/t/001_basic.pl
+++ b/src/bin/pg_dump/t/001_basic.pl
@@ -50,12 +50,30 @@ command_fails_like(
 	'pg_dump: options -s/--schema-only and -a/--data-only cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '-s', '-X' ],
+	qr/\Qpg_dump: error: options -s\/--schema-only and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: error: options -s/--schema-only and -X/--statistics-only cannot be used together'
+);
+
+command_fails_like(
+	[ 'pg_dump', '-a', '-X' ],
+	qr/\Qpg_dump: error: options -a\/--data-only and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: error: options -a/--data-only and -X/--statistics-only cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-s', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: options -s\/--schema-only and --include-foreign-data cannot be used together\E/,
 	'pg_dump: options -s/--schema-only and --include-foreign-data cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '--statistics-only', '--no-statistics' ],
+	qr/\Qpg_dump: error: options -X\/--statistics-only and --no-statistics cannot be used together\E/,
+	'pg_dump: options -X\/--statistics-only and --no-statistics cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-j2', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: option --include-foreign-data is not supported with parallel backup\E/,
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index d66e901f51..aa4785c612 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -123,7 +123,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Dump only the data, not the schema (data definitions).
+        Dump only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are dumped.
        </para>
 
@@ -141,8 +141,9 @@ PostgreSQL documentation
       <listitem>
        <para>
         Include large objects in the dump.  This is the default behavior
-        except when <option>--schema</option>, <option>--table</option>, or
-        <option>--schema-only</option> is specified.  The <option>-b</option>
+        except when <option>--schema</option>, <option>--table</option>,
+        <option>--schema-only</option>, or
+        <option>--statistics-only</option> is specified.  The <option>-b</option>
         switch is therefore only useful to add large objects to dumps
         where a specific schema or table has been requested.  Note that
         large objects are considered data and therefore will be included when
@@ -516,10 +517,11 @@ PostgreSQL documentation
       <term><option>--schema-only</option></term>
       <listitem>
        <para>
-        Dump only the object definitions (schema), not data.
+        Dump only the object definitions (schema), not data or statistics.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive to <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
         <option>--section=pre-data --section=post-data</option>.
@@ -652,6 +654,18 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Dump only the statistics, not the schema (data definitions) or data.
+        Statistics for tables, materialized views, and indexes are dumped.
+       </para>
+
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-Z <replaceable class="parameter">level</replaceable></option></term>
       <term><option>-Z <replaceable class="parameter">method</replaceable></option>[:<replaceable>detail</replaceable>]</term>
@@ -833,7 +847,8 @@ PostgreSQL documentation
         though you do not need the data in it.
        </para>
        <para>
-        To exclude data for all tables in the database, see <option>--schema-only</option>.
+        To exclude data for all tables in the database, see <option>--schema-only</option>
+        or <option>--statistics-only</option>.
        </para>
       </listitem>
      </varlistentry>
@@ -1098,6 +1113,15 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index b8b27e1719..ff1441a243 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -94,7 +94,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Restore only the data, not the schema (data definitions).
+        Restore only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are restored,
         if present in the archive.
        </para>
@@ -483,10 +483,11 @@ PostgreSQL documentation
         to the extent that schema entries are present in the archive.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive of <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
-        <option>--section=pre-data --section=post-data</option>.
+        <option>--section=pre-data --section=post-data --no-statistics</option>.
        </para>
        <para>
         (Do not confuse this with the <option>--schema</option> option, which
@@ -599,6 +600,20 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Restore only the statistics, not schema or data.
+       </para>
+       <para>
+        (Do not confuse this with the <option>--schema</option> option, which
+        uses the word <quote>schema</quote> in a different meaning.)
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-1</option></term>
       <term><option>--single-transaction</option></term>
@@ -723,6 +738,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore statistics, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
-- 
2.47.0

v33-0004-Consolidate-attribute-syscache-lookups-into-one-.patchtext/x-patch; charset=US-ASCII; name=v33-0004-Consolidate-attribute-syscache-lookups-into-one-.patchDownload
From 0750f8c4fa8d5144857d7dfe2117d1414cc10b56 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 14 Nov 2024 03:53:33 -0500
Subject: [PATCH v33 04/11] Consolidate attribute syscache lookups into one
 call by name.

Previously we were doing one lookup by attname and one lookup by attnum,
which seems wasteful.
---
 src/backend/statistics/attribute_stats.c | 55 +++++++++++-------------
 1 file changed, 24 insertions(+), 31 deletions(-)

diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index b97ba7b0c0..6393783f8e 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -78,8 +78,8 @@ static struct StatsArgInfo attarginfo[] =
 
 static bool attribute_statistics_update(FunctionCallInfo fcinfo, int elevel);
 static Node *get_attr_expr(Relation rel, int attnum);
-static void get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
-							   Oid *atttypid, int32 *atttypmod,
+static void get_attr_stat_type(Oid reloid, Name attname, int elevel,
+							   AttrNumber *attnum, Oid *atttypid, int32 *atttypmod,
 							   char *atttyptype, Oid *atttypcoll,
 							   Oid *eq_opr, Oid *lt_opr);
 static bool get_elem_stat_type(Oid atttypid, char atttyptype, int elevel,
@@ -166,23 +166,16 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 
 	stats_check_required_arg(fcinfo, attarginfo, ATTNAME_ARG);
 	attname = PG_GETARG_NAME(ATTNAME_ARG);
-	attnum = get_attnum(reloid, NameStr(*attname));
-
-	if (attnum < 0)
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("cannot modify statistics on system column \"%s\"",
-						NameStr(*attname))));
-
-	if (attnum == InvalidAttrNumber)
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_COLUMN),
-				 errmsg("column \"%s\" of relation \"%s\" does not exist",
-						NameStr(*attname), get_rel_name(reloid))));
 
 	stats_check_required_arg(fcinfo, attarginfo, INHERITED_ARG);
 	inherited = PG_GETARG_BOOL(INHERITED_ARG);
 
+	/* derive type information from attribute */
+	get_attr_stat_type(reloid, attname, elevel,
+					   &attnum, &atttypid, &atttypmod,
+					   &atttyptype, &atttypcoll,
+					   &eq_opr, &lt_opr);
+
 	/*
 	 * Check argument sanity. If some arguments are unusable, emit at elevel
 	 * and set the corresponding argument to NULL in fcinfo.
@@ -232,12 +225,6 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 		result = false;
 	}
 
-	/* derive information from attribute */
-	get_attr_stat_type(reloid, attnum, elevel,
-					   &atttypid, &atttypmod,
-					   &atttyptype, &atttypcoll,
-					   &eq_opr, &lt_opr);
-
 	/* if needed, derive element type */
 	if (do_mcelem || do_dechist)
 	{
@@ -503,8 +490,8 @@ get_attr_expr(Relation rel, int attnum)
  * Derive type information from the attribute.
  */
 static void
-get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
-				   Oid *atttypid, int32 *atttypmod,
+get_attr_stat_type(Oid reloid, Name attname, int elevel,
+				   AttrNumber *attnum, Oid *atttypid, int32 *atttypmod,
 				   char *atttyptype, Oid *atttypcoll,
 				   Oid *eq_opr, Oid *lt_opr)
 {
@@ -514,24 +501,30 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
 	Node	   *expr;
 	TypeCacheEntry *typcache;
 
-	atup = SearchSysCache2(ATTNUM, ObjectIdGetDatum(reloid),
-						   Int16GetDatum(attnum));
+	atup = SearchSysCacheAttName(reloid, NameStr(*attname));
 
-	/* Attribute not found */
+	/* Attribute not found or is dropped */
 	if (!HeapTupleIsValid(atup))
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
-				 errmsg("attribute %d of relation \"%s\" does not exist",
-						attnum, RelationGetRelationName(rel))));
+				 errmsg("column \"%s\" of relation \"%s\" does not exist",
+						NameStr(*attname), get_rel_name(reloid))));
 
 	attr = (Form_pg_attribute) GETSTRUCT(atup);
 
-	if (attr->attisdropped)
+	if (attr->attnum < 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("cannot modify statistics on system column \"%s\"",
+						NameStr(*attname))));
+
+	if (attr->attnum == InvalidAttrNumber)
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
-				 errmsg("attribute %d of relation \"%s\" does not exist",
-						attnum, RelationGetRelationName(rel))));
+				 errmsg("column \"%s\" of relation \"%s\" does not exist",
+						NameStr(*attname), get_rel_name(reloid))));
 
+	*attnum = attr->attnum;
 	expr = get_attr_expr(rel, attr->attnum);
 
 	/*
-- 
2.47.0

v33-0006-split-out-print_processing_notice.patchtext/x-patch; charset=US-ASCII; name=v33-0006-split-out-print_processing_notice.patchDownload
From 041dd15f564088ed6b2df7885af7ffdcd71cb554 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 6 Nov 2024 15:46:51 -0500
Subject: [PATCH v33 06/11] split out print_processing_notice

---
 src/bin/scripts/vacuumdb.c | 41 +++++++++++++++++++++++---------------
 1 file changed, 25 insertions(+), 16 deletions(-)

diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index 7b97a9428a..e9946f79b2 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -535,6 +535,30 @@ check_conn_options(PGconn *conn, vacuumingOptions *vacopts)
 }
 
 
+/*
+ * print the processing notice for a database.
+ */
+static void
+print_processing_notice(PGconn *conn, int stage, const char *progname, bool quiet)
+{
+	const char *stage_messages[] = {
+		gettext_noop("Generating minimal optimizer statistics (1 target)"),
+		gettext_noop("Generating medium optimizer statistics (10 targets)"),
+		gettext_noop("Generating default (full) optimizer statistics")
+	};
+
+	if (!quiet)
+	{
+		if (stage != ANALYZE_NO_STAGE)
+			printf(_("%s: processing database \"%s\": %s\n"),
+				   progname, PQdb(conn), _(stage_messages[stage]));
+		else
+			printf(_("%s: vacuuming database \"%s\"\n"),
+				   progname, PQdb(conn));
+		fflush(stdout);
+	}
+}
+
 /*
  * vacuum_one_database
  *
@@ -574,11 +598,6 @@ vacuum_one_database(ConnParams *cparams,
 		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
 		"RESET default_statistics_target;"
 	};
-	const char *stage_messages[] = {
-		gettext_noop("Generating minimal optimizer statistics (1 target)"),
-		gettext_noop("Generating medium optimizer statistics (10 targets)"),
-		gettext_noop("Generating default (full) optimizer statistics")
-	};
 
 	Assert(stage == ANALYZE_NO_STAGE ||
 		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
@@ -586,17 +605,7 @@ vacuum_one_database(ConnParams *cparams,
 	conn = connectDatabase(cparams, progname, echo, false, true);
 
 	check_conn_options(conn, vacopts);
-
-	if (!quiet)
-	{
-		if (stage != ANALYZE_NO_STAGE)
-			printf(_("%s: processing database \"%s\": %s\n"),
-				   progname, PQdb(conn), _(stage_messages[stage]));
-		else
-			printf(_("%s: vacuuming database \"%s\"\n"),
-				   progname, PQdb(conn));
-		fflush(stdout);
-	}
+	print_processing_notice(conn, stage, progname, quiet);
 
 	/*
 	 * Prepare the list of tables to process by querying the catalogs.
-- 
2.47.0

v33-0007-split-out-generate_catalog_list.patchtext/x-patch; charset=US-ASCII; name=v33-0007-split-out-generate_catalog_list.patchDownload
From ccb3412a776e5df2dae1f3f1d2b7f708b310f2af Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 6 Nov 2024 16:15:16 -0500
Subject: [PATCH v33 07/11] split out generate_catalog_list

---
 src/bin/scripts/vacuumdb.c | 176 +++++++++++++++++++++----------------
 1 file changed, 99 insertions(+), 77 deletions(-)

diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index e9946f79b2..36f4796db0 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -560,67 +560,38 @@ print_processing_notice(PGconn *conn, int stage, const char *progname, bool quie
 }
 
 /*
- * vacuum_one_database
- *
- * Process tables in the given database.  If the 'tables' list is empty,
- * process all tables in the database.
- *
- * Note that this function is only concerned with running exactly one stage
- * when in analyze-in-stages mode; caller must iterate on us if necessary.
- *
- * If concurrentCons is > 1, multiple connections are used to vacuum tables
- * in parallel.  In this case and if the table list is empty, we first obtain
- * a list of tables from the database.
- */
-static void
-vacuum_one_database(ConnParams *cparams,
-					vacuumingOptions *vacopts,
-					int stage,
-					SimpleStringList *objects,
-					int concurrentCons,
-					const char *progname, bool echo, bool quiet)
+	* Prepare the list of tables to process by querying the catalogs.
+	*
+	* Since we execute the constructed query with the default search_path
+	* (which could be unsafe), everything in this query MUST be fully
+	* qualified.
+	*
+	* First, build a WITH clause for the catalog query if any tables were
+	* specified, with a set of values made of relation names and their
+	* optional set of columns.  This is used to match any provided column
+	* lists with the generated qualified identifiers and to filter for the
+	* tables provided via --table.  If a listed table does not exist, the
+	* catalog query will fail.
+	*/
+static SimpleStringList *
+generate_catalog_list(PGconn *conn,
+					  vacuumingOptions *vacopts,
+					  SimpleStringList *objects,
+					  bool echo,
+					  int *ntups)
 {
-	PQExpBufferData sql;
-	PQExpBufferData buf;
 	PQExpBufferData catalog_query;
-	PGresult   *res;
-	PGconn	   *conn;
+	PQExpBufferData buf;
+	SimpleStringList *dbtables;
 	SimpleStringListCell *cell;
-	ParallelSlotArray *sa;
-	SimpleStringList dbtables = {NULL, NULL};
-	int			i;
-	int			ntups;
-	bool		failed = false;
 	bool		objects_listed = false;
-	const char *initcmd;
-	const char *stage_commands[] = {
-		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
-		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
-		"RESET default_statistics_target;"
-	};
+	PGresult   *res;
+	int			i;
 
-	Assert(stage == ANALYZE_NO_STAGE ||
-		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
+	dbtables = palloc(sizeof(SimpleStringList));
+	dbtables->head = NULL;
+	dbtables->tail = NULL;
 
-	conn = connectDatabase(cparams, progname, echo, false, true);
-
-	check_conn_options(conn, vacopts);
-	print_processing_notice(conn, stage, progname, quiet);
-
-	/*
-	 * Prepare the list of tables to process by querying the catalogs.
-	 *
-	 * Since we execute the constructed query with the default search_path
-	 * (which could be unsafe), everything in this query MUST be fully
-	 * qualified.
-	 *
-	 * First, build a WITH clause for the catalog query if any tables were
-	 * specified, with a set of values made of relation names and their
-	 * optional set of columns.  This is used to match any provided column
-	 * lists with the generated qualified identifiers and to filter for the
-	 * tables provided via --table.  If a listed table does not exist, the
-	 * catalog query will fail.
-	 */
 	initPQExpBuffer(&catalog_query);
 	for (cell = objects ? objects->head : NULL; cell; cell = cell->next)
 	{
@@ -771,40 +742,91 @@ vacuum_one_database(ConnParams *cparams,
 	appendPQExpBufferStr(&catalog_query, " ORDER BY c.relpages DESC;");
 	executeCommand(conn, "RESET search_path;", echo);
 	res = executeQuery(conn, catalog_query.data, echo);
+	*ntups = PQntuples(res);
 	termPQExpBuffer(&catalog_query);
 	PQclear(executeQuery(conn, ALWAYS_SECURE_SEARCH_PATH_SQL, echo));
 
-	/*
-	 * If no rows are returned, there are no matching tables, so we are done.
-	 */
-	ntups = PQntuples(res);
-	if (ntups == 0)
-	{
-		PQclear(res);
-		PQfinish(conn);
-		return;
-	}
-
 	/*
 	 * Build qualified identifiers for each table, including the column list
 	 * if given.
 	 */
-	initPQExpBuffer(&buf);
-	for (i = 0; i < ntups; i++)
+	if (*ntups > 0)
 	{
-		appendPQExpBufferStr(&buf,
-							 fmtQualifiedId(PQgetvalue(res, i, 1),
-											PQgetvalue(res, i, 0)));
+		initPQExpBuffer(&buf);
+		for (i = 0; i < *ntups; i++)
+		{
+			appendPQExpBufferStr(&buf,
+								fmtQualifiedId(PQgetvalue(res, i, 1),
+												PQgetvalue(res, i, 0)));
 
-		if (objects_listed && !PQgetisnull(res, i, 2))
-			appendPQExpBufferStr(&buf, PQgetvalue(res, i, 2));
+			if (objects_listed && !PQgetisnull(res, i, 2))
+				appendPQExpBufferStr(&buf, PQgetvalue(res, i, 2));
 
-		simple_string_list_append(&dbtables, buf.data);
-		resetPQExpBuffer(&buf);
+			simple_string_list_append(dbtables, buf.data);
+			resetPQExpBuffer(&buf);
+		}
+		termPQExpBuffer(&buf);
 	}
-	termPQExpBuffer(&buf);
 	PQclear(res);
 
+	return dbtables;
+}
+
+/*
+ * vacuum_one_database
+ *
+ * Process tables in the given database.  If the 'tables' list is empty,
+ * process all tables in the database.
+ *
+ * Note that this function is only concerned with running exactly one stage
+ * when in analyze-in-stages mode; caller must iterate on us if necessary.
+ *
+ * If concurrentCons is > 1, multiple connections are used to vacuum tables
+ * in parallel.  In this case and if the table list is empty, we first obtain
+ * a list of tables from the database.
+ */
+static void
+vacuum_one_database(ConnParams *cparams,
+					vacuumingOptions *vacopts,
+					int stage,
+					SimpleStringList *objects,
+					int concurrentCons,
+					const char *progname, bool echo, bool quiet)
+{
+	PQExpBufferData sql;
+	PGconn	   *conn;
+	SimpleStringListCell *cell;
+	ParallelSlotArray *sa;
+	int			ntups;
+	bool		failed = false;
+	const char *initcmd;
+	SimpleStringList *dbtables;
+	const char *stage_commands[] = {
+		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
+		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
+		"RESET default_statistics_target;"
+	};
+
+	Assert(stage == ANALYZE_NO_STAGE ||
+		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
+
+	conn = connectDatabase(cparams, progname, echo, false, true);
+
+	check_conn_options(conn, vacopts);
+	print_processing_notice(conn, stage, progname, quiet);
+
+	dbtables = generate_catalog_list(conn, vacopts, objects, echo, &ntups);
+
+	/*
+	 * If no rows are returned, there are no matching tables, so we are done.
+	 */
+	if (ntups == 0)
+	{
+		PQfinish(conn);
+		return;
+	}
+
+
 	/*
 	 * Ensure concurrentCons is sane.  If there are more connections than
 	 * vacuumable relations, we don't need to use them all.
@@ -837,7 +859,7 @@ vacuum_one_database(ConnParams *cparams,
 
 	initPQExpBuffer(&sql);
 
-	cell = dbtables.head;
+	cell = dbtables->head;
 	do
 	{
 		const char *tabname = cell->val;
-- 
2.47.0

v33-0009-Add-issues_sql_unlike-opposite-of-issues_sql_lik.patchtext/x-patch; charset=US-ASCII; name=v33-0009-Add-issues_sql_unlike-opposite-of-issues_sql_lik.patchDownload
From e924cc42fa8e2ae3e479f125cfaae2d82c193dbf Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 7 Nov 2024 18:06:52 -0500
Subject: [PATCH v33 09/11] Add issues_sql_unlike, opposite of issues_sql_like

This is the same as issues_sql_like(), but the specified text is
prohibited from being in the output rather than required.

This became necessary to test that a command-line filter was in fact
filtering out certain output that a prior test required.
---
 src/test/perl/PostgreSQL/Test/Cluster.pm | 25 ++++++++++++++++++++++++
 1 file changed, 25 insertions(+)

diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index 508e5e3917..1281071479 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -2799,6 +2799,31 @@ sub issues_sql_like
 }
 
 =pod
+=item $node->issues_sql_unlike(cmd, prohibited_sql, test_name)
+
+Run a command on the node, then verify that $prohibited_sql does not appear
+in the server log file.
+
+=cut
+
+sub issues_sql_unlike
+{
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+	my ($self, $cmd, $prohibited_sql, $test_name) = @_;
+
+	local %ENV = $self->_get_env();
+
+	my $log_location = -s $self->logfile;
+
+	my $result = PostgreSQL::Test::Utils::run_log($cmd);
+	ok($result, "@$cmd exit code 0");
+	my $log =
+	  PostgreSQL::Test::Utils::slurp_file($self->logfile, $log_location);
+	unlike($log, $prohibited_sql, "$test_name: SQL found in server log");
+	return;
+}
+
 
 =item $node->log_content()
 
-- 
2.47.0

v33-0008-preserve-catalog-lists-across-staged-runs.patchtext/x-patch; charset=US-ASCII; name=v33-0008-preserve-catalog-lists-across-staged-runs.patchDownload
From 975dd88313e88ae8d579b1e0cd68d7944cb6a391 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 6 Nov 2024 21:30:49 -0500
Subject: [PATCH v33 08/11] preserve catalog lists across staged runs

---
 src/bin/scripts/vacuumdb.c | 113 ++++++++++++++++++++++++++-----------
 1 file changed, 80 insertions(+), 33 deletions(-)

diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index 36f4796db0..b13f3c4224 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -68,6 +68,9 @@ static void vacuum_one_database(ConnParams *cparams,
 								int stage,
 								SimpleStringList *objects,
 								int concurrentCons,
+								PGconn *conn,
+								SimpleStringList *dbtables,
+								int ntups,
 								const char *progname, bool echo, bool quiet);
 
 static void vacuum_all_databases(ConnParams *cparams,
@@ -83,6 +86,14 @@ static void prepare_vacuum_command(PQExpBuffer sql, int serverVersion,
 static void run_vacuum_command(PGconn *conn, const char *sql, bool echo,
 							   const char *table);
 
+static void check_conn_options(PGconn *conn, vacuumingOptions *vacopts);
+
+static void
+print_processing_notice(PGconn *conn, int stage, const char *progname, bool quiet);
+
+static SimpleStringList * generate_catalog_list(PGconn *conn, vacuumingOptions *vacopts,
+												SimpleStringList *objects, bool echo, int *ntups);
+
 static void help(const char *progname);
 
 void		check_objfilter(void);
@@ -386,6 +397,11 @@ main(int argc, char *argv[])
 	}
 	else
 	{
+		PGconn	   *conn;
+		int			ntup;
+		SimpleStringList   *found_objects;
+		int			stage;
+
 		if (dbname == NULL)
 		{
 			if (getenv("PGDATABASE"))
@@ -397,25 +413,37 @@ main(int argc, char *argv[])
 		}
 
 		cparams.dbname = dbname;
+		stage = (analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
+
+		conn = connectDatabase(&cparams, progname, echo, false, true);
+		check_conn_options(conn, &vacopts);
+		print_processing_notice(conn, stage, progname, quiet);
+		found_objects = generate_catalog_list(conn, &vacopts, &objects, echo, &ntup);
 
 		if (analyze_in_stages)
 		{
-			int			stage;
-
 			for (stage = 0; stage < ANALYZE_NUM_STAGES; stage++)
 			{
-				vacuum_one_database(&cparams, &vacopts,
-									stage,
+				/* the last pass disconnected the conn */
+				if (stage > 0)
+					conn = connectDatabase(&cparams, progname, echo, false, true);
+
+				vacuum_one_database(&cparams, &vacopts, stage,
 									&objects,
 									concurrentCons,
+									conn,
+									found_objects,
+									ntup,
 									progname, echo, quiet);
 			}
 		}
 		else
-			vacuum_one_database(&cparams, &vacopts,
-								ANALYZE_NO_STAGE,
+			vacuum_one_database(&cparams, &vacopts, stage,
 								&objects,
 								concurrentCons,
+								conn,
+								found_objects,
+								ntup,
 								progname, echo, quiet);
 	}
 
@@ -791,16 +819,17 @@ vacuum_one_database(ConnParams *cparams,
 					int stage,
 					SimpleStringList *objects,
 					int concurrentCons,
+					PGconn *conn,
+					SimpleStringList *dbtables,
+					int ntups,
 					const char *progname, bool echo, bool quiet)
 {
 	PQExpBufferData sql;
-	PGconn	   *conn;
 	SimpleStringListCell *cell;
 	ParallelSlotArray *sa;
-	int			ntups;
 	bool		failed = false;
 	const char *initcmd;
-	SimpleStringList *dbtables;
+
 	const char *stage_commands[] = {
 		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
 		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
@@ -810,13 +839,6 @@ vacuum_one_database(ConnParams *cparams,
 	Assert(stage == ANALYZE_NO_STAGE ||
 		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
 
-	conn = connectDatabase(cparams, progname, echo, false, true);
-
-	check_conn_options(conn, vacopts);
-	print_processing_notice(conn, stage, progname, quiet);
-
-	dbtables = generate_catalog_list(conn, vacopts, objects, echo, &ntups);
-
 	/*
 	 * If no rows are returned, there are no matching tables, so we are done.
 	 */
@@ -928,7 +950,7 @@ finish:
 }
 
 /*
- * Vacuum/analyze all connectable databases.
+ * Vacuum/analyze all ccparams->override_dbname = PQgetvalue(result, i, 0);onnectable databases.
  *
  * In analyze-in-stages mode, we process all databases in one stage before
  * moving on to the next stage.  That ensure minimal stats are available
@@ -944,8 +966,13 @@ vacuum_all_databases(ConnParams *cparams,
 {
 	PGconn	   *conn;
 	PGresult   *result;
-	int			stage;
 	int			i;
+	int			stage;
+
+	SimpleStringList  **found_objects;
+	int				   *num_tuples;
+
+	stage = (analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
 
 	conn = connectMaintenanceDatabase(cparams, progname, echo);
 	result = executeQuery(conn,
@@ -953,7 +980,33 @@ vacuum_all_databases(ConnParams *cparams,
 						  echo);
 	PQfinish(conn);
 
-	if (analyze_in_stages)
+	/*
+	 * connect to each database, check validity of options,
+	 * build the list of found objects per database,
+	 * and run the first/only vacuum stage
+	 */
+	found_objects = palloc(PQntuples(result) * sizeof(SimpleStringList *));
+	num_tuples = palloc(PQntuples(result) * sizeof (int));
+
+	for (i = 0; i < PQntuples(result); i++)
+	{
+		cparams->override_dbname = PQgetvalue(result, i, 0);
+		conn = connectDatabase(cparams, progname, echo, false, true);
+		check_conn_options(conn, vacopts);
+		print_processing_notice(conn, stage, progname, quiet);
+		found_objects[i] = generate_catalog_list(conn, vacopts, objects, echo, &num_tuples[i]);
+
+		vacuum_one_database(cparams, vacopts,
+							stage,
+							objects,
+							concurrentCons,
+							conn,
+							found_objects[i],
+							num_tuples[i],
+							progname, echo, quiet);
+	}
+
+	if (stage != ANALYZE_NO_STAGE)
 	{
 		/*
 		 * When analyzing all databases in stages, we analyze them all in the
@@ -963,35 +1016,29 @@ vacuum_all_databases(ConnParams *cparams,
 		 * This means we establish several times as many connections, but
 		 * that's a secondary consideration.
 		 */
-		for (stage = 0; stage < ANALYZE_NUM_STAGES; stage++)
+		for (stage = 1; stage < ANALYZE_NUM_STAGES; stage++)
 		{
 			for (i = 0; i < PQntuples(result); i++)
 			{
 				cparams->override_dbname = PQgetvalue(result, i, 0);
+				conn = connectDatabase(cparams, progname, echo, false, true);
+				print_processing_notice(conn, stage, progname, quiet);
 
 				vacuum_one_database(cparams, vacopts,
 									stage,
 									objects,
 									concurrentCons,
+									conn,
+									found_objects[i],
+									num_tuples[i],
 									progname, echo, quiet);
 			}
 		}
 	}
-	else
-	{
-		for (i = 0; i < PQntuples(result); i++)
-		{
-			cparams->override_dbname = PQgetvalue(result, i, 0);
-
-			vacuum_one_database(cparams, vacopts,
-								ANALYZE_NO_STAGE,
-								objects,
-								concurrentCons,
-								progname, echo, quiet);
-		}
-	}
 
 	PQclear(result);
+	pfree(found_objects);
+	pfree(num_tuples);
 }
 
 /*
-- 
2.47.0

v33-0010-Add-force-analyze-to-vacuumdb.patchtext/x-patch; charset=US-ASCII; name=v33-0010-Add-force-analyze-to-vacuumdb.patchDownload
From ae94d0a1f6af01a0816d9515d6b03d1de9e0596e Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Fri, 8 Nov 2024 12:27:50 -0500
Subject: [PATCH v33 10/11] Add --force-analyze to vacuumdb.

The vacuumdb options of --analyze-in-stages and --analyze-only are often
used after a restore from a dump or a pg_upgrade to quickly rebuild
stats on a databse.

However, now that stats are imported in most (but not all) cases,
running either of these commands will be at least partially redundant,
and will overwrite the stats that were just imported, which is a big
POLA violation.

We could add a new option such as --analyze-missing-in-stages, but that
wouldn't help the userbase that grown accustomed to running
--analyze-in-stages after an upgrade.

The least-bad option to handle the situation is to change the behavior
of --analyze-only and --analyze-in-stages to only analyze tables which
were missing stats before the vacuumdb started, but offer the
--force-analyze flag to restore the old behavior for those who truly
wanted it.
---
 src/bin/scripts/t/100_vacuumdb.pl |  6 +-
 src/bin/scripts/vacuumdb.c        | 91 ++++++++++++++++++++++++-------
 doc/src/sgml/ref/vacuumdb.sgml    | 48 ++++++++++++++++
 3 files changed, 125 insertions(+), 20 deletions(-)

diff --git a/src/bin/scripts/t/100_vacuumdb.pl b/src/bin/scripts/t/100_vacuumdb.pl
index 1a2bcb4959..2d669391fe 100644
--- a/src/bin/scripts/t/100_vacuumdb.pl
+++ b/src/bin/scripts/t/100_vacuumdb.pl
@@ -127,9 +127,13 @@ $node->issues_sql_like(
 	qr/statement: VACUUM \(SKIP_DATABASE_STATS, ANALYZE\) public.vactable\(a, b\);/,
 	'vacuumdb --analyze with complete column list');
 $node->issues_sql_like(
+	[ 'vacuumdb', '--analyze-only', '--force-analyze', '--table', 'vactable(b)', 'postgres' ],
+	qr/statement: ANALYZE public.vactable\(b\);/,
+	'vacuumdb --analyze-only --force-analyze with partial column list');
+$node->issues_sql_unlike(
 	[ 'vacuumdb', '--analyze-only', '--table', 'vactable(b)', 'postgres' ],
 	qr/statement: ANALYZE public.vactable\(b\);/,
-	'vacuumdb --analyze-only with partial column list');
+	'vacuumdb --analyze-only --force-analyze with partial column list skipping vacuumed tables');
 $node->command_checks_all(
 	[ 'vacuumdb', '--analyze', '--table', 'vacview', 'postgres' ],
 	0,
diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index b13f3c4224..1aa5c46af5 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -25,6 +25,7 @@
 #include "fe_utils/query_utils.h"
 #include "fe_utils/simple_list.h"
 #include "fe_utils/string_utils.h"
+#include "pqexpbuffer.h"
 
 
 /* vacuum options controlled by user flags */
@@ -47,6 +48,8 @@ typedef struct vacuumingOptions
 	bool		process_main;
 	bool		process_toast;
 	bool		skip_database_stats;
+	bool		analyze_in_stages;
+	bool		force_analyze;
 	char	   *buffer_usage_limit;
 } vacuumingOptions;
 
@@ -75,7 +78,6 @@ static void vacuum_one_database(ConnParams *cparams,
 
 static void vacuum_all_databases(ConnParams *cparams,
 								 vacuumingOptions *vacopts,
-								 bool analyze_in_stages,
 								 SimpleStringList *objects,
 								 int concurrentCons,
 								 const char *progname, bool echo, bool quiet);
@@ -140,6 +142,7 @@ main(int argc, char *argv[])
 		{"no-process-toast", no_argument, NULL, 11},
 		{"no-process-main", no_argument, NULL, 12},
 		{"buffer-usage-limit", required_argument, NULL, 13},
+		{"force-analyze", no_argument, NULL, 14},
 		{NULL, 0, NULL, 0}
 	};
 
@@ -156,7 +159,6 @@ main(int argc, char *argv[])
 	bool		echo = false;
 	bool		quiet = false;
 	vacuumingOptions vacopts;
-	bool		analyze_in_stages = false;
 	SimpleStringList objects = {NULL, NULL};
 	int			concurrentCons = 1;
 	int			tbl_count = 0;
@@ -170,6 +172,8 @@ main(int argc, char *argv[])
 	vacopts.do_truncate = true;
 	vacopts.process_main = true;
 	vacopts.process_toast = true;
+	vacopts.force_analyze = false;
+	vacopts.analyze_in_stages = false;
 
 	pg_logging_init(argv[0]);
 	progname = get_progname(argv[0]);
@@ -251,7 +255,7 @@ main(int argc, char *argv[])
 				maintenance_db = pg_strdup(optarg);
 				break;
 			case 3:
-				analyze_in_stages = vacopts.analyze_only = true;
+				vacopts.analyze_in_stages = vacopts.analyze_only = true;
 				break;
 			case 4:
 				vacopts.disable_page_skipping = true;
@@ -287,6 +291,9 @@ main(int argc, char *argv[])
 			case 13:
 				vacopts.buffer_usage_limit = escape_quotes(optarg);
 				break;
+			case 14:
+				vacopts.force_analyze = true;
+				break;
 			default:
 				/* getopt_long already emitted a complaint */
 				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -372,6 +379,14 @@ main(int argc, char *argv[])
 		pg_fatal("cannot use the \"%s\" option with the \"%s\" option",
 				 "buffer-usage-limit", "full");
 
+	/*
+	 * --force-analyze is only valid when used with --analyze-only, -analyze,
+	 * or --analyze-in-stages
+	 */
+	if (vacopts.force_analyze && !vacopts.analyze_only && !vacopts.analyze_in_stages)
+		pg_fatal("can only use the \"%s\" option with \"%s\" or \"%s\"",
+				 "--force-analyze", "-Z/--analyze-only", "--analyze-in-stages");
+
 	/* fill cparams except for dbname, which is set below */
 	cparams.pghost = host;
 	cparams.pgport = port;
@@ -390,7 +405,6 @@ main(int argc, char *argv[])
 		cparams.dbname = maintenance_db;
 
 		vacuum_all_databases(&cparams, &vacopts,
-							 analyze_in_stages,
 							 &objects,
 							 concurrentCons,
 							 progname, echo, quiet);
@@ -413,20 +427,27 @@ main(int argc, char *argv[])
 		}
 
 		cparams.dbname = dbname;
-		stage = (analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
+		stage = (vacopts.analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
 
 		conn = connectDatabase(&cparams, progname, echo, false, true);
 		check_conn_options(conn, &vacopts);
 		print_processing_notice(conn, stage, progname, quiet);
 		found_objects = generate_catalog_list(conn, &vacopts, &objects, echo, &ntup);
+		vacuum_one_database(&cparams, &vacopts, stage,
+							&objects,
+							concurrentCons,
+							conn,
+							found_objects,
+							ntup,
+							progname, echo, quiet);
 
-		if (analyze_in_stages)
+		if (stage != ANALYZE_NO_STAGE)
 		{
-			for (stage = 0; stage < ANALYZE_NUM_STAGES; stage++)
+			for (stage = 1; stage < ANALYZE_NUM_STAGES; stage++)
 			{
 				/* the last pass disconnected the conn */
-				if (stage > 0)
-					conn = connectDatabase(&cparams, progname, echo, false, true);
+				conn = connectDatabase(&cparams, progname, echo, false, true);
+				print_processing_notice(conn, stage, progname, quiet);
 
 				vacuum_one_database(&cparams, &vacopts, stage,
 									&objects,
@@ -437,14 +458,6 @@ main(int argc, char *argv[])
 									progname, echo, quiet);
 			}
 		}
-		else
-			vacuum_one_database(&cparams, &vacopts, stage,
-								&objects,
-								concurrentCons,
-								conn,
-								found_objects,
-								ntup,
-								progname, echo, quiet);
 	}
 
 	exit(0);
@@ -763,6 +776,47 @@ generate_catalog_list(PGconn *conn,
 						  vacopts->min_mxid_age);
 	}
 
+	/*
+	 * If this query is for an analyze-only or analyze-in-stages, two
+	 * upgrade-centric operations, and force-analyze is NOT set, then
+	 * exclude any relations that already have their full compliment
+	 * of attribute stats and extended stats.
+	 *
+	 *
+	 */
+	if ((vacopts->analyze_only || vacopts->analyze_in_stages) &&
+		!vacopts->force_analyze)
+	{
+		/*
+		 * The pg_class in question has no pg_statistic rows representing
+		 * user-visible columns that lack a corresponding pg_statitic row.
+		 * Currently no differentiation is made for whether the
+		 * pg_statistic.stainherit is true or false.
+		 */
+		appendPQExpBufferStr(&catalog_query,
+							 " AND NOT EXISTS (\n"
+							 " SELECT NULL FROM pg_catalog.pg_attribute AS a\n"
+							 " WHERE a.attrelid OPERATOR(pg_catalog.=) c.oid\n"
+							 " AND a.attnum OPERATOR(pg_catalog.>) 0 AND NOT a.attisdropped\n"
+							 " AND NOT EXISTS (\n"
+							 " SELECT NULL FROM pg_catalog.pg_statistic AS s\n"
+							 " WHERE s.starelid OPERATOR(pg_catalog.=) c.oid\n"
+							 " AND s.staattnum OPERATOR(pg_catalog.=) a.attnum))\n");
+
+		/*
+		 * The pg_class entry has no pg_statistic_ext rows that lack a corresponding
+		 * pg_statistic_ext_data row. Currently no differentiation is made for whether
+		 * pg_statistic_exta_data.stxdinherit is true or false.
+		 */
+		appendPQExpBufferStr(&catalog_query,
+							 " AND NOT EXISTS (\n"
+							 " SELECT NULL FROM pg_catalog.pg_statistic_ext AS e\n"
+							 " WHERE e.stxrelid OPERATOR(pg_catalog.=) c.oid\n"
+							 " AND NOT EXISTS (\n"
+							 " SELECT NULL FROM pg_catalog.pg_statistic_ext_data AS d\n"
+							 " WHERE d.stxoid OPERATOR(pg_catalog.=) e.oid))\n");
+	}
+
 	/*
 	 * Execute the catalog query.  We use the default search_path for this
 	 * query for consistency with table lookups done elsewhere by the user.
@@ -959,7 +1013,6 @@ finish:
 static void
 vacuum_all_databases(ConnParams *cparams,
 					 vacuumingOptions *vacopts,
-					 bool analyze_in_stages,
 					 SimpleStringList *objects,
 					 int concurrentCons,
 					 const char *progname, bool echo, bool quiet)
@@ -972,7 +1025,7 @@ vacuum_all_databases(ConnParams *cparams,
 	SimpleStringList  **found_objects;
 	int				   *num_tuples;
 
-	stage = (analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
+	stage = (vacopts->analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
 
 	conn = connectMaintenanceDatabase(cparams, progname, echo);
 	result = executeQuery(conn,
diff --git a/doc/src/sgml/ref/vacuumdb.sgml b/doc/src/sgml/ref/vacuumdb.sgml
index 66fccb30a2..00ec927606 100644
--- a/doc/src/sgml/ref/vacuumdb.sgml
+++ b/doc/src/sgml/ref/vacuumdb.sgml
@@ -425,6 +425,12 @@ PostgreSQL documentation
        <para>
         Only calculate statistics for use by the optimizer (no vacuum).
        </para>
+       <para>
+        By default, this operation excludes relations that already have
+        statistics generated. If the option <option>--force-analyze</option>
+        is also specified, then relations with existing stastistics are not
+        excluded.
+       </para>
       </listitem>
      </varlistentry>
 
@@ -439,6 +445,47 @@ PostgreSQL documentation
         to produce usable statistics faster, and subsequent stages build the
         full statistics.
        </para>
+       <para>
+        This option is intended to be run after a <command>pg_upgrade</command>
+        to generate statistics for relations that have no stistatics or incomplete
+        statistics (such as those with extended statistics objects, which are not
+        imported on upgrade).
+       </para>
+       <para>
+        If the option <option>--force-analyze</option> is also specified, then
+        relations with existing stastistics are not excluded.
+        This option is only useful to analyze a database that currently has
+        no statistics or has wholly incorrect ones, such as if it is newly
+        populated from a restored dump.
+        Be aware that running with this option combinationin a database with
+        existing statistics may cause the query optimizer choices to become
+        transiently worse due to the low statistics targets of the early
+        stages.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--force-analyze</option></term>
+      <listitem>
+       <para>
+        This option can only be used if either <option>--analyze-only</option>
+        or <option>--analyze-in-stages</option> is specified. It modifies those
+        options to not filter out relations that already have statistics.
+       </para>
+       <para>
+
+        Only calculate statistics for use by the optimizer (no vacuum),
+        like <option>--analyze-only</option>.  Run three
+        stages of analyze; the first stage uses the lowest possible statistics
+        target (see <xref linkend="guc-default-statistics-target"/>)
+        to produce usable statistics faster, and subsequent stages build the
+        full statistics.
+       </para>
+
+       <para>
+        This option was created
+       </para>
 
        <para>
         This option is only useful to analyze a database that currently has
@@ -452,6 +499,7 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+
      <varlistentry>
        <term><option>-?</option></term>
        <term><option>--help</option></term>
-- 
2.47.0

v33-0011-Enable-pg_clear_relation_stats-to-handle-differe.patchtext/x-patch; charset=US-ASCII; name=v33-0011-Enable-pg_clear_relation_stats-to-handle-differe.patchDownload
From 8dafe8543f0c5e6204f8f8161e6c349b6a022d33 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Mon, 4 Nov 2024 16:21:39 -0500
Subject: [PATCH v33 11/11] Enable pg_clear_relation_stats to handle different
 default relpages.

If it comes to pass that relpages has the default of -1.0 for
partitioned tables (and indexes), then this patch will handle that.
---
 src/backend/statistics/relation_stats.c | 47 +++++++++++++------------
 1 file changed, 24 insertions(+), 23 deletions(-)

diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index 24f646048c..e27fe79406 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -19,15 +19,12 @@
 
 #include "access/heapam.h"
 #include "catalog/indexing.h"
+#include "catalog/pg_class_d.h"
 #include "statistics/stat_utils.h"
 #include "utils/fmgrprotos.h"
 #include "utils/syscache.h"
 #include "utils/fmgroids.h"
 
-#define DEFAULT_RELPAGES Int32GetDatum(0)
-#define DEFAULT_RELTUPLES Float4GetDatum(-1.0)
-#define DEFAULT_RELALLVISIBLE Int32GetDatum(0)
-
 /*
  * Positional argument numbers, names, and types for
  * relation_statistics_update().
@@ -51,14 +48,11 @@ static struct StatsArgInfo relarginfo[] =
 	[NUM_RELATION_STATS_ARGS] = {0}
 };
 
-static bool relation_statistics_update(FunctionCallInfo fcinfo, int elevel,
-									   bool inplace);
-
 /*
  * Internal function for modifying statistics for a relation.
  */
 static bool
-relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
+relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace, bool clear)
 {
 	Oid			reloid;
 	Relation	crel;
@@ -116,9 +110,19 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
 	pgcform = (Form_pg_class) GETSTRUCT(ctup);
 
 	/* relpages */
-	if (!PG_ARGISNULL(RELPAGES_ARG))
+	if (!PG_ARGISNULL(RELPAGES_ARG) || clear)
 	{
-		int32		relpages = PG_GETARG_INT32(RELPAGES_ARG);
+		int32		relpages;
+
+		if (clear)
+			/* relpages default varies by relkind */
+			if ((crel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) ||
+				(crel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+				relpages = -1;
+			else
+				relpages = 0;
+		else
+			relpages = PG_GETARG_INT32(RELPAGES_ARG);
 
 		/*
 		 * Partitioned tables may have relpages=-1. Note: for relations with
@@ -145,9 +149,9 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
 		}
 	}
 
-	if (!PG_ARGISNULL(RELTUPLES_ARG))
+	if (!PG_ARGISNULL(RELTUPLES_ARG) || clear)
 	{
-		float		reltuples = PG_GETARG_FLOAT4(RELTUPLES_ARG);
+		float		reltuples = (clear) ? -1.0 : PG_GETARG_FLOAT4(RELTUPLES_ARG);
 
 		if (reltuples < -1.0)
 		{
@@ -170,9 +174,9 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
 
 	}
 
-	if (!PG_ARGISNULL(RELALLVISIBLE_ARG))
+	if (!PG_ARGISNULL(RELALLVISIBLE_ARG) || clear)
 	{
-		int32		relallvisible = PG_GETARG_INT32(RELALLVISIBLE_ARG);
+		int32		relallvisible = (clear) ? 0 : PG_GETARG_INT32(RELALLVISIBLE_ARG);
 
 		if (relallvisible < 0)
 		{
@@ -226,7 +230,7 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
 Datum
 pg_set_relation_stats(PG_FUNCTION_ARGS)
 {
-	relation_statistics_update(fcinfo, ERROR, false);
+	relation_statistics_update(fcinfo, ERROR, false, false);
 	PG_RETURN_VOID();
 }
 
@@ -243,14 +247,11 @@ pg_clear_relation_stats(PG_FUNCTION_ARGS)
 
 	newfcinfo->args[0].value = PG_GETARG_OID(0);
 	newfcinfo->args[0].isnull = PG_ARGISNULL(0);
-	newfcinfo->args[1].value = DEFAULT_RELPAGES;
-	newfcinfo->args[1].isnull = false;
-	newfcinfo->args[2].value = DEFAULT_RELTUPLES;
-	newfcinfo->args[2].isnull = false;
-	newfcinfo->args[3].value = DEFAULT_RELALLVISIBLE;
-	newfcinfo->args[3].isnull = false;
+	newfcinfo->args[1].isnull = true;
+	newfcinfo->args[2].isnull = true;
+	newfcinfo->args[3].isnull = true;
 
-	relation_statistics_update(newfcinfo, ERROR, false);
+	relation_statistics_update(newfcinfo, ERROR, false, true);
 	PG_RETURN_VOID();
 }
 
@@ -268,7 +269,7 @@ pg_restore_relation_stats(PG_FUNCTION_ARGS)
 										  relarginfo, WARNING))
 		result = false;
 
-	if (!relation_statistics_update(positional_fcinfo, WARNING, true))
+	if (!relation_statistics_update(positional_fcinfo, WARNING, true, false))
 		result = false;
 
 	PG_RETURN_BOOL(result);
-- 
2.47.0

#241Magnus Hagander
magnus@hagander.net
In reply to: Bruce Momjian (#229)
Re: Statistics Import and Export

On Tue, Nov 19, 2024 at 1:50 PM Bruce Momjian <bruce@momjian.us> wrote:

On Mon, Nov 18, 2024 at 08:42:35PM -0500, Bruce Momjian wrote:

On Mon, Nov 18, 2024 at 08:29:10PM -0500, Corey Huinker wrote:

That's not a great surprise for group 6, but I have to believe that

group is

smaller than group 5, and it's definitely smaller than the group of

users that

need to upgrade.

Again, a clean API is the goal, not surprise calculus.

Maybe I was too harsh. "Surprise calculus" is fine, but only after we
have an API that will be clearly understood by new users. We have to
assume that in the long run new users will use this API more than
existing users.

If you want to avoid both the surprise and confusion factor mentioned
before, maybe what's needed is to *remove* --analyze-in-stages, and replace
it with --analyze-missing-in-stages and --analyze-all-in-stages (with the
clear warning about what --analyze-all-in-stages can do to your system if
you already have statistics).

That goes with the "immediate breakage that you see right away is better
than silently doing the unexpected where you might not notice the problem
until much later".

That might trade some of that surprise and confusion for annoyance instead,
but going forward that might be a clearer path?

--
Magnus Hagander
Me: https://www.hagander.net/ <http://www.hagander.net/&gt;
Work: https://www.redpill-linpro.com/ <http://www.redpill-linpro.com/&gt;

#242Bruce Momjian
bruce@momjian.us
In reply to: Magnus Hagander (#241)
Re: Statistics Import and Export

On Wed, Nov 27, 2024 at 02:44:01PM +0100, Magnus Hagander wrote:

If you want to avoid both the surprise and confusion factor mentioned before,
maybe what's needed is to *remove* --analyze-in-stages, and replace it with
--analyze-missing-in-stages and --analyze-all-in-stages (with the clear warning
about what --analyze-all-in-stages can do to your system if you already have
statistics).

That goes with the "immediate breakage that you see right away is better than
silently doing the unexpected where you might not notice the problem until much
later".

That might trade some of that surprise and confusion for annoyance instead, but
going forward that might be a clearer path?

Oh, so remove --analyze-in-stages and have it issue a suggestion, and
make two versions --- yeah, that would work too.

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

When a patient asks the doctor, "Am I going to die?", he means
"Am I going to die soon?"

#243Alvaro Herrera
alvherre@alvh.no-ip.org
In reply to: Bruce Momjian (#242)
Re: Statistics Import and Export

On 2024-Nov-27, Bruce Momjian wrote:

On Wed, Nov 27, 2024 at 02:44:01PM +0100, Magnus Hagander wrote:

If you want to avoid both the surprise and confusion factor mentioned before,
maybe what's needed is to *remove* --analyze-in-stages, and replace it with
--analyze-missing-in-stages and --analyze-all-in-stages (with the clear warning
about what --analyze-all-in-stages can do to your system if you already have
statistics).

That goes with the "immediate breakage that you see right away is better than
silently doing the unexpected where you might not notice the problem until much
later".

That might trade some of that surprise and confusion for annoyance instead, but
going forward that might be a clearer path?

Oh, so remove --analyze-in-stages and have it issue a suggestion, and
make two versions --- yeah, that would work too.

Maybe not remove the option, but add a required parameter:
--analyze-in-stages=all / missing

That way, if the option is missing, the user can adapt the command line
according to need.

--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
"How amazing is that? I call it a night and come back to find that a bug has
been identified and patched while I sleep." (Robert Davidson)
http://archives.postgresql.org/pgsql-sql/2006-03/msg00378.php

#244Nathan Bossart
nathandbossart@gmail.com
In reply to: Alvaro Herrera (#243)
Re: Statistics Import and Export

On Wed, Nov 27, 2024 at 04:00:02PM +0100, Alvaro Herrera wrote:

On 2024-Nov-27, Bruce Momjian wrote:

On Wed, Nov 27, 2024 at 02:44:01PM +0100, Magnus Hagander wrote:

If you want to avoid both the surprise and confusion factor mentioned before,
maybe what's needed is to *remove* --analyze-in-stages, and replace it with
--analyze-missing-in-stages and --analyze-all-in-stages (with the clear warning
about what --analyze-all-in-stages can do to your system if you already have
statistics).

That goes with the "immediate breakage that you see right away is better than
silently doing the unexpected where you might not notice the problem until much
later".

That might trade some of that surprise and confusion for annoyance instead, but
going forward that might be a clearer path?

Oh, so remove --analyze-in-stages and have it issue a suggestion, and
make two versions --- yeah, that would work too.

We did something similar when we removed exclusive backup mode.
pg_start_backup() and pg_stop_backup() were renamed to pg_backup_start()
and pg_backup_stop() to prevent folks' backup scripts from silently
changing behavior after an upgrade.

Maybe not remove the option, but add a required parameter:
--analyze-in-stages=all / missing

That way, if the option is missing, the user can adapt the command line
according to need.

I like this idea.

--
nathan

#245Bruce Momjian
bruce@momjian.us
In reply to: Nathan Bossart (#244)
Re: Statistics Import and Export

On Wed, Nov 27, 2024 at 09:18:45AM -0600, Nathan Bossart wrote:

On Wed, Nov 27, 2024 at 04:00:02PM +0100, Alvaro Herrera wrote:

On 2024-Nov-27, Bruce Momjian wrote:

On Wed, Nov 27, 2024 at 02:44:01PM +0100, Magnus Hagander wrote:

If you want to avoid both the surprise and confusion factor mentioned before,
maybe what's needed is to *remove* --analyze-in-stages, and replace it with
--analyze-missing-in-stages and --analyze-all-in-stages (with the clear warning
about what --analyze-all-in-stages can do to your system if you already have
statistics).

That goes with the "immediate breakage that you see right away is better than
silently doing the unexpected where you might not notice the problem until much
later".

That might trade some of that surprise and confusion for annoyance instead, but
going forward that might be a clearer path?

Oh, so remove --analyze-in-stages and have it issue a suggestion, and
make two versions --- yeah, that would work too.

We did something similar when we removed exclusive backup mode.
pg_start_backup() and pg_stop_backup() were renamed to pg_backup_start()
and pg_backup_stop() to prevent folks' backup scripts from silently
changing behavior after an upgrade.

Maybe not remove the option, but add a required parameter:
--analyze-in-stages=all / missing

That way, if the option is missing, the user can adapt the command line
according to need.

I like this idea.

Uh, do we have parameters that require a boolean option like this?
Would there be a default?

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

When a patient asks the doctor, "Am I going to die?", he means
"Am I going to die soon?"

#246Alvaro Herrera
alvherre@alvh.no-ip.org
In reply to: Bruce Momjian (#245)
Re: Statistics Import and Export

On 2024-Nov-27, Bruce Momjian wrote:

On Wed, Nov 27, 2024 at 09:18:45AM -0600, Nathan Bossart wrote:

Maybe not remove the option, but add a required parameter:
--analyze-in-stages=all / missing

That way, if the option is missing, the user can adapt the command line
according to need.

I like this idea.

Would there be a default?

There would be no default. Running with no option given would raise an
error. The point is: you want to break scripts currently running
--analyze-in-stages so that they can make a choice of which of these two
modes to run. Your proposal (as I understand it) is to remove the
--analyze-in-stages option and add two other options. My proposal is to
keep --analyze-in-stages, but require it to have a specifier of which
mode to run. Both achieve what you want, but I think mine achieves it
in a cleaner way.

Uh, do we have parameters that require a boolean option like this?

I'm not sure what exactly are you asking here.

--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
"La gente vulgar sólo piensa en pasar el tiempo;
el que tiene talento, en aprovecharlo"

#247Nathan Bossart
nathandbossart@gmail.com
In reply to: Alvaro Herrera (#246)
Re: Statistics Import and Export

On Wed, Nov 27, 2024 at 04:57:35PM +0100, Alvaro Herrera wrote:

On 2024-Nov-27, Bruce Momjian wrote:

Uh, do we have parameters that require a boolean option like this?

I'm not sure what exactly are you asking here.

We do have options like initdb's --sync-method that require specifying one
of a small set of valid arguments. I don't see any reason that wouldn't
work here, too.

--
nathan

#248Tom Lane
tgl@sss.pgh.pa.us
In reply to: Alvaro Herrera (#246)
Re: Statistics Import and Export

Alvaro Herrera <alvherre@alvh.no-ip.org> writes:

On 2024-Nov-27, Bruce Momjian wrote:

Would there be a default?

There would be no default. Running with no option given would raise an
error. The point is: you want to break scripts currently running
--analyze-in-stages so that they can make a choice of which of these two
modes to run. Your proposal (as I understand it) is to remove the
--analyze-in-stages option and add two other options. My proposal is to
keep --analyze-in-stages, but require it to have a specifier of which
mode to run. Both achieve what you want, but I think mine achieves it
in a cleaner way.

I do not like the idea of breaking existing upgrade scripts,
especially not by requiring them to use a parameter that older
vacuumdb versions will reject. That makes it impossible to have a
script that is version independent. I really doubt that there is any
usability improvement to be had here that's worth that.

How about causing "--analyze-in-stages" (as currently spelled) to
be a no-op? We could keep the behavior available under some other
name.

regards, tom lane

#249Bruce Momjian
bruce@momjian.us
In reply to: Alvaro Herrera (#246)
Re: Statistics Import and Export

On Wed, Nov 27, 2024 at 04:57:35PM +0100, Álvaro Herrera wrote:

On 2024-Nov-27, Bruce Momjian wrote:
There would be no default. Running with no option given would raise an
error. The point is: you want to break scripts currently running
--analyze-in-stages so that they can make a choice of which of these two
modes to run. Your proposal (as I understand it) is to remove the
--analyze-in-stages option and add two other options. My proposal is to
keep --analyze-in-stages, but require it to have a specifier of which
mode to run. Both achieve what you want, but I think mine achieves it
in a cleaner way.

Uh, do we have parameters that require a boolean option like this?

I'm not sure what exactly are you asking here.

I can't think of a Postgres option that can take only one of two
possible values, and where there is no default value.

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

When a patient asks the doctor, "Am I going to die?", he means
"Am I going to die soon?"

#250Bruce Momjian
bruce@momjian.us
In reply to: Tom Lane (#248)
Re: Statistics Import and Export

On Wed, Nov 27, 2024 at 11:44:25AM -0500, Tom Lane wrote:

Alvaro Herrera <alvherre@alvh.no-ip.org> writes:

On 2024-Nov-27, Bruce Momjian wrote:

Would there be a default?

There would be no default. Running with no option given would raise an
error. The point is: you want to break scripts currently running
--analyze-in-stages so that they can make a choice of which of these two
modes to run. Your proposal (as I understand it) is to remove the
--analyze-in-stages option and add two other options. My proposal is to
keep --analyze-in-stages, but require it to have a specifier of which
mode to run. Both achieve what you want, but I think mine achieves it
in a cleaner way.

I do not like the idea of breaking existing upgrade scripts,
especially not by requiring them to use a parameter that older
vacuumdb versions will reject. That makes it impossible to have a
script that is version independent. I really doubt that there is any
usability improvement to be had here that's worth that.

How about causing "--analyze-in-stages" (as currently spelled) to
be a no-op? We could keep the behavior available under some other
name.

Uh, I guess we could do that, but we should emit something like
"--analyze-in-stages option ignored".

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

When a patient asks the doctor, "Am I going to die?", he means
"Am I going to die soon?"

#251Alvaro Herrera
alvherre@alvh.no-ip.org
In reply to: Tom Lane (#248)
Re: Statistics Import and Export

On 2024-Nov-27, Tom Lane wrote:

I do not like the idea of breaking existing upgrade scripts,
especially not by requiring them to use a parameter that older
vacuumdb versions will reject. That makes it impossible to have a
script that is version independent. I really doubt that there is any
usability improvement to be had here that's worth that.

I was only suggesting to break it because it was said upthread that that
was desirable behavior.

How about causing "--analyze-in-stages" (as currently spelled) to
be a no-op? We could keep the behavior available under some other
name.

I think making it a no-op isn't useful, because people who run the old
scripts will get the behavior we do not want: clobber the statistics and
recompute them, losing the benefit that this feature brings.

On 2024-Nov-27, Bruce Momjian wrote:

Uh, I guess we could do that, but we should emit something like
"--analyze-in-stages option ignored".

I think emitting a message is not useful. It's quite possible that the
output of pg_upgrade will be redirected somewhere and this will go
unnoticed.

Maybe the most convenient for users is to keep "vacuumdb
--analyze-in-stages" doing exactly what we want to happen after
pg_upgrade, that is, in 18+, only recreate the missing stats. This is
because of what Corey said about messaging: many users are not going to
get our message that they need to adapt their scripts, so they won't.
Breaking the script would convey that message pretty quickly, but you're
right that it's not very convenient.

For people that want to use the old behavior of recomputing _all_
statistics not just the missing ones, we could add a different switch,
or an (optional) option to --analyze-in-stages.

--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/

#252Magnus Hagander
magnus@hagander.net
In reply to: Tom Lane (#248)
Re: Statistics Import and Export

On Wed, Nov 27, 2024, 17:44 Tom Lane <tgl@sss.pgh.pa.us> wrote:

Alvaro Herrera <alvherre@alvh.no-ip.org> writes:

On 2024-Nov-27, Bruce Momjian wrote:

Would there be a default?

There would be no default. Running with no option given would raise an
error. The point is: you want to break scripts currently running
--analyze-in-stages so that they can make a choice of which of these two
modes to run. Your proposal (as I understand it) is to remove the
--analyze-in-stages option and add two other options. My proposal is to
keep --analyze-in-stages, but require it to have a specifier of which
mode to run. Both achieve what you want, but I think mine achieves it
in a cleaner way.

I do not like the idea of breaking existing upgrade scripts,
especially not by requiring them to use a parameter that older
vacuumdb versions will reject. That makes it impossible to have a
script that is version independent. I really doubt that there is any
usability improvement to be had here that's worth that.

How about causing "--analyze-in-stages" (as currently spelled) to
be a no-op? We could keep the behavior available under some other
name.

If we're doing that we might as well make this be the "when missing".

If we make it do nothing then we surprise the users of it today just as
much as we do if we make it "when missing". And it would actually solve the
problem for the others. But the point was that people didn't like silently
changing the behavior of the existing parameter - and making it a noop
would change it even more.

/Magnus

Show quoted text
#253Corey Huinker
corey.huinker@gmail.com
In reply to: Alvaro Herrera (#251)
Re: Statistics Import and Export

For people that want to use the old behavior of recomputing _all_
statistics not just the missing ones, we could add a different switch,
or an (optional) option to --analyze-in-stages.

The current patchset provides that in the form of the parameter
"--force-analyze", which is a modifier to "--analyze-in-stages" and
"--analyze-only".

#254Bruce Momjian
bruce@momjian.us
In reply to: Corey Huinker (#253)
Re: Statistics Import and Export

On Wed, Nov 27, 2024 at 01:15:45PM -0500, Corey Huinker wrote:

For people that want to use the old behavior of recomputing _all_
statistics not just the missing ones, we could add a different switch,
or an (optional) option to --analyze-in-stages.

The current patchset provides that in the form of the parameter
"--force-analyze", which is a modifier to "--analyze-in-stages" and
"--analyze-only". 

I don't think there is consensus to change --analyze-only, only maybe
--analyze-in-stages.

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

When a patient asks the doctor, "Am I going to die?", he means
"Am I going to die soon?"

#255Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#240)
1 attachment(s)
Re: Statistics Import and Export

On Wed, 2024-11-27 at 00:08 -0500, Corey Huinker wrote:

0003 - Re-enabling in-place updates because catalog bloat bad.

Attached is my version of this patch, which I intend to commit soon.

I added docs and tests, and I refactored a bit to check the arguments
first.

Also, I separated the mvcc and in-place paths, so that it was easier to
review that each one is following the right protocol.

Regards,
Jeff Davis

Attachments:

v33j-0001-Use-in-place-updates-for-pg_restore_relation_st.patchtext/x-patch; charset=UTF-8; name=v33j-0001-Use-in-place-updates-for-pg_restore_relation_st.patchDownload
From a41503b82469297797db78163426c40a7a86317f Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Fri, 6 Dec 2024 12:32:29 -0800
Subject: [PATCH v33j] Use in-place updates for pg_restore_relation_stats().

This matches the behavior of vac_update_relstats(), which is important
to avoid bloating pg_class.

Author: Corey Huinker
Discussion: https://postgr.es/m/CADkLM=fc3je+ufv3gsHqjjSSf+t8674RXpuXW62EL55MUEQd-g@mail.gmail.com
---
 doc/src/sgml/func.sgml                     |   8 +
 src/backend/statistics/relation_stats.c    | 198 +++++++++++++--------
 src/test/regress/expected/stats_import.out |  61 +++++++
 src/test/regress/sql/stats_import.sql      |  37 ++++
 4 files changed, 233 insertions(+), 71 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 8b81106fa23..2c35252dc06 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30175,6 +30175,14 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          function is to maintain a consistent function signature to avoid
          errors when restoring statistics from previous versions.
         </para>
+        <para>
+         To match the behavior of <xref linkend="sql-vacuum"/> and <xref
+         linkend="sql-analyze"/> when updating relation statistics,
+         <function>pg_restore_relation_stats()</function> does not follow MVCC
+         transactional semantics (see <xref linkend="mvcc"/>). New relation
+         statistics may be durable even if the transaction aborts, and the
+         changes are not isolated from other transactions.
+        </para>
         <para>
          Arguments are passed as pairs of <replaceable>argname</replaceable>
          and <replaceable>argvalue</replaceable>, where
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index e619d5cf5b1..f84157b16ab 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -20,6 +20,7 @@
 #include "access/heapam.h"
 #include "catalog/indexing.h"
 #include "statistics/stat_utils.h"
+#include "utils/fmgroids.h"
 #include "utils/fmgrprotos.h"
 #include "utils/syscache.h"
 
@@ -50,59 +51,28 @@ static struct StatsArgInfo relarginfo[] =
 	[NUM_RELATION_STATS_ARGS] = {0}
 };
 
-static bool relation_statistics_update(FunctionCallInfo fcinfo, int elevel);
+static bool relation_statistics_update(FunctionCallInfo fcinfo, int elevel,
+									   bool inplace);
 
 /*
  * Internal function for modifying statistics for a relation.
  */
 static bool
-relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
+relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
 {
 	Oid			reloid;
 	Relation	crel;
-	HeapTuple	ctup;
-	Form_pg_class pgcform;
-	int			replaces[3] = {0};
-	Datum		values[3] = {0};
-	bool		nulls[3] = {0};
-	int			ncols = 0;
-	TupleDesc	tupdesc;
+	int32		relpages = DEFAULT_RELPAGES;
+	bool		update_relpages = false;
+	float		reltuples = DEFAULT_RELTUPLES;
+	bool		update_reltuples = false;
+	int32		relallvisible = DEFAULT_RELALLVISIBLE;
+	bool		update_relallvisible = false;
 	bool		result = true;
 
-	stats_check_required_arg(fcinfo, relarginfo, RELATION_ARG);
-	reloid = PG_GETARG_OID(RELATION_ARG);
-
-	if (RecoveryInProgress())
-		ereport(ERROR,
-				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
-				 errmsg("recovery is in progress"),
-				 errhint("Statistics cannot be modified during recovery.")));
-
-	stats_lock_check_privileges(reloid);
-
-	/*
-	 * Take RowExclusiveLock on pg_class, consistent with
-	 * vac_update_relstats().
-	 */
-	crel = table_open(RelationRelationId, RowExclusiveLock);
-
-	tupdesc = RelationGetDescr(crel);
-	ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(reloid));
-	if (!HeapTupleIsValid(ctup))
-	{
-		ereport(elevel,
-				(errcode(ERRCODE_OBJECT_IN_USE),
-				 errmsg("pg_class entry for relid %u not found", reloid)));
-		table_close(crel, RowExclusiveLock);
-		return false;
-	}
-
-	pgcform = (Form_pg_class) GETSTRUCT(ctup);
-
-	/* relpages */
 	if (!PG_ARGISNULL(RELPAGES_ARG))
 	{
-		int32		relpages = PG_GETARG_INT32(RELPAGES_ARG);
+		relpages = PG_GETARG_INT32(RELPAGES_ARG);
 
 		/*
 		 * Partitioned tables may have relpages=-1. Note: for relations with
@@ -116,17 +86,13 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 					 errmsg("relpages cannot be < -1")));
 			result = false;
 		}
-		else if (relpages != pgcform->relpages)
-		{
-			replaces[ncols] = Anum_pg_class_relpages;
-			values[ncols] = Int32GetDatum(relpages);
-			ncols++;
-		}
+		else
+			update_relpages = true;
 	}
 
 	if (!PG_ARGISNULL(RELTUPLES_ARG))
 	{
-		float		reltuples = PG_GETARG_FLOAT4(RELTUPLES_ARG);
+		reltuples = PG_GETARG_FLOAT4(RELTUPLES_ARG);
 
 		if (reltuples < -1.0)
 		{
@@ -135,18 +101,13 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 					 errmsg("reltuples cannot be < -1.0")));
 			result = false;
 		}
-		else if (reltuples != pgcform->reltuples)
-		{
-			replaces[ncols] = Anum_pg_class_reltuples;
-			values[ncols] = Float4GetDatum(reltuples);
-			ncols++;
-		}
-
+		else
+			update_reltuples = true;
 	}
 
 	if (!PG_ARGISNULL(RELALLVISIBLE_ARG))
 	{
-		int32		relallvisible = PG_GETARG_INT32(RELALLVISIBLE_ARG);
+		relallvisible = PG_GETARG_INT32(RELALLVISIBLE_ARG);
 
 		if (relallvisible < 0)
 		{
@@ -155,23 +116,118 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 					 errmsg("relallvisible cannot be < 0")));
 			result = false;
 		}
-		else if (relallvisible != pgcform->relallvisible)
+		else
+			update_relallvisible = true;
+	}
+
+	stats_check_required_arg(fcinfo, relarginfo, RELATION_ARG);
+	reloid = PG_GETARG_OID(RELATION_ARG);
+
+	if (RecoveryInProgress())
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("recovery is in progress"),
+				 errhint("Statistics cannot be modified during recovery.")));
+
+	stats_lock_check_privileges(reloid);
+
+	/*
+	 * Take RowExclusiveLock on pg_class, consistent with
+	 * vac_update_relstats().
+	 */
+	crel = table_open(RelationRelationId, RowExclusiveLock);
+
+	if (inplace)
+	{
+		HeapTuple	ctup = NULL;
+		ScanKeyData key[1];
+		Form_pg_class pgcform;
+		void	   *inplace_state = NULL;
+		bool		dirty = false;
+
+		ScanKeyInit(&key[0], Anum_pg_class_oid, BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(reloid));
+		systable_inplace_update_begin(crel, ClassOidIndexId, true, NULL, 1, key,
+									  &ctup, &inplace_state);
+		if (!HeapTupleIsValid(ctup))
+			elog(ERROR, "pg_class entry for relid %u vanished while updating statistics",
+				 reloid);
+		pgcform = (Form_pg_class) GETSTRUCT(ctup);
+
+		if (update_relpages && relpages != pgcform->relpages)
+		{
+			pgcform->relpages = relpages;
+			dirty = true;
+		}
+		if (update_reltuples && pgcform->reltuples != reltuples)
 		{
-			replaces[ncols] = Anum_pg_class_relallvisible;
-			values[ncols] = Int32GetDatum(relallvisible);
-			ncols++;
+			pgcform->reltuples = reltuples;
+			dirty = true;
+		}
+		if (update_relallvisible && pgcform->relallvisible != relallvisible)
+		{
+			pgcform->relallvisible = relallvisible;
+			dirty = true;
 		}
-	}
 
-	/* only update pg_class if there is a meaningful change */
-	if (ncols > 0)
+		if (dirty)
+			systable_inplace_update_finish(inplace_state, ctup);
+		else
+			systable_inplace_update_cancel(inplace_state);
+	}
+	else
 	{
-		HeapTuple	newtup;
+		TupleDesc	tupdesc = RelationGetDescr(crel);
+		HeapTuple	ctup;
+		Form_pg_class pgcform;
+		int			replaces[3] = {0};
+		Datum		values[3] = {0};
+		bool		nulls[3] = {0};
+		int			nreplaces = 0;
+
+		ctup = SearchSysCache1(RELOID, ObjectIdGetDatum(reloid));
+		if (!HeapTupleIsValid(ctup))
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_OBJECT_IN_USE),
+					 errmsg("pg_class entry for relid %u not found", reloid)));
+			table_close(crel, RowExclusiveLock);
+			return false;
+		}
+		pgcform = (Form_pg_class) GETSTRUCT(ctup);
+
+		if (update_relpages && relpages != pgcform->relpages)
+		{
+			replaces[nreplaces] = Anum_pg_class_relpages;
+			values[nreplaces] = Int32GetDatum(relpages);
+			nreplaces++;
+		}
+
+		if (update_reltuples && reltuples != pgcform->reltuples)
+		{
+			replaces[nreplaces] = Anum_pg_class_reltuples;
+			values[nreplaces] = Float4GetDatum(reltuples);
+			nreplaces++;
+		}
+
+		if (update_relallvisible && relallvisible != pgcform->relallvisible)
+		{
+			replaces[nreplaces] = Anum_pg_class_relallvisible;
+			values[nreplaces] = Int32GetDatum(relallvisible);
+			nreplaces++;
+		}
+
+		if (nreplaces > 0)
+		{
+			HeapTuple	newtup;
+
+			newtup = heap_modify_tuple_by_cols(ctup, tupdesc, nreplaces,
+											   replaces, values, nulls);
+			CatalogTupleUpdate(crel, &newtup->t_self, newtup);
+			heap_freetuple(newtup);
+		}
 
-		newtup = heap_modify_tuple_by_cols(ctup, tupdesc, ncols, replaces, values,
-										   nulls);
-		CatalogTupleUpdate(crel, &newtup->t_self, newtup);
-		heap_freetuple(newtup);
+		ReleaseSysCache(ctup);
 	}
 
 	/* release the lock, consistent with vac_update_relstats() */
@@ -188,7 +244,7 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 Datum
 pg_set_relation_stats(PG_FUNCTION_ARGS)
 {
-	relation_statistics_update(fcinfo, ERROR);
+	relation_statistics_update(fcinfo, ERROR, false);
 	PG_RETURN_VOID();
 }
 
@@ -212,7 +268,7 @@ pg_clear_relation_stats(PG_FUNCTION_ARGS)
 	newfcinfo->args[3].value = DEFAULT_RELALLVISIBLE;
 	newfcinfo->args[3].isnull = false;
 
-	relation_statistics_update(newfcinfo, ERROR);
+	relation_statistics_update(newfcinfo, ERROR, false);
 	PG_RETURN_VOID();
 }
 
@@ -230,7 +286,7 @@ pg_restore_relation_stats(PG_FUNCTION_ARGS)
 										  relarginfo, WARNING))
 		result = false;
 
-	if (!relation_statistics_update(positional_fcinfo, WARNING))
+	if (!relation_statistics_update(positional_fcinfo, WARNING, true))
 		result = false;
 
 	PG_RETURN_BOOL(result);
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index aab862c97c7..fb50da1cd83 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -105,6 +105,47 @@ WHERE oid = 'stats_import.test'::regclass;
        18 |       401 |             5
 (1 row)
 
+-- test MVCC behavior: changes do not persist after abort (in contrast
+-- to pg_restore_relation_stats(), which uses in-place updates).
+BEGIN;
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => NULL::integer,
+        reltuples => 4000.0::real,
+        relallvisible => 4::integer);
+ pg_set_relation_stats 
+-----------------------
+ 
+(1 row)
+
+ABORT;
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       18 |       401 |             5
+(1 row)
+
+BEGIN;
+SELECT
+    pg_catalog.pg_clear_relation_stats(
+        'stats_import.test'::regclass);
+ pg_clear_relation_stats 
+-------------------------
+ 
+(1 row)
+
+ABORT;
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       18 |       401 |             5
+(1 row)
+
 -- clear
 SELECT
     pg_catalog.pg_clear_relation_stats(
@@ -705,6 +746,25 @@ WHERE oid = 'stats_import.test'::regclass;
 (1 row)
 
 -- ok: just relpages
+SELECT pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'version', 150000::integer,
+        'relpages', '15'::integer);
+ pg_restore_relation_stats 
+---------------------------
+ t
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       15 |       400 |             4
+(1 row)
+
+-- test non-MVCC behavior: new value should persist after abort
+BEGIN;
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
         'version', 150000::integer,
@@ -714,6 +774,7 @@ SELECT pg_restore_relation_stats(
  t
 (1 row)
 
+ABORT;
 SELECT relpages, reltuples, relallvisible
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index 31455b58c1d..d3058bf8f6b 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -76,6 +76,31 @@ SELECT relpages, reltuples, relallvisible
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
+-- test MVCC behavior: changes do not persist after abort (in contrast
+-- to pg_restore_relation_stats(), which uses in-place updates).
+BEGIN;
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test'::regclass,
+        relpages => NULL::integer,
+        reltuples => 4000.0::real,
+        relallvisible => 4::integer);
+ABORT;
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+BEGIN;
+SELECT
+    pg_catalog.pg_clear_relation_stats(
+        'stats_import.test'::regclass);
+ABORT;
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
 -- clear
 SELECT
     pg_catalog.pg_clear_relation_stats(
@@ -565,10 +590,22 @@ FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: just relpages
+SELECT pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'version', 150000::integer,
+        'relpages', '15'::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+-- test non-MVCC behavior: new value should persist after abort
+BEGIN;
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
         'version', 150000::integer,
         'relpages', '16'::integer);
+ABORT;
 
 SELECT relpages, reltuples, relallvisible
 FROM pg_class
-- 
2.34.1

#256Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#239)
Re: Statistics Import and Export

On Tue, 2024-11-26 at 17:11 -0500, Corey Huinker wrote:

* Why does binary upgrade cause statistics to be dumped? Can you
just
make pg_upgrade specify the appropriate set of flags?

That decision goes back a ways, I tried to dig in the archives last
night but I was getting a Server Error on postgresql.org.

I suggest that pg_upgrade be changed to pass --no-data to pg_dump,
rather than --schema-only.

That way, you don't need to create a special case for the pg_dump
default that depends on whether it's a binary upgrade or not.

If wanted, there could also be a new option to pg_upgrade to specify --
with-statistics (default, passes --no-data to pg_dump) or --no-
statistics (passes --schema-only to pg_dump). But that option is
probably not necessary; everyone upgrading probably wants the stats.

Regards,
Jeff Davis

#257Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#256)
Re: Statistics Import and Export

On Sat, Dec 7, 2024 at 2:27 PM Jeff Davis <pgsql@j-davis.com> wrote:

On Tue, 2024-11-26 at 17:11 -0500, Corey Huinker wrote:

* Why does binary upgrade cause statistics to be dumped? Can you
just
make pg_upgrade specify the appropriate set of flags?

That decision goes back a ways, I tried to dig in the archives last
night but I was getting a Server Error on postgresql.org.

I suggest that pg_upgrade be changed to pass --no-data to pg_dump,
rather than --schema-only.

That way, you don't need to create a special case for the pg_dump
default that depends on whether it's a binary upgrade or not.

+1

If wanted, there could also be a new option to pg_upgrade to specify --
with-statistics (default, passes --no-data to pg_dump) or --no-
statistics (passes --schema-only to pg_dump). But that option is
probably not necessary; everyone upgrading probably wants the stats.

This makes sense, though perhaps instead of --schema-only perhaps we should
pass both --no-statistics and --no-data. I don't envision a fourth option
to the new data/schema/stats triumvirate, but --no-statistics shouldn't
have a bearing on that future fourth option.

#258Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#257)
Re: Statistics Import and Export

On Sat, 2024-12-07 at 14:56 -0500, Corey Huinker wrote:

This makes sense, though perhaps instead of --schema-only perhaps we
should pass both --no-statistics and --no-data. I don't envision a
fourth option to the new data/schema/stats triumvirate, but --no-
statistics shouldn't have a bearing on that future fourth option.

+1, assuming such an option is wanted at all. I suppose it should be
there for the unlikely (and hopefully impossible) case that statistics
are causing a problem during upgrade.

Regards,
Jeff Davis 

#259Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#258)
11 attachment(s)
Re: Statistics Import and Export

+1, assuming such an option is wanted at all. I suppose it should be
there for the unlikely (and hopefully impossible) case that statistics
are causing a problem during upgrade.

Here you go, rebased and re-ordered:

0001-0004 are the pg_dump/pg_upgrade related patches.
0005 is an optimization to the attribute stats update
0006-0011 is the still-up-for-debate vacuumdb changes.

The patch for handling the as-yet-theoretical change to default relpages
for partitioned tables got messy in the rebase, so I decided to just leave
it out for now, as the change to relpages looks increasingly unlikely.

Attachments:

v34-0002-Add-no-data-option.patchtext/x-patch; charset=US-ASCII; name=v34-0002-Add-no-data-option.patchDownload
From c10e1c1f87bc8039dfae38077d6b37446bcf408d Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 14 Nov 2024 04:58:17 -0500
Subject: [PATCH v34 02/11] Add --no-data option.

This option is useful for situations where someone wishes to test
query plans from a production database without copying production data.
---
 src/bin/pg_dump/pg_backup.h          | 2 ++
 src/bin/pg_dump/pg_backup_archiver.c | 2 ++
 src/bin/pg_dump/pg_dump.c            | 7 ++++++-
 src/bin/pg_dump/pg_restore.c         | 5 ++++-
 4 files changed, 14 insertions(+), 2 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 8fbb39d399..241855d017 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -110,6 +110,7 @@ typedef struct _restoreOptions
 	int			column_inserts;
 	int			if_exists;
 	int			no_comments;	/* Skip comments */
+	int			no_data;			/* Skip data */
 	int			no_publications;	/* Skip publication entries */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
@@ -185,6 +186,7 @@ typedef struct _dumpOptions
 	int			no_publications;
 	int			no_subscriptions;
 	int			no_statistics;
+	int			no_data;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 185d7fbb7e..41001e64ac 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -186,6 +186,8 @@ dumpOptionsFromRestoreOptions(RestoreOptions *ropt)
 	dopt->no_publications = ropt->no_publications;
 	dopt->no_security_labels = ropt->no_security_labels;
 	dopt->no_subscriptions = ropt->no_subscriptions;
+	dopt->no_data = ropt->no_data;
+	dopt->no_statistics = ropt->no_statistics;
 	dopt->lockWaitTimeout = ropt->lockWaitTimeout;
 	dopt->include_everything = ropt->include_everything;
 	dopt->enable_row_security = ropt->enable_row_security;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 8f85b04c33..c8a0b4afdf 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -494,6 +494,7 @@ main(int argc, char **argv)
 		{"strict-names", no_argument, &strict_names, 1},
 		{"use-set-session-authorization", no_argument, &dopt.use_setsessauth, 1},
 		{"no-comments", no_argument, &dopt.no_comments, 1},
+		{"no-data", no_argument, &dopt.no_data, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
 		{"no-statistics", no_argument, &dopt.no_statistics, 1},
@@ -796,6 +797,9 @@ main(int argc, char **argv)
 	if (data_only && statistics_only)
 		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
 
+	if (data_only && dopt.no_data)
+		pg_fatal("options -a/--data-only and --no-data cannot be used together");
+
 	if (statistics_only && dopt.no_statistics)
 		pg_fatal("options -X/--statistics-only and --no-statistics cannot be used together");
 
@@ -812,7 +816,7 @@ main(int argc, char **argv)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
 	/* set derivative flags */
-	dopt.dumpData = data_only || (!schema_only && !statistics_only);
+	dopt.dumpData = data_only || (!schema_only && !statistics_only && !dopt.no_data);
 	dopt.dumpSchema = schema_only || (!data_only && !statistics_only);
 
 	if (statistics_only)
@@ -1247,6 +1251,7 @@ help(const char *progname)
 	printf(_("  --inserts                    dump data as INSERT commands, rather than COPY\n"));
 	printf(_("  --load-via-partition-root    load partitions via the root table\n"));
 	printf(_("  --no-comments                do not dump comment commands\n"));
+	printf(_("  --no-data                    do not dump data\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
 	printf(_("  --no-statistics              do not dump statistics\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 355f0439da..31c3cd32de 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -71,6 +71,7 @@ main(int argc, char **argv)
 	static int	outputNoTableAm = 0;
 	static int	outputNoTablespaces = 0;
 	static int	use_setsessauth = 0;
+	static int	no_data = 0;
 	static int	no_comments = 0;
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
@@ -132,6 +133,7 @@ main(int argc, char **argv)
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
 		{"no-statistics", no_argument, &no_statistics, 1},
+		{"no-data", no_argument, &no_data, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -375,7 +377,7 @@ main(int argc, char **argv)
 
 	/* set derivative flags */
 	opts->dumpSchema = (!data_only && !statistics_only);
-	opts->dumpData = (!schema_only && !statistics_only);
+	opts->dumpData = (!no_data && !schema_only && !statistics_only);
 	opts->dumpStatistics = (!no_statistics && !data_only && !schema_only);
 
 	opts->disable_triggers = disable_triggers;
@@ -389,6 +391,7 @@ main(int argc, char **argv)
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
 	opts->no_statistics = no_statistics;
+	opts->no_data = no_data;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
-- 
2.47.1

v34-0003-Change-pg_upgrade-s-invocation-of-pg_dump-to-use.patchtext/x-patch; charset=US-ASCII; name=v34-0003-Change-pg_upgrade-s-invocation-of-pg_dump-to-use.patchDownload
From 0a648ab51f0cefa8bc74897a24d3310918133d7a Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 11 Dec 2024 14:28:54 -0500
Subject: [PATCH v34 03/11] Change pg_upgrade's invocation of pg_dump to use
 --no-data

---
 src/bin/pg_dump/pg_dump.c | 13 +------------
 src/bin/pg_upgrade/dump.c |  2 +-
 2 files changed, 2 insertions(+), 13 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index c8a0b4afdf..e626243f3a 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -818,18 +818,7 @@ main(int argc, char **argv)
 	/* set derivative flags */
 	dopt.dumpData = data_only || (!schema_only && !statistics_only && !dopt.no_data);
 	dopt.dumpSchema = schema_only || (!data_only && !statistics_only);
-
-	if (statistics_only)
-		/* stats are only thing wanted */
-		dopt.dumpStatistics = true;
-	else if (dopt.no_statistics)
-		/* stats specifically excluded */
-		dopt.dumpStatistics = false;
-	else if (dopt.binary_upgrade)
-		/* binary upgrade and not specifically excluded */
-		dopt.dumpStatistics = true;
-	else
-		dopt.dumpStatistics = (!data_only && !schema_only);
+	dopt.dumpStatistics = statistics_only || (!data_only && !schema_only && !dopt.no_statistics);
 
 	/*
 	 * --inserts are already implied above if --column-inserts or
diff --git a/src/bin/pg_upgrade/dump.c b/src/bin/pg_upgrade/dump.c
index 8345f55be8..ef2f14a79b 100644
--- a/src/bin/pg_upgrade/dump.c
+++ b/src/bin/pg_upgrade/dump.c
@@ -52,7 +52,7 @@ generate_old_dump(void)
 		snprintf(log_file_name, sizeof(log_file_name), DB_DUMP_LOG_FILE_MASK, old_db->db_oid);
 
 		parallel_exec_prog(log_file_name, NULL,
-						   "\"%s/pg_dump\" %s --schema-only --quote-all-identifiers "
+						   "\"%s/pg_dump\" %s --no-data --quote-all-identifiers "
 						   "--binary-upgrade --format=custom %s --no-sync --file=\"%s/%s\" %s",
 						   new_cluster.bindir, cluster_conn_opts(&old_cluster),
 						   log_opts.verbose ? "--verbose" : "",
-- 
2.47.1

v34-0005-Consolidate-attribute-syscache-lookups-into-one-.patchtext/x-patch; charset=US-ASCII; name=v34-0005-Consolidate-attribute-syscache-lookups-into-one-.patchDownload
From b0f1797617b8c33ecad93c83089b1cee27b1072f Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 14 Nov 2024 03:53:33 -0500
Subject: [PATCH v34 05/11] Consolidate attribute syscache lookups into one
 call by name.

Previously we were doing one lookup by attname and one lookup by attnum,
which seems wasteful.
---
 src/backend/statistics/attribute_stats.c | 55 +++++++++++-------------
 1 file changed, 24 insertions(+), 31 deletions(-)

diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index b97ba7b0c0..6393783f8e 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -78,8 +78,8 @@ static struct StatsArgInfo attarginfo[] =
 
 static bool attribute_statistics_update(FunctionCallInfo fcinfo, int elevel);
 static Node *get_attr_expr(Relation rel, int attnum);
-static void get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
-							   Oid *atttypid, int32 *atttypmod,
+static void get_attr_stat_type(Oid reloid, Name attname, int elevel,
+							   AttrNumber *attnum, Oid *atttypid, int32 *atttypmod,
 							   char *atttyptype, Oid *atttypcoll,
 							   Oid *eq_opr, Oid *lt_opr);
 static bool get_elem_stat_type(Oid atttypid, char atttyptype, int elevel,
@@ -166,23 +166,16 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 
 	stats_check_required_arg(fcinfo, attarginfo, ATTNAME_ARG);
 	attname = PG_GETARG_NAME(ATTNAME_ARG);
-	attnum = get_attnum(reloid, NameStr(*attname));
-
-	if (attnum < 0)
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("cannot modify statistics on system column \"%s\"",
-						NameStr(*attname))));
-
-	if (attnum == InvalidAttrNumber)
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_COLUMN),
-				 errmsg("column \"%s\" of relation \"%s\" does not exist",
-						NameStr(*attname), get_rel_name(reloid))));
 
 	stats_check_required_arg(fcinfo, attarginfo, INHERITED_ARG);
 	inherited = PG_GETARG_BOOL(INHERITED_ARG);
 
+	/* derive type information from attribute */
+	get_attr_stat_type(reloid, attname, elevel,
+					   &attnum, &atttypid, &atttypmod,
+					   &atttyptype, &atttypcoll,
+					   &eq_opr, &lt_opr);
+
 	/*
 	 * Check argument sanity. If some arguments are unusable, emit at elevel
 	 * and set the corresponding argument to NULL in fcinfo.
@@ -232,12 +225,6 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 		result = false;
 	}
 
-	/* derive information from attribute */
-	get_attr_stat_type(reloid, attnum, elevel,
-					   &atttypid, &atttypmod,
-					   &atttyptype, &atttypcoll,
-					   &eq_opr, &lt_opr);
-
 	/* if needed, derive element type */
 	if (do_mcelem || do_dechist)
 	{
@@ -503,8 +490,8 @@ get_attr_expr(Relation rel, int attnum)
  * Derive type information from the attribute.
  */
 static void
-get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
-				   Oid *atttypid, int32 *atttypmod,
+get_attr_stat_type(Oid reloid, Name attname, int elevel,
+				   AttrNumber *attnum, Oid *atttypid, int32 *atttypmod,
 				   char *atttyptype, Oid *atttypcoll,
 				   Oid *eq_opr, Oid *lt_opr)
 {
@@ -514,24 +501,30 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
 	Node	   *expr;
 	TypeCacheEntry *typcache;
 
-	atup = SearchSysCache2(ATTNUM, ObjectIdGetDatum(reloid),
-						   Int16GetDatum(attnum));
+	atup = SearchSysCacheAttName(reloid, NameStr(*attname));
 
-	/* Attribute not found */
+	/* Attribute not found or is dropped */
 	if (!HeapTupleIsValid(atup))
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
-				 errmsg("attribute %d of relation \"%s\" does not exist",
-						attnum, RelationGetRelationName(rel))));
+				 errmsg("column \"%s\" of relation \"%s\" does not exist",
+						NameStr(*attname), get_rel_name(reloid))));
 
 	attr = (Form_pg_attribute) GETSTRUCT(atup);
 
-	if (attr->attisdropped)
+	if (attr->attnum < 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("cannot modify statistics on system column \"%s\"",
+						NameStr(*attname))));
+
+	if (attr->attnum == InvalidAttrNumber)
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
-				 errmsg("attribute %d of relation \"%s\" does not exist",
-						attnum, RelationGetRelationName(rel))));
+				 errmsg("column \"%s\" of relation \"%s\" does not exist",
+						NameStr(*attname), get_rel_name(reloid))));
 
+	*attnum = attr->attnum;
 	expr = get_attr_expr(rel, attr->attnum);
 
 	/*
-- 
2.47.1

v34-0004-Add-statistics-flags-to-pg_upgrade.patchtext/x-patch; charset=US-ASCII; name=v34-0004-Add-statistics-flags-to-pg_upgrade.patchDownload
From e9dd923a80b2486c24224acc654c47da926d6fe6 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 11 Dec 2024 22:38:00 -0500
Subject: [PATCH v34 04/11] Add statistics flags to pg_upgrade

---
 src/bin/pg_upgrade/dump.c       |  6 ++++--
 src/bin/pg_upgrade/option.c     | 12 ++++++++++++
 src/bin/pg_upgrade/pg_upgrade.h |  1 +
 3 files changed, 17 insertions(+), 2 deletions(-)

diff --git a/src/bin/pg_upgrade/dump.c b/src/bin/pg_upgrade/dump.c
index ef2f14a79b..954e7bacf4 100644
--- a/src/bin/pg_upgrade/dump.c
+++ b/src/bin/pg_upgrade/dump.c
@@ -21,10 +21,11 @@ generate_old_dump(void)
 
 	/* run new pg_dumpall binary for globals */
 	exec_prog(UTILITY_LOG_FILE, NULL, true, true,
-			  "\"%s/pg_dumpall\" %s --globals-only --quote-all-identifiers "
+			  "\"%s/pg_dumpall\" %s %s --globals-only --quote-all-identifiers "
 			  "--binary-upgrade %s --no-sync -f \"%s/%s\"",
 			  new_cluster.bindir, cluster_conn_opts(&old_cluster),
 			  log_opts.verbose ? "--verbose" : "",
+			  user_opts.do_statistics ? "" : "--no-statistics",
 			  log_opts.dumpdir,
 			  GLOBALS_DUMP_FILE);
 	check_ok();
@@ -52,10 +53,11 @@ generate_old_dump(void)
 		snprintf(log_file_name, sizeof(log_file_name), DB_DUMP_LOG_FILE_MASK, old_db->db_oid);
 
 		parallel_exec_prog(log_file_name, NULL,
-						   "\"%s/pg_dump\" %s --no-data --quote-all-identifiers "
+						   "\"%s/pg_dump\" %s --no-data %s --quote-all-identifiers "
 						   "--binary-upgrade --format=custom %s --no-sync --file=\"%s/%s\" %s",
 						   new_cluster.bindir, cluster_conn_opts(&old_cluster),
 						   log_opts.verbose ? "--verbose" : "",
+						   user_opts.do_statistics ? "" : "--no-statistics",
 						   log_opts.dumpdir,
 						   sql_file_name, escaped_connstr.data);
 
diff --git a/src/bin/pg_upgrade/option.c b/src/bin/pg_upgrade/option.c
index 6f41d63eed..76a0bb2991 100644
--- a/src/bin/pg_upgrade/option.c
+++ b/src/bin/pg_upgrade/option.c
@@ -60,6 +60,8 @@ parseCommandLine(int argc, char *argv[])
 		{"copy", no_argument, NULL, 2},
 		{"copy-file-range", no_argument, NULL, 3},
 		{"sync-method", required_argument, NULL, 4},
+		{"with-statistics", no_argument, NULL, 5},
+		{"no-statistics", no_argument, NULL, 6},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -70,6 +72,7 @@ parseCommandLine(int argc, char *argv[])
 
 	user_opts.do_sync = true;
 	user_opts.transfer_mode = TRANSFER_MODE_COPY;
+	user_opts.do_statistics = true;
 
 	os_info.progname = get_progname(argv[0]);
 
@@ -212,6 +215,13 @@ parseCommandLine(int argc, char *argv[])
 				user_opts.sync_method = pg_strdup(optarg);
 				break;
 
+			case 5:
+				user_opts.do_statistics = true;
+				break;
+			case 6:
+				user_opts.do_statistics = false;
+				break;
+
 			default:
 				fprintf(stderr, _("Try \"%s --help\" for more information.\n"),
 						os_info.progname);
@@ -307,6 +317,8 @@ usage(void)
 	printf(_("  --copy                        copy files to new cluster (default)\n"));
 	printf(_("  --copy-file-range             copy files to new cluster with copy_file_range\n"));
 	printf(_("  --sync-method=METHOD          set method for syncing files to disk\n"));
+	printf(_("  --with-statisttics            import statistics from old cluster (default)\n"));
+	printf(_("  --no-statisttics              do not import statistics from old cluster\n"));
 	printf(_("  -?, --help                    show this help, then exit\n"));
 	printf(_("\n"
 			 "Before running pg_upgrade you must:\n"
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index 53f693c2d4..371f9a2d06 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -327,6 +327,7 @@ typedef struct
 	int			jobs;			/* number of processes/threads to use */
 	char	   *socketdir;		/* directory to use for Unix sockets */
 	char	   *sync_method;
+	bool		do_statistics;	/* carry over statistics from old cluster */
 } UserOpts;
 
 typedef struct
-- 
2.47.1

v34-0001-Enable-dumping-of-table-index-stats-in-pg_dump.patchtext/x-patch; charset=US-ASCII; name=v34-0001-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From d85115b777fe64c0f81a08d2b3abedc8bf9b3f65 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH v34 01/11] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will generate a statement that
calls pg_set_relation_stats(), and it will generate a series of
statements that call pg_set_attribute_stats(), one per attribute. These
statements will restore the statistics of the current system onto the
destination system.

Adds the command-line options -X / --statistics-only, which are mutually
exclusive to --schema-only and --data-only.

Statistics are not dumped when --schema-only is specified, except during
a binary upgrade.

As is the pattern with pg_dump options, staistics can be disabled using
--no-statistics.

Table statistics are dumped in the data section. This is true even if
dumping stats in a binary upgrade. Index and Materialized View
statistics are dumped in the post-data section.
---
 src/bin/pg_dump/pg_backup.h          |   4 +
 src/bin/pg_dump/pg_backup_archiver.c |   5 +
 src/bin/pg_dump/pg_dump.c            | 375 ++++++++++++++++++++++++++-
 src/bin/pg_dump/pg_dump.h            |   8 +
 src/bin/pg_dump/pg_dump_sort.c       |   5 +
 src/bin/pg_dump/pg_dumpall.c         |   5 +
 src/bin/pg_dump/pg_restore.c         |  18 +-
 src/bin/pg_dump/t/001_basic.pl       |  18 ++
 doc/src/sgml/ref/pg_dump.sgml        |  36 ++-
 doc/src/sgml/ref/pg_restore.sgml     |  31 ++-
 10 files changed, 489 insertions(+), 16 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index f0f19bb0b2..8fbb39d399 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -113,6 +113,7 @@ typedef struct _restoreOptions
 	int			no_publications;	/* Skip publication entries */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
@@ -160,6 +161,7 @@ typedef struct _restoreOptions
 	/* flags derived from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } RestoreOptions;
 
 typedef struct _dumpOptions
@@ -182,6 +184,7 @@ typedef struct _dumpOptions
 	int			no_security_labels;
 	int			no_publications;
 	int			no_subscriptions;
+	int			no_statistics;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
@@ -208,6 +211,7 @@ typedef struct _dumpOptions
 	/* flags derived from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } DumpOptions;
 
 /*
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 707a3fc844..185d7fbb7e 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2962,6 +2962,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's a stats dump, maybe ignore it */
+	if (ropt->no_statistics && strcmp(te->desc, "STATISTICS") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2991,6 +2995,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 89276524ae..8f85b04c33 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -430,6 +430,7 @@ main(int argc, char **argv)
 	DataDirSyncMethod sync_method = DATA_DIR_SYNC_METHOD_FSYNC;
 	bool		data_only = false;
 	bool		schema_only = false;
+	bool		statistics_only = false;
 
 	static DumpOptions dopt;
 
@@ -466,6 +467,7 @@ main(int argc, char **argv)
 		{"encoding", required_argument, NULL, 'E'},
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
+		{"statistics-only", no_argument, NULL, 'X'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -494,6 +496,7 @@ main(int argc, char **argv)
 		{"no-comments", no_argument, &dopt.no_comments, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -539,7 +542,7 @@ main(int argc, char **argv)
 
 	InitDumpOptions(&dopt);
 
-	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:RsS:t:T:U:vwWxZ:",
+	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:PRsS:t:T:U:vwWxXZ:",
 							long_options, &optindex)) != -1)
 	{
 		switch (c)
@@ -613,6 +616,10 @@ main(int argc, char **argv)
 				dopt.cparams.pgport = pg_strdup(optarg);
 				break;
 
+			case 'X':			/* Dump statistics only */
+				statistics_only = true;
+				break;
+
 			case 'R':
 				/* no-op, still accepted for backwards compatibility */
 				break;
@@ -784,6 +791,13 @@ main(int argc, char **argv)
 
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (schema_only && statistics_only)
+		pg_fatal("options -s/--schema-only and -X/--statistics-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
+
+	if (statistics_only && dopt.no_statistics)
+		pg_fatal("options -X/--statistics-only and --no-statistics cannot be used together");
 
 	if (schema_only && foreign_servers_include_patterns.head != NULL)
 		pg_fatal("options -s/--schema-only and --include-foreign-data cannot be used together");
@@ -798,8 +812,20 @@ main(int argc, char **argv)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
 	/* set derivative flags */
-	dopt.dumpSchema = (!data_only);
-	dopt.dumpData = (!schema_only);
+	dopt.dumpData = data_only || (!schema_only && !statistics_only);
+	dopt.dumpSchema = schema_only || (!data_only && !statistics_only);
+
+	if (statistics_only)
+		/* stats are only thing wanted */
+		dopt.dumpStatistics = true;
+	else if (dopt.no_statistics)
+		/* stats specifically excluded */
+		dopt.dumpStatistics = false;
+	else if (dopt.binary_upgrade)
+		/* binary upgrade and not specifically excluded */
+		dopt.dumpStatistics = true;
+	else
+		dopt.dumpStatistics = (!data_only && !schema_only);
 
 	/*
 	 * --inserts are already implied above if --column-inserts or
@@ -1099,6 +1125,7 @@ main(int argc, char **argv)
 	ropt->dropSchema = dopt.outputClean;
 	ropt->dumpData = dopt.dumpData;
 	ropt->dumpSchema = dopt.dumpSchema;
+	ropt->dumpStatistics = dopt.dumpStatistics;
 	ropt->if_exists = dopt.if_exists;
 	ropt->column_inserts = dopt.column_inserts;
 	ropt->dumpSections = dopt.dumpSections;
@@ -1177,7 +1204,7 @@ help(const char *progname)
 	printf(_("  -?, --help                   show this help, then exit\n"));
 
 	printf(_("\nOptions controlling the output content:\n"));
-	printf(_("  -a, --data-only              dump only the data, not the schema\n"));
+	printf(_("  -a, --data-only              dump only the data, not the schema or statistics\n"));
 	printf(_("  -b, --large-objects          include large objects in dump\n"));
 	printf(_("  --blobs                      (same as --large-objects, deprecated)\n"));
 	printf(_("  -B, --no-large-objects       exclude large objects in dump\n"));
@@ -1190,11 +1217,12 @@ help(const char *progname)
 	printf(_("  -N, --exclude-schema=PATTERN do NOT dump the specified schema(s)\n"));
 	printf(_("  -O, --no-owner               skip restoration of object ownership in\n"
 			 "                               plain-text format\n"));
-	printf(_("  -s, --schema-only            dump only the schema, no data\n"));
+	printf(_("  -s, --schema-only            dump only the schema, no data or statistics\n"));
 	printf(_("  -S, --superuser=NAME         superuser user name to use in plain-text format\n"));
 	printf(_("  -t, --table=PATTERN          dump only the specified table(s)\n"));
 	printf(_("  -T, --exclude-table=PATTERN  do NOT dump the specified table(s)\n"));
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
+	printf(_("  -X, --statistics-only        dump only the statistics, not schema or data\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
@@ -1221,6 +1249,7 @@ help(const char *progname)
 	printf(_("  --no-comments                do not dump comment commands\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -6777,6 +6806,42 @@ getFuncs(Archive *fout)
 	destroyPQExpBuffer(query);
 }
 
+/*
+ * getRelationStatistics
+ *    register the statistics object as a dependent of the relation.
+ *
+ */
+static RelStatsInfo *
+getRelationStatistics(Archive *fout, DumpableObject *rel, char relkind)
+{
+	if ((relkind == RELKIND_RELATION) ||
+		(relkind == RELKIND_PARTITIONED_TABLE) ||
+		(relkind == RELKIND_INDEX) ||
+		(relkind == RELKIND_PARTITIONED_INDEX) ||
+		(relkind == RELKIND_MATVIEW))
+	{
+		RelStatsInfo	   *info = pg_malloc0(sizeof(RelStatsInfo));
+		DumpableObject	   *dobj = &info->dobj;
+
+		dobj->objType = DO_REL_STATS;
+		dobj->catId.tableoid = 0;
+		dobj->catId.oid = 0;
+		AssignDumpId(dobj);
+		dobj->dependencies = (DumpId *) pg_malloc(sizeof(DumpId));
+		dobj->dependencies[0] = rel->dumpId;
+		dobj->nDeps = 1;
+		dobj->allocDeps = 1;
+		dobj->components |= DUMP_COMPONENT_STATISTICS;
+		dobj->dump = rel->dump;
+		dobj->name = pg_strdup(rel->name);
+		dobj->namespace = rel->namespace;
+		info->relkind = relkind;
+
+		return info;
+	}
+	return NULL;
+}
+
 /*
  * getTables
  *	  read all the tables (no indexes) in the system catalogs,
@@ -7154,6 +7219,7 @@ getTables(Archive *fout, int *numTables)
 
 		/* Tables have data */
 		tblinfo[i].dobj.components |= DUMP_COMPONENT_DATA;
+		tblinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
 
 		/* Mark whether table has an ACL */
 		if (!PQgetisnull(res, i, i_relacl))
@@ -7202,6 +7268,8 @@ getTables(Archive *fout, int *numTables)
 				}
 			}
 		}
+		if (tblinfo[i].interesting)
+			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relkind);
 	}
 
 	if (query->len != 0)
@@ -7646,11 +7714,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 		for (int c = 0; c < numinds; c++, j++)
 		{
 			char		contype;
+			char		indexkind;
+			RelStatsInfo   *relstats;
 
 			indxinfo[j].dobj.objType = DO_INDEX;
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -7673,7 +7744,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			{
 				NULL, NULL
 			};
+
+			if (indxinfo[j].parentidx == 0)
+				indexkind = RELKIND_INDEX;
+			else
+				indexkind = RELKIND_PARTITIONED_INDEX;
+
 			contype = *(PQgetvalue(res, j, i_contype));
+			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, indexkind);
 
 			if (contype == 'p' || contype == 'u' || contype == 'x')
 			{
@@ -7707,6 +7785,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				constrinfo->separate = true;
 
 				indxinfo[j].indexconstraint = constrinfo->dobj.dumpId;
+				if (relstats != NULL)
+					addObjectDependency(&relstats->dobj, constrinfo->dobj.dumpId);
 			}
 			else
 			{
@@ -10294,6 +10374,276 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * Tabular description of the parameters to pg_restore_relation_stats()
+ * param_name, param_type
+ */
+static const char *rel_stats_arginfo[][2] = {
+	{"relation", "regclass"},
+	{"version", "integer"},
+	{"relpages", "integer"},
+	{"reltuples", "real"},
+	{"relallvisible", "integer"},
+};
+
+/*
+ * Tabular description of the parameters to pg_restore_attribute_stats()
+ * param_name, param_type
+ */
+static const char *att_stats_arginfo[][2] = {
+	{"relation", "regclass"},
+	{"attname", "name"},
+	{"inherited", "boolean"},
+	{"version", "integer"},
+	{"null_frac", "float4"},
+	{"avg_width", "integer"},
+	{"n_distinct", "float4"},
+	{"most_common_vals", "text"},
+	{"most_common_freqs", "float4[]"},
+	{"histogram_bounds", "text"},
+	{"correlation", "float4"},
+	{"most_common_elems", "text"},
+	{"most_common_elem_freqs", "float4[]"},
+	{"elem_count_histogram", "float4[]"},
+	{"range_length_histogram", "text"},
+	{"range_empty_frac", "float4"},
+	{"range_bounds_histogram", "text"},
+};
+
+/*
+ * getRelStatsExportQuery --
+ *
+ * Generate a query that will fetch all relation (e.g. pg_class)
+ * stats for a given relation.
+ */
+static void
+getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "current_setting('server_version_num') AS version, "
+						 "c.relpages, c.reltuples, c.relallvisible "
+						 "FROM pg_class c "
+						 "JOIN pg_namespace n "
+						 "ON n.oid = c.relnamespace "
+						 "WHERE n.nspname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND c.relname = ");
+	appendStringLiteralAH(query, relname, fout);
+}
+
+/*
+ * getAttStatsExportQuery --
+ *
+ * Generate a query that will fetch all attribute (e.g. pg_statistic)
+ * stats for a given relation.
+ */
+static void
+getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "s.attname,"
+						 "s.inherited,"
+						 "current_setting('server_version_num') AS version, "
+						 "s.null_frac,"
+						 "s.avg_width,"
+						 "s.n_distinct,"
+						 "s.most_common_vals,"
+						 "s.most_common_freqs,"
+						 "s.histogram_bounds,"
+						 "s.correlation,"
+						 "s.most_common_elems,"
+						 "s.most_common_elem_freqs,"
+						 "s.elem_count_histogram,");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 "s.range_length_histogram,"
+							 "s.range_empty_frac,"
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(query,
+							 "NULL AS range_length_histogram,"
+							 "NULL AS range_empty_frac,"
+							 "NULL AS range_bounds_histogram ");
+
+	appendPQExpBufferStr(query,
+						 "FROM pg_stats s "
+						 "JOIN pg_namespace n "
+						 "ON n.nspname = s.schemaname "
+						 "JOIN pg_class c "
+						 "ON c.relname = s.tablename "
+						 "AND c.relnamespace = n.oid "
+						 "WHERE s.schemaname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND s.tablename = ");
+	appendStringLiteralAH(query, relname, fout);
+	appendPQExpBufferStr(query, " ORDER BY s.attname, s.inherited");
+}
+
+
+/*
+ * appendNamedArgument --
+ *
+ * Convenience routine for constructing parameters of the form:
+ * 'paraname', 'value'::type
+ */
+static void
+appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
+					const char *argval, const char *argtype)
+{
+	appendPQExpBufferStr(out, "\t");
+
+	appendStringLiteralAH(out, argname, fout);
+	appendPQExpBufferStr(out, ", ");
+
+	appendStringLiteralAH(out, argval, fout);
+	appendPQExpBuffer(out, "::%s", argtype);
+}
+
+/*
+ * appendRelStatsImport --
+ *
+ * Append a formatted pg_restore_relation_stats statement.
+ */
+static void
+appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	const char *sep = "";
+
+	if (PQntuples(res) == 0)
+		return;
+
+	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
+
+	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
+	{
+		const char *argname = rel_stats_arginfo[argno][0];
+		const char *argtype = rel_stats_arginfo[argno][1];
+		int			fieldno = PQfnumber(res, argname);
+
+		if (fieldno < 0)
+			pg_fatal("relation stats export query missing field '%s'",
+					 argname);
+
+		if (PQgetisnull(res, 0, fieldno))
+			continue;
+
+		appendPQExpBufferStr(out, sep);
+		appendNamedArgument(out, fout, argname, PQgetvalue(res, 0, fieldno), argtype);
+
+		sep = ",\n";
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+/*
+ * appendAttStatsImport --
+ *
+ * Append a series of formatted pg_restore_attribute_stats statements.
+ */
+static void
+appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	{
+		const char *sep = "";
+
+		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+		for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
+		{
+			const char *argname = att_stats_arginfo[argno][0];
+			const char *argtype = att_stats_arginfo[argno][1];
+			int			fieldno = PQfnumber(res, argname);
+
+			if (fieldno < 0)
+				pg_fatal("attribute stats export query missing field '%s'",
+						 argname);
+
+			if (PQgetisnull(res, rownum, fieldno))
+				continue;
+
+			appendPQExpBufferStr(out, sep);
+			appendNamedArgument(out, fout, argname, PQgetvalue(res, rownum, fieldno), argtype);
+			sep = ",\n";
+		}
+		appendPQExpBufferStr(out, "\n);\n");
+	}
+}
+
+/*
+ * get statistics dump section, which depends on the parent object type
+ *
+ * objects created in SECTION_PRE_DATA have stats in SECTION_DATA
+ * objects created in SECTION_POST_DATA have stats in SECTION_POST_DATA
+ */
+static teSection
+statisticsDumpSection(const RelStatsInfo *rsinfo)
+{
+
+	if ((rsinfo->relkind == RELKIND_MATVIEW) ||
+		(rsinfo->relkind == RELKIND_INDEX) ||
+		(rsinfo->relkind == RELKIND_PARTITIONED_INDEX))
+			return SECTION_POST_DATA;
+
+	return SECTION_DATA;
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	PGresult	   *res;
+	PQExpBuffer		query;
+	PQExpBuffer		out;
+	PQExpBuffer		tag;
+	DumpableObject *dobj = (DumpableObject *) &rsinfo->dobj;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", "STATISTICS DATA", fmtId(dobj->name));
+
+	query = createPQExpBuffer();
+	out = createPQExpBuffer();
+
+	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendRelStatsImport(out, fout, res);
+	PQclear(res);
+
+	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendAttStatsImport(out, fout, res);
+	PQclear(res);
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATISTICS DATA",
+							  .section = statisticsDumpSection(rsinfo),
+							  .createStmt = out->data,
+							  .deps = dobj->dependencies,
+							  .nDeps = dobj->nDeps));
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(out);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -10742,6 +11092,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_SUBSCRIPTION_REL:
 			dumpSubscriptionTable(fout, (const SubRelInfo *) dobj);
 			break;
+		case DO_REL_STATS:
+			dumpRelationStats(fout, (const RelStatsInfo *) dobj);
+			break;
 		case DO_PRE_DATA_BOUNDARY:
 		case DO_POST_DATA_BOUNDARY:
 			/* never dumped, nothing to do */
@@ -17172,6 +17525,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 		free(indstatvalsarray);
 	}
 
+	/* Comments and stats share same .dep */
+
 	/* Dump Index Comments */
 	if (indxinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
 		dumpComment(fout, "INDEX", qindxname,
@@ -18959,6 +19314,16 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 				/* must come after the pre-data boundary */
 				addObjectDependency(dobj, preDataBound->dumpId);
 				break;
+			case DO_REL_STATS:
+				/* stats section varies by parent object type, DATA or POST */
+				if (statisticsDumpSection((RelStatsInfo *) dobj) == SECTION_DATA)
+				{
+					addObjectDependency(dobj, preDataBound->dumpId);
+					addObjectDependency(postDataBound, dobj->dumpId);
+				}
+				else
+					addObjectDependency(dobj, postDataBound->dumpId);
+				break;
 		}
 	}
 }
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 2e55a0e3bb..def5f292e6 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -84,6 +84,7 @@ typedef enum
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION,
 	DO_SUBSCRIPTION_REL,		/* see note for SubRelInfo */
+	DO_REL_STATS,
 } DumpableObjectType;
 
 /*
@@ -101,6 +102,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
@@ -421,6 +423,12 @@ typedef struct _indexAttachInfo
 	IndxInfo   *partitionIdx;	/* link to index on partition */
 } IndexAttachInfo;
 
+typedef struct _relStatsInfo
+{
+	DumpableObject	dobj;
+	char			relkind;		/* 'r', 'v', 'c', etc */
+} RelStatsInfo;
+
 typedef struct _statsExtInfo
 {
 	DumpableObject dobj;
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 3c8f2eb808..989d20aa27 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -1500,6 +1500,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "POST-DATA BOUNDARY  (ID %d)",
 					 obj->dumpId);
 			return;
+		case DO_REL_STATS:
+			snprintf(buf, bufsize,
+					 "RELATION STATISTICS FOR %s  (ID %d OID %u)",
+					 obj->name, obj->dumpId, obj->catId.oid);
+			return;
 	}
 	/* shouldn't get here */
 	snprintf(buf, bufsize,
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 9a04e51c81..62e2766c09 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -103,6 +103,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -172,6 +173,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -451,6 +453,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -666,6 +670,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 88ae39d938..355f0439da 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -63,6 +63,7 @@ main(int argc, char **argv)
 	int			numWorkers = 1;
 	Archive    *AH;
 	char	   *inputFileSpec;
+	bool		statistics_only = false;
 	static int	disable_triggers = 0;
 	static int	enable_row_security = 0;
 	static int	if_exists = 0;
@@ -74,6 +75,7 @@ main(int argc, char **argv)
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
 	static int	no_subscriptions = 0;
+	static int  no_statistics = 0;
 	static int	strict_names = 0;
 	bool		data_only = false;
 	bool		schema_only = false;
@@ -108,6 +110,7 @@ main(int argc, char **argv)
 		{"username", 1, NULL, 'U'},
 		{"verbose", 0, NULL, 'v'},
 		{"single-transaction", 0, NULL, '1'},
+		{"statistics-only", no_argument, NULL, 'P'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -128,6 +131,7 @@ main(int argc, char **argv)
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
+		{"no-statistics", no_argument, &no_statistics, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -271,6 +275,10 @@ main(int argc, char **argv)
 				opts->aclsSkip = 1;
 				break;
 
+			case 'X':			/* Restore statistics only */
+				statistics_only = true;
+				break;
+
 			case '1':			/* Restore data in a single transaction */
 				opts->single_txn = true;
 				opts->exit_on_error = true;
@@ -343,6 +351,10 @@ main(int argc, char **argv)
 
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
+	if (schema_only && statistics_only)
+		pg_fatal("options -s/--schema-only and -X/--statistics-only cannot be used together");
 
 	if (data_only && opts->dropSchema)
 		pg_fatal("options -c/--clean and -a/--data-only cannot be used together");
@@ -362,8 +374,9 @@ main(int argc, char **argv)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
 	/* set derivative flags */
-	opts->dumpSchema = (!data_only);
-	opts->dumpData = (!schema_only);
+	opts->dumpSchema = (!data_only && !statistics_only);
+	opts->dumpData = (!schema_only && !statistics_only);
+	opts->dumpStatistics = (!no_statistics && !data_only && !schema_only);
 
 	opts->disable_triggers = disable_triggers;
 	opts->enable_row_security = enable_row_security;
@@ -375,6 +388,7 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
diff --git a/src/bin/pg_dump/t/001_basic.pl b/src/bin/pg_dump/t/001_basic.pl
index b9d13a0e1d..76ebf15f55 100644
--- a/src/bin/pg_dump/t/001_basic.pl
+++ b/src/bin/pg_dump/t/001_basic.pl
@@ -50,12 +50,30 @@ command_fails_like(
 	'pg_dump: options -s/--schema-only and -a/--data-only cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '-s', '-X' ],
+	qr/\Qpg_dump: error: options -s\/--schema-only and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: error: options -s/--schema-only and -X/--statistics-only cannot be used together'
+);
+
+command_fails_like(
+	[ 'pg_dump', '-a', '-X' ],
+	qr/\Qpg_dump: error: options -a\/--data-only and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: error: options -a/--data-only and -X/--statistics-only cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-s', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: options -s\/--schema-only and --include-foreign-data cannot be used together\E/,
 	'pg_dump: options -s/--schema-only and --include-foreign-data cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '--statistics-only', '--no-statistics' ],
+	qr/\Qpg_dump: error: options -X\/--statistics-only and --no-statistics cannot be used together\E/,
+	'pg_dump: options -X\/--statistics-only and --no-statistics cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-j2', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: option --include-foreign-data is not supported with parallel backup\E/,
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index d66e901f51..aa4785c612 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -123,7 +123,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Dump only the data, not the schema (data definitions).
+        Dump only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are dumped.
        </para>
 
@@ -141,8 +141,9 @@ PostgreSQL documentation
       <listitem>
        <para>
         Include large objects in the dump.  This is the default behavior
-        except when <option>--schema</option>, <option>--table</option>, or
-        <option>--schema-only</option> is specified.  The <option>-b</option>
+        except when <option>--schema</option>, <option>--table</option>,
+        <option>--schema-only</option>, or
+        <option>--statistics-only</option> is specified.  The <option>-b</option>
         switch is therefore only useful to add large objects to dumps
         where a specific schema or table has been requested.  Note that
         large objects are considered data and therefore will be included when
@@ -516,10 +517,11 @@ PostgreSQL documentation
       <term><option>--schema-only</option></term>
       <listitem>
        <para>
-        Dump only the object definitions (schema), not data.
+        Dump only the object definitions (schema), not data or statistics.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive to <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
         <option>--section=pre-data --section=post-data</option>.
@@ -652,6 +654,18 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Dump only the statistics, not the schema (data definitions) or data.
+        Statistics for tables, materialized views, and indexes are dumped.
+       </para>
+
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-Z <replaceable class="parameter">level</replaceable></option></term>
       <term><option>-Z <replaceable class="parameter">method</replaceable></option>[:<replaceable>detail</replaceable>]</term>
@@ -833,7 +847,8 @@ PostgreSQL documentation
         though you do not need the data in it.
        </para>
        <para>
-        To exclude data for all tables in the database, see <option>--schema-only</option>.
+        To exclude data for all tables in the database, see <option>--schema-only</option>
+        or <option>--statistics-only</option>.
        </para>
       </listitem>
      </varlistentry>
@@ -1098,6 +1113,15 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index b8b27e1719..ff1441a243 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -94,7 +94,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Restore only the data, not the schema (data definitions).
+        Restore only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are restored,
         if present in the archive.
        </para>
@@ -483,10 +483,11 @@ PostgreSQL documentation
         to the extent that schema entries are present in the archive.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive of <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
-        <option>--section=pre-data --section=post-data</option>.
+        <option>--section=pre-data --section=post-data --no-statistics</option>.
        </para>
        <para>
         (Do not confuse this with the <option>--schema</option> option, which
@@ -599,6 +600,20 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Restore only the statistics, not schema or data.
+       </para>
+       <para>
+        (Do not confuse this with the <option>--schema</option> option, which
+        uses the word <quote>schema</quote> in a different meaning.)
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-1</option></term>
       <term><option>--single-transaction</option></term>
@@ -723,6 +738,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore statistics, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
-- 
2.47.1

v34-0006-split-out-check_conn_options.patchtext/x-patch; charset=US-ASCII; name=v34-0006-split-out-check_conn_options.patchDownload
From 1108589023ac2789a5b9dd44fb35663e4af7e392 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 6 Nov 2024 15:20:07 -0500
Subject: [PATCH v34 06/11] split out check_conn_options

---
 src/bin/scripts/vacuumdb.c | 103 ++++++++++++++++++++-----------------
 1 file changed, 57 insertions(+), 46 deletions(-)

diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index d07ab7d67e..7b97a9428a 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -10,6 +10,7 @@
  *-------------------------------------------------------------------------
  */
 
+#include "libpq-fe.h"
 #include "postgres_fe.h"
 
 #include <limits.h>
@@ -459,55 +460,11 @@ escape_quotes(const char *src)
 }
 
 /*
- * vacuum_one_database
- *
- * Process tables in the given database.  If the 'tables' list is empty,
- * process all tables in the database.
- *
- * Note that this function is only concerned with running exactly one stage
- * when in analyze-in-stages mode; caller must iterate on us if necessary.
- *
- * If concurrentCons is > 1, multiple connections are used to vacuum tables
- * in parallel.  In this case and if the table list is empty, we first obtain
- * a list of tables from the database.
+ * Check connection options for compatibility with the connected database.
  */
 static void
-vacuum_one_database(ConnParams *cparams,
-					vacuumingOptions *vacopts,
-					int stage,
-					SimpleStringList *objects,
-					int concurrentCons,
-					const char *progname, bool echo, bool quiet)
+check_conn_options(PGconn *conn, vacuumingOptions *vacopts)
 {
-	PQExpBufferData sql;
-	PQExpBufferData buf;
-	PQExpBufferData catalog_query;
-	PGresult   *res;
-	PGconn	   *conn;
-	SimpleStringListCell *cell;
-	ParallelSlotArray *sa;
-	SimpleStringList dbtables = {NULL, NULL};
-	int			i;
-	int			ntups;
-	bool		failed = false;
-	bool		objects_listed = false;
-	const char *initcmd;
-	const char *stage_commands[] = {
-		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
-		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
-		"RESET default_statistics_target;"
-	};
-	const char *stage_messages[] = {
-		gettext_noop("Generating minimal optimizer statistics (1 target)"),
-		gettext_noop("Generating medium optimizer statistics (10 targets)"),
-		gettext_noop("Generating default (full) optimizer statistics")
-	};
-
-	Assert(stage == ANALYZE_NO_STAGE ||
-		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
-
-	conn = connectDatabase(cparams, progname, echo, false, true);
-
 	if (vacopts->disable_page_skipping && PQserverVersion(conn) < 90600)
 	{
 		PQfinish(conn);
@@ -575,6 +532,60 @@ vacuum_one_database(ConnParams *cparams,
 
 	/* skip_database_stats is used automatically if server supports it */
 	vacopts->skip_database_stats = (PQserverVersion(conn) >= 160000);
+}
+
+
+/*
+ * vacuum_one_database
+ *
+ * Process tables in the given database.  If the 'tables' list is empty,
+ * process all tables in the database.
+ *
+ * Note that this function is only concerned with running exactly one stage
+ * when in analyze-in-stages mode; caller must iterate on us if necessary.
+ *
+ * If concurrentCons is > 1, multiple connections are used to vacuum tables
+ * in parallel.  In this case and if the table list is empty, we first obtain
+ * a list of tables from the database.
+ */
+static void
+vacuum_one_database(ConnParams *cparams,
+					vacuumingOptions *vacopts,
+					int stage,
+					SimpleStringList *objects,
+					int concurrentCons,
+					const char *progname, bool echo, bool quiet)
+{
+	PQExpBufferData sql;
+	PQExpBufferData buf;
+	PQExpBufferData catalog_query;
+	PGresult   *res;
+	PGconn	   *conn;
+	SimpleStringListCell *cell;
+	ParallelSlotArray *sa;
+	SimpleStringList dbtables = {NULL, NULL};
+	int			i;
+	int			ntups;
+	bool		failed = false;
+	bool		objects_listed = false;
+	const char *initcmd;
+	const char *stage_commands[] = {
+		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
+		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
+		"RESET default_statistics_target;"
+	};
+	const char *stage_messages[] = {
+		gettext_noop("Generating minimal optimizer statistics (1 target)"),
+		gettext_noop("Generating medium optimizer statistics (10 targets)"),
+		gettext_noop("Generating default (full) optimizer statistics")
+	};
+
+	Assert(stage == ANALYZE_NO_STAGE ||
+		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
+
+	conn = connectDatabase(cparams, progname, echo, false, true);
+
+	check_conn_options(conn, vacopts);
 
 	if (!quiet)
 	{
-- 
2.47.1

v34-0008-split-out-generate_catalog_list.patchtext/x-patch; charset=US-ASCII; name=v34-0008-split-out-generate_catalog_list.patchDownload
From 29f721142c9b7bb10f071db1461373aaec5586ed Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 6 Nov 2024 16:15:16 -0500
Subject: [PATCH v34 08/11] split out generate_catalog_list

---
 src/bin/scripts/vacuumdb.c | 176 +++++++++++++++++++++----------------
 1 file changed, 99 insertions(+), 77 deletions(-)

diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index e9946f79b2..36f4796db0 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -560,67 +560,38 @@ print_processing_notice(PGconn *conn, int stage, const char *progname, bool quie
 }
 
 /*
- * vacuum_one_database
- *
- * Process tables in the given database.  If the 'tables' list is empty,
- * process all tables in the database.
- *
- * Note that this function is only concerned with running exactly one stage
- * when in analyze-in-stages mode; caller must iterate on us if necessary.
- *
- * If concurrentCons is > 1, multiple connections are used to vacuum tables
- * in parallel.  In this case and if the table list is empty, we first obtain
- * a list of tables from the database.
- */
-static void
-vacuum_one_database(ConnParams *cparams,
-					vacuumingOptions *vacopts,
-					int stage,
-					SimpleStringList *objects,
-					int concurrentCons,
-					const char *progname, bool echo, bool quiet)
+	* Prepare the list of tables to process by querying the catalogs.
+	*
+	* Since we execute the constructed query with the default search_path
+	* (which could be unsafe), everything in this query MUST be fully
+	* qualified.
+	*
+	* First, build a WITH clause for the catalog query if any tables were
+	* specified, with a set of values made of relation names and their
+	* optional set of columns.  This is used to match any provided column
+	* lists with the generated qualified identifiers and to filter for the
+	* tables provided via --table.  If a listed table does not exist, the
+	* catalog query will fail.
+	*/
+static SimpleStringList *
+generate_catalog_list(PGconn *conn,
+					  vacuumingOptions *vacopts,
+					  SimpleStringList *objects,
+					  bool echo,
+					  int *ntups)
 {
-	PQExpBufferData sql;
-	PQExpBufferData buf;
 	PQExpBufferData catalog_query;
-	PGresult   *res;
-	PGconn	   *conn;
+	PQExpBufferData buf;
+	SimpleStringList *dbtables;
 	SimpleStringListCell *cell;
-	ParallelSlotArray *sa;
-	SimpleStringList dbtables = {NULL, NULL};
-	int			i;
-	int			ntups;
-	bool		failed = false;
 	bool		objects_listed = false;
-	const char *initcmd;
-	const char *stage_commands[] = {
-		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
-		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
-		"RESET default_statistics_target;"
-	};
+	PGresult   *res;
+	int			i;
 
-	Assert(stage == ANALYZE_NO_STAGE ||
-		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
+	dbtables = palloc(sizeof(SimpleStringList));
+	dbtables->head = NULL;
+	dbtables->tail = NULL;
 
-	conn = connectDatabase(cparams, progname, echo, false, true);
-
-	check_conn_options(conn, vacopts);
-	print_processing_notice(conn, stage, progname, quiet);
-
-	/*
-	 * Prepare the list of tables to process by querying the catalogs.
-	 *
-	 * Since we execute the constructed query with the default search_path
-	 * (which could be unsafe), everything in this query MUST be fully
-	 * qualified.
-	 *
-	 * First, build a WITH clause for the catalog query if any tables were
-	 * specified, with a set of values made of relation names and their
-	 * optional set of columns.  This is used to match any provided column
-	 * lists with the generated qualified identifiers and to filter for the
-	 * tables provided via --table.  If a listed table does not exist, the
-	 * catalog query will fail.
-	 */
 	initPQExpBuffer(&catalog_query);
 	for (cell = objects ? objects->head : NULL; cell; cell = cell->next)
 	{
@@ -771,40 +742,91 @@ vacuum_one_database(ConnParams *cparams,
 	appendPQExpBufferStr(&catalog_query, " ORDER BY c.relpages DESC;");
 	executeCommand(conn, "RESET search_path;", echo);
 	res = executeQuery(conn, catalog_query.data, echo);
+	*ntups = PQntuples(res);
 	termPQExpBuffer(&catalog_query);
 	PQclear(executeQuery(conn, ALWAYS_SECURE_SEARCH_PATH_SQL, echo));
 
-	/*
-	 * If no rows are returned, there are no matching tables, so we are done.
-	 */
-	ntups = PQntuples(res);
-	if (ntups == 0)
-	{
-		PQclear(res);
-		PQfinish(conn);
-		return;
-	}
-
 	/*
 	 * Build qualified identifiers for each table, including the column list
 	 * if given.
 	 */
-	initPQExpBuffer(&buf);
-	for (i = 0; i < ntups; i++)
+	if (*ntups > 0)
 	{
-		appendPQExpBufferStr(&buf,
-							 fmtQualifiedId(PQgetvalue(res, i, 1),
-											PQgetvalue(res, i, 0)));
+		initPQExpBuffer(&buf);
+		for (i = 0; i < *ntups; i++)
+		{
+			appendPQExpBufferStr(&buf,
+								fmtQualifiedId(PQgetvalue(res, i, 1),
+												PQgetvalue(res, i, 0)));
 
-		if (objects_listed && !PQgetisnull(res, i, 2))
-			appendPQExpBufferStr(&buf, PQgetvalue(res, i, 2));
+			if (objects_listed && !PQgetisnull(res, i, 2))
+				appendPQExpBufferStr(&buf, PQgetvalue(res, i, 2));
 
-		simple_string_list_append(&dbtables, buf.data);
-		resetPQExpBuffer(&buf);
+			simple_string_list_append(dbtables, buf.data);
+			resetPQExpBuffer(&buf);
+		}
+		termPQExpBuffer(&buf);
 	}
-	termPQExpBuffer(&buf);
 	PQclear(res);
 
+	return dbtables;
+}
+
+/*
+ * vacuum_one_database
+ *
+ * Process tables in the given database.  If the 'tables' list is empty,
+ * process all tables in the database.
+ *
+ * Note that this function is only concerned with running exactly one stage
+ * when in analyze-in-stages mode; caller must iterate on us if necessary.
+ *
+ * If concurrentCons is > 1, multiple connections are used to vacuum tables
+ * in parallel.  In this case and if the table list is empty, we first obtain
+ * a list of tables from the database.
+ */
+static void
+vacuum_one_database(ConnParams *cparams,
+					vacuumingOptions *vacopts,
+					int stage,
+					SimpleStringList *objects,
+					int concurrentCons,
+					const char *progname, bool echo, bool quiet)
+{
+	PQExpBufferData sql;
+	PGconn	   *conn;
+	SimpleStringListCell *cell;
+	ParallelSlotArray *sa;
+	int			ntups;
+	bool		failed = false;
+	const char *initcmd;
+	SimpleStringList *dbtables;
+	const char *stage_commands[] = {
+		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
+		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
+		"RESET default_statistics_target;"
+	};
+
+	Assert(stage == ANALYZE_NO_STAGE ||
+		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
+
+	conn = connectDatabase(cparams, progname, echo, false, true);
+
+	check_conn_options(conn, vacopts);
+	print_processing_notice(conn, stage, progname, quiet);
+
+	dbtables = generate_catalog_list(conn, vacopts, objects, echo, &ntups);
+
+	/*
+	 * If no rows are returned, there are no matching tables, so we are done.
+	 */
+	if (ntups == 0)
+	{
+		PQfinish(conn);
+		return;
+	}
+
+
 	/*
 	 * Ensure concurrentCons is sane.  If there are more connections than
 	 * vacuumable relations, we don't need to use them all.
@@ -837,7 +859,7 @@ vacuum_one_database(ConnParams *cparams,
 
 	initPQExpBuffer(&sql);
 
-	cell = dbtables.head;
+	cell = dbtables->head;
 	do
 	{
 		const char *tabname = cell->val;
-- 
2.47.1

v34-0007-split-out-print_processing_notice.patchtext/x-patch; charset=US-ASCII; name=v34-0007-split-out-print_processing_notice.patchDownload
From 0de5fd8cd0ebc8d6f0144ff4d4e7922b7a4cfe09 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 6 Nov 2024 15:46:51 -0500
Subject: [PATCH v34 07/11] split out print_processing_notice

---
 src/bin/scripts/vacuumdb.c | 41 +++++++++++++++++++++++---------------
 1 file changed, 25 insertions(+), 16 deletions(-)

diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index 7b97a9428a..e9946f79b2 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -535,6 +535,30 @@ check_conn_options(PGconn *conn, vacuumingOptions *vacopts)
 }
 
 
+/*
+ * print the processing notice for a database.
+ */
+static void
+print_processing_notice(PGconn *conn, int stage, const char *progname, bool quiet)
+{
+	const char *stage_messages[] = {
+		gettext_noop("Generating minimal optimizer statistics (1 target)"),
+		gettext_noop("Generating medium optimizer statistics (10 targets)"),
+		gettext_noop("Generating default (full) optimizer statistics")
+	};
+
+	if (!quiet)
+	{
+		if (stage != ANALYZE_NO_STAGE)
+			printf(_("%s: processing database \"%s\": %s\n"),
+				   progname, PQdb(conn), _(stage_messages[stage]));
+		else
+			printf(_("%s: vacuuming database \"%s\"\n"),
+				   progname, PQdb(conn));
+		fflush(stdout);
+	}
+}
+
 /*
  * vacuum_one_database
  *
@@ -574,11 +598,6 @@ vacuum_one_database(ConnParams *cparams,
 		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
 		"RESET default_statistics_target;"
 	};
-	const char *stage_messages[] = {
-		gettext_noop("Generating minimal optimizer statistics (1 target)"),
-		gettext_noop("Generating medium optimizer statistics (10 targets)"),
-		gettext_noop("Generating default (full) optimizer statistics")
-	};
 
 	Assert(stage == ANALYZE_NO_STAGE ||
 		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
@@ -586,17 +605,7 @@ vacuum_one_database(ConnParams *cparams,
 	conn = connectDatabase(cparams, progname, echo, false, true);
 
 	check_conn_options(conn, vacopts);
-
-	if (!quiet)
-	{
-		if (stage != ANALYZE_NO_STAGE)
-			printf(_("%s: processing database \"%s\": %s\n"),
-				   progname, PQdb(conn), _(stage_messages[stage]));
-		else
-			printf(_("%s: vacuuming database \"%s\"\n"),
-				   progname, PQdb(conn));
-		fflush(stdout);
-	}
+	print_processing_notice(conn, stage, progname, quiet);
 
 	/*
 	 * Prepare the list of tables to process by querying the catalogs.
-- 
2.47.1

v34-0010-Add-issues_sql_unlike-opposite-of-issues_sql_lik.patchtext/x-patch; charset=US-ASCII; name=v34-0010-Add-issues_sql_unlike-opposite-of-issues_sql_lik.patchDownload
From 378de1ba4e7f0ddb7598bf7ff785a65996be21a4 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 7 Nov 2024 18:06:52 -0500
Subject: [PATCH v34 10/11] Add issues_sql_unlike, opposite of issues_sql_like

This is the same as issues_sql_like(), but the specified text is
prohibited from being in the output rather than required.

This became necessary to test that a command-line filter was in fact
filtering out certain output that a prior test required.
---
 src/test/perl/PostgreSQL/Test/Cluster.pm | 25 ++++++++++++++++++++++++
 1 file changed, 25 insertions(+)

diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index 508e5e3917..1281071479 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -2799,6 +2799,31 @@ sub issues_sql_like
 }
 
 =pod
+=item $node->issues_sql_unlike(cmd, prohibited_sql, test_name)
+
+Run a command on the node, then verify that $prohibited_sql does not appear
+in the server log file.
+
+=cut
+
+sub issues_sql_unlike
+{
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+	my ($self, $cmd, $prohibited_sql, $test_name) = @_;
+
+	local %ENV = $self->_get_env();
+
+	my $log_location = -s $self->logfile;
+
+	my $result = PostgreSQL::Test::Utils::run_log($cmd);
+	ok($result, "@$cmd exit code 0");
+	my $log =
+	  PostgreSQL::Test::Utils::slurp_file($self->logfile, $log_location);
+	unlike($log, $prohibited_sql, "$test_name: SQL found in server log");
+	return;
+}
+
 
 =item $node->log_content()
 
-- 
2.47.1

v34-0009-preserve-catalog-lists-across-staged-runs.patchtext/x-patch; charset=US-ASCII; name=v34-0009-preserve-catalog-lists-across-staged-runs.patchDownload
From 431cc9dc617e4ba314637b48c190e22736509b36 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 6 Nov 2024 21:30:49 -0500
Subject: [PATCH v34 09/11] preserve catalog lists across staged runs

---
 src/bin/scripts/vacuumdb.c | 113 ++++++++++++++++++++++++++-----------
 1 file changed, 80 insertions(+), 33 deletions(-)

diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index 36f4796db0..b13f3c4224 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -68,6 +68,9 @@ static void vacuum_one_database(ConnParams *cparams,
 								int stage,
 								SimpleStringList *objects,
 								int concurrentCons,
+								PGconn *conn,
+								SimpleStringList *dbtables,
+								int ntups,
 								const char *progname, bool echo, bool quiet);
 
 static void vacuum_all_databases(ConnParams *cparams,
@@ -83,6 +86,14 @@ static void prepare_vacuum_command(PQExpBuffer sql, int serverVersion,
 static void run_vacuum_command(PGconn *conn, const char *sql, bool echo,
 							   const char *table);
 
+static void check_conn_options(PGconn *conn, vacuumingOptions *vacopts);
+
+static void
+print_processing_notice(PGconn *conn, int stage, const char *progname, bool quiet);
+
+static SimpleStringList * generate_catalog_list(PGconn *conn, vacuumingOptions *vacopts,
+												SimpleStringList *objects, bool echo, int *ntups);
+
 static void help(const char *progname);
 
 void		check_objfilter(void);
@@ -386,6 +397,11 @@ main(int argc, char *argv[])
 	}
 	else
 	{
+		PGconn	   *conn;
+		int			ntup;
+		SimpleStringList   *found_objects;
+		int			stage;
+
 		if (dbname == NULL)
 		{
 			if (getenv("PGDATABASE"))
@@ -397,25 +413,37 @@ main(int argc, char *argv[])
 		}
 
 		cparams.dbname = dbname;
+		stage = (analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
+
+		conn = connectDatabase(&cparams, progname, echo, false, true);
+		check_conn_options(conn, &vacopts);
+		print_processing_notice(conn, stage, progname, quiet);
+		found_objects = generate_catalog_list(conn, &vacopts, &objects, echo, &ntup);
 
 		if (analyze_in_stages)
 		{
-			int			stage;
-
 			for (stage = 0; stage < ANALYZE_NUM_STAGES; stage++)
 			{
-				vacuum_one_database(&cparams, &vacopts,
-									stage,
+				/* the last pass disconnected the conn */
+				if (stage > 0)
+					conn = connectDatabase(&cparams, progname, echo, false, true);
+
+				vacuum_one_database(&cparams, &vacopts, stage,
 									&objects,
 									concurrentCons,
+									conn,
+									found_objects,
+									ntup,
 									progname, echo, quiet);
 			}
 		}
 		else
-			vacuum_one_database(&cparams, &vacopts,
-								ANALYZE_NO_STAGE,
+			vacuum_one_database(&cparams, &vacopts, stage,
 								&objects,
 								concurrentCons,
+								conn,
+								found_objects,
+								ntup,
 								progname, echo, quiet);
 	}
 
@@ -791,16 +819,17 @@ vacuum_one_database(ConnParams *cparams,
 					int stage,
 					SimpleStringList *objects,
 					int concurrentCons,
+					PGconn *conn,
+					SimpleStringList *dbtables,
+					int ntups,
 					const char *progname, bool echo, bool quiet)
 {
 	PQExpBufferData sql;
-	PGconn	   *conn;
 	SimpleStringListCell *cell;
 	ParallelSlotArray *sa;
-	int			ntups;
 	bool		failed = false;
 	const char *initcmd;
-	SimpleStringList *dbtables;
+
 	const char *stage_commands[] = {
 		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
 		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
@@ -810,13 +839,6 @@ vacuum_one_database(ConnParams *cparams,
 	Assert(stage == ANALYZE_NO_STAGE ||
 		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
 
-	conn = connectDatabase(cparams, progname, echo, false, true);
-
-	check_conn_options(conn, vacopts);
-	print_processing_notice(conn, stage, progname, quiet);
-
-	dbtables = generate_catalog_list(conn, vacopts, objects, echo, &ntups);
-
 	/*
 	 * If no rows are returned, there are no matching tables, so we are done.
 	 */
@@ -928,7 +950,7 @@ finish:
 }
 
 /*
- * Vacuum/analyze all connectable databases.
+ * Vacuum/analyze all ccparams->override_dbname = PQgetvalue(result, i, 0);onnectable databases.
  *
  * In analyze-in-stages mode, we process all databases in one stage before
  * moving on to the next stage.  That ensure minimal stats are available
@@ -944,8 +966,13 @@ vacuum_all_databases(ConnParams *cparams,
 {
 	PGconn	   *conn;
 	PGresult   *result;
-	int			stage;
 	int			i;
+	int			stage;
+
+	SimpleStringList  **found_objects;
+	int				   *num_tuples;
+
+	stage = (analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
 
 	conn = connectMaintenanceDatabase(cparams, progname, echo);
 	result = executeQuery(conn,
@@ -953,7 +980,33 @@ vacuum_all_databases(ConnParams *cparams,
 						  echo);
 	PQfinish(conn);
 
-	if (analyze_in_stages)
+	/*
+	 * connect to each database, check validity of options,
+	 * build the list of found objects per database,
+	 * and run the first/only vacuum stage
+	 */
+	found_objects = palloc(PQntuples(result) * sizeof(SimpleStringList *));
+	num_tuples = palloc(PQntuples(result) * sizeof (int));
+
+	for (i = 0; i < PQntuples(result); i++)
+	{
+		cparams->override_dbname = PQgetvalue(result, i, 0);
+		conn = connectDatabase(cparams, progname, echo, false, true);
+		check_conn_options(conn, vacopts);
+		print_processing_notice(conn, stage, progname, quiet);
+		found_objects[i] = generate_catalog_list(conn, vacopts, objects, echo, &num_tuples[i]);
+
+		vacuum_one_database(cparams, vacopts,
+							stage,
+							objects,
+							concurrentCons,
+							conn,
+							found_objects[i],
+							num_tuples[i],
+							progname, echo, quiet);
+	}
+
+	if (stage != ANALYZE_NO_STAGE)
 	{
 		/*
 		 * When analyzing all databases in stages, we analyze them all in the
@@ -963,35 +1016,29 @@ vacuum_all_databases(ConnParams *cparams,
 		 * This means we establish several times as many connections, but
 		 * that's a secondary consideration.
 		 */
-		for (stage = 0; stage < ANALYZE_NUM_STAGES; stage++)
+		for (stage = 1; stage < ANALYZE_NUM_STAGES; stage++)
 		{
 			for (i = 0; i < PQntuples(result); i++)
 			{
 				cparams->override_dbname = PQgetvalue(result, i, 0);
+				conn = connectDatabase(cparams, progname, echo, false, true);
+				print_processing_notice(conn, stage, progname, quiet);
 
 				vacuum_one_database(cparams, vacopts,
 									stage,
 									objects,
 									concurrentCons,
+									conn,
+									found_objects[i],
+									num_tuples[i],
 									progname, echo, quiet);
 			}
 		}
 	}
-	else
-	{
-		for (i = 0; i < PQntuples(result); i++)
-		{
-			cparams->override_dbname = PQgetvalue(result, i, 0);
-
-			vacuum_one_database(cparams, vacopts,
-								ANALYZE_NO_STAGE,
-								objects,
-								concurrentCons,
-								progname, echo, quiet);
-		}
-	}
 
 	PQclear(result);
+	pfree(found_objects);
+	pfree(num_tuples);
 }
 
 /*
-- 
2.47.1

v34-0011-Add-force-analyze-to-vacuumdb.patchtext/x-patch; charset=US-ASCII; name=v34-0011-Add-force-analyze-to-vacuumdb.patchDownload
From cf4e731db9ffaa4e89d7c5d14b32668529c8c89a Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Fri, 8 Nov 2024 12:27:50 -0500
Subject: [PATCH v34 11/11] Add --force-analyze to vacuumdb.

The vacuumdb options of --analyze-in-stages and --analyze-only are often
used after a restore from a dump or a pg_upgrade to quickly rebuild
stats on a databse.

However, now that stats are imported in most (but not all) cases,
running either of these commands will be at least partially redundant,
and will overwrite the stats that were just imported, which is a big
POLA violation.

We could add a new option such as --analyze-missing-in-stages, but that
wouldn't help the userbase that grown accustomed to running
--analyze-in-stages after an upgrade.

The least-bad option to handle the situation is to change the behavior
of --analyze-only and --analyze-in-stages to only analyze tables which
were missing stats before the vacuumdb started, but offer the
--force-analyze flag to restore the old behavior for those who truly
wanted it.
---
 src/bin/scripts/t/100_vacuumdb.pl |  6 +-
 src/bin/scripts/vacuumdb.c        | 91 ++++++++++++++++++++++++-------
 doc/src/sgml/ref/vacuumdb.sgml    | 48 ++++++++++++++++
 3 files changed, 125 insertions(+), 20 deletions(-)

diff --git a/src/bin/scripts/t/100_vacuumdb.pl b/src/bin/scripts/t/100_vacuumdb.pl
index 1a2bcb4959..2d669391fe 100644
--- a/src/bin/scripts/t/100_vacuumdb.pl
+++ b/src/bin/scripts/t/100_vacuumdb.pl
@@ -127,9 +127,13 @@ $node->issues_sql_like(
 	qr/statement: VACUUM \(SKIP_DATABASE_STATS, ANALYZE\) public.vactable\(a, b\);/,
 	'vacuumdb --analyze with complete column list');
 $node->issues_sql_like(
+	[ 'vacuumdb', '--analyze-only', '--force-analyze', '--table', 'vactable(b)', 'postgres' ],
+	qr/statement: ANALYZE public.vactable\(b\);/,
+	'vacuumdb --analyze-only --force-analyze with partial column list');
+$node->issues_sql_unlike(
 	[ 'vacuumdb', '--analyze-only', '--table', 'vactable(b)', 'postgres' ],
 	qr/statement: ANALYZE public.vactable\(b\);/,
-	'vacuumdb --analyze-only with partial column list');
+	'vacuumdb --analyze-only --force-analyze with partial column list skipping vacuumed tables');
 $node->command_checks_all(
 	[ 'vacuumdb', '--analyze', '--table', 'vacview', 'postgres' ],
 	0,
diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index b13f3c4224..1aa5c46af5 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -25,6 +25,7 @@
 #include "fe_utils/query_utils.h"
 #include "fe_utils/simple_list.h"
 #include "fe_utils/string_utils.h"
+#include "pqexpbuffer.h"
 
 
 /* vacuum options controlled by user flags */
@@ -47,6 +48,8 @@ typedef struct vacuumingOptions
 	bool		process_main;
 	bool		process_toast;
 	bool		skip_database_stats;
+	bool		analyze_in_stages;
+	bool		force_analyze;
 	char	   *buffer_usage_limit;
 } vacuumingOptions;
 
@@ -75,7 +78,6 @@ static void vacuum_one_database(ConnParams *cparams,
 
 static void vacuum_all_databases(ConnParams *cparams,
 								 vacuumingOptions *vacopts,
-								 bool analyze_in_stages,
 								 SimpleStringList *objects,
 								 int concurrentCons,
 								 const char *progname, bool echo, bool quiet);
@@ -140,6 +142,7 @@ main(int argc, char *argv[])
 		{"no-process-toast", no_argument, NULL, 11},
 		{"no-process-main", no_argument, NULL, 12},
 		{"buffer-usage-limit", required_argument, NULL, 13},
+		{"force-analyze", no_argument, NULL, 14},
 		{NULL, 0, NULL, 0}
 	};
 
@@ -156,7 +159,6 @@ main(int argc, char *argv[])
 	bool		echo = false;
 	bool		quiet = false;
 	vacuumingOptions vacopts;
-	bool		analyze_in_stages = false;
 	SimpleStringList objects = {NULL, NULL};
 	int			concurrentCons = 1;
 	int			tbl_count = 0;
@@ -170,6 +172,8 @@ main(int argc, char *argv[])
 	vacopts.do_truncate = true;
 	vacopts.process_main = true;
 	vacopts.process_toast = true;
+	vacopts.force_analyze = false;
+	vacopts.analyze_in_stages = false;
 
 	pg_logging_init(argv[0]);
 	progname = get_progname(argv[0]);
@@ -251,7 +255,7 @@ main(int argc, char *argv[])
 				maintenance_db = pg_strdup(optarg);
 				break;
 			case 3:
-				analyze_in_stages = vacopts.analyze_only = true;
+				vacopts.analyze_in_stages = vacopts.analyze_only = true;
 				break;
 			case 4:
 				vacopts.disable_page_skipping = true;
@@ -287,6 +291,9 @@ main(int argc, char *argv[])
 			case 13:
 				vacopts.buffer_usage_limit = escape_quotes(optarg);
 				break;
+			case 14:
+				vacopts.force_analyze = true;
+				break;
 			default:
 				/* getopt_long already emitted a complaint */
 				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -372,6 +379,14 @@ main(int argc, char *argv[])
 		pg_fatal("cannot use the \"%s\" option with the \"%s\" option",
 				 "buffer-usage-limit", "full");
 
+	/*
+	 * --force-analyze is only valid when used with --analyze-only, -analyze,
+	 * or --analyze-in-stages
+	 */
+	if (vacopts.force_analyze && !vacopts.analyze_only && !vacopts.analyze_in_stages)
+		pg_fatal("can only use the \"%s\" option with \"%s\" or \"%s\"",
+				 "--force-analyze", "-Z/--analyze-only", "--analyze-in-stages");
+
 	/* fill cparams except for dbname, which is set below */
 	cparams.pghost = host;
 	cparams.pgport = port;
@@ -390,7 +405,6 @@ main(int argc, char *argv[])
 		cparams.dbname = maintenance_db;
 
 		vacuum_all_databases(&cparams, &vacopts,
-							 analyze_in_stages,
 							 &objects,
 							 concurrentCons,
 							 progname, echo, quiet);
@@ -413,20 +427,27 @@ main(int argc, char *argv[])
 		}
 
 		cparams.dbname = dbname;
-		stage = (analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
+		stage = (vacopts.analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
 
 		conn = connectDatabase(&cparams, progname, echo, false, true);
 		check_conn_options(conn, &vacopts);
 		print_processing_notice(conn, stage, progname, quiet);
 		found_objects = generate_catalog_list(conn, &vacopts, &objects, echo, &ntup);
+		vacuum_one_database(&cparams, &vacopts, stage,
+							&objects,
+							concurrentCons,
+							conn,
+							found_objects,
+							ntup,
+							progname, echo, quiet);
 
-		if (analyze_in_stages)
+		if (stage != ANALYZE_NO_STAGE)
 		{
-			for (stage = 0; stage < ANALYZE_NUM_STAGES; stage++)
+			for (stage = 1; stage < ANALYZE_NUM_STAGES; stage++)
 			{
 				/* the last pass disconnected the conn */
-				if (stage > 0)
-					conn = connectDatabase(&cparams, progname, echo, false, true);
+				conn = connectDatabase(&cparams, progname, echo, false, true);
+				print_processing_notice(conn, stage, progname, quiet);
 
 				vacuum_one_database(&cparams, &vacopts, stage,
 									&objects,
@@ -437,14 +458,6 @@ main(int argc, char *argv[])
 									progname, echo, quiet);
 			}
 		}
-		else
-			vacuum_one_database(&cparams, &vacopts, stage,
-								&objects,
-								concurrentCons,
-								conn,
-								found_objects,
-								ntup,
-								progname, echo, quiet);
 	}
 
 	exit(0);
@@ -763,6 +776,47 @@ generate_catalog_list(PGconn *conn,
 						  vacopts->min_mxid_age);
 	}
 
+	/*
+	 * If this query is for an analyze-only or analyze-in-stages, two
+	 * upgrade-centric operations, and force-analyze is NOT set, then
+	 * exclude any relations that already have their full compliment
+	 * of attribute stats and extended stats.
+	 *
+	 *
+	 */
+	if ((vacopts->analyze_only || vacopts->analyze_in_stages) &&
+		!vacopts->force_analyze)
+	{
+		/*
+		 * The pg_class in question has no pg_statistic rows representing
+		 * user-visible columns that lack a corresponding pg_statitic row.
+		 * Currently no differentiation is made for whether the
+		 * pg_statistic.stainherit is true or false.
+		 */
+		appendPQExpBufferStr(&catalog_query,
+							 " AND NOT EXISTS (\n"
+							 " SELECT NULL FROM pg_catalog.pg_attribute AS a\n"
+							 " WHERE a.attrelid OPERATOR(pg_catalog.=) c.oid\n"
+							 " AND a.attnum OPERATOR(pg_catalog.>) 0 AND NOT a.attisdropped\n"
+							 " AND NOT EXISTS (\n"
+							 " SELECT NULL FROM pg_catalog.pg_statistic AS s\n"
+							 " WHERE s.starelid OPERATOR(pg_catalog.=) c.oid\n"
+							 " AND s.staattnum OPERATOR(pg_catalog.=) a.attnum))\n");
+
+		/*
+		 * The pg_class entry has no pg_statistic_ext rows that lack a corresponding
+		 * pg_statistic_ext_data row. Currently no differentiation is made for whether
+		 * pg_statistic_exta_data.stxdinherit is true or false.
+		 */
+		appendPQExpBufferStr(&catalog_query,
+							 " AND NOT EXISTS (\n"
+							 " SELECT NULL FROM pg_catalog.pg_statistic_ext AS e\n"
+							 " WHERE e.stxrelid OPERATOR(pg_catalog.=) c.oid\n"
+							 " AND NOT EXISTS (\n"
+							 " SELECT NULL FROM pg_catalog.pg_statistic_ext_data AS d\n"
+							 " WHERE d.stxoid OPERATOR(pg_catalog.=) e.oid))\n");
+	}
+
 	/*
 	 * Execute the catalog query.  We use the default search_path for this
 	 * query for consistency with table lookups done elsewhere by the user.
@@ -959,7 +1013,6 @@ finish:
 static void
 vacuum_all_databases(ConnParams *cparams,
 					 vacuumingOptions *vacopts,
-					 bool analyze_in_stages,
 					 SimpleStringList *objects,
 					 int concurrentCons,
 					 const char *progname, bool echo, bool quiet)
@@ -972,7 +1025,7 @@ vacuum_all_databases(ConnParams *cparams,
 	SimpleStringList  **found_objects;
 	int				   *num_tuples;
 
-	stage = (analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
+	stage = (vacopts->analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
 
 	conn = connectMaintenanceDatabase(cparams, progname, echo);
 	result = executeQuery(conn,
diff --git a/doc/src/sgml/ref/vacuumdb.sgml b/doc/src/sgml/ref/vacuumdb.sgml
index 66fccb30a2..00ec927606 100644
--- a/doc/src/sgml/ref/vacuumdb.sgml
+++ b/doc/src/sgml/ref/vacuumdb.sgml
@@ -425,6 +425,12 @@ PostgreSQL documentation
        <para>
         Only calculate statistics for use by the optimizer (no vacuum).
        </para>
+       <para>
+        By default, this operation excludes relations that already have
+        statistics generated. If the option <option>--force-analyze</option>
+        is also specified, then relations with existing stastistics are not
+        excluded.
+       </para>
       </listitem>
      </varlistentry>
 
@@ -439,6 +445,47 @@ PostgreSQL documentation
         to produce usable statistics faster, and subsequent stages build the
         full statistics.
        </para>
+       <para>
+        This option is intended to be run after a <command>pg_upgrade</command>
+        to generate statistics for relations that have no stistatics or incomplete
+        statistics (such as those with extended statistics objects, which are not
+        imported on upgrade).
+       </para>
+       <para>
+        If the option <option>--force-analyze</option> is also specified, then
+        relations with existing stastistics are not excluded.
+        This option is only useful to analyze a database that currently has
+        no statistics or has wholly incorrect ones, such as if it is newly
+        populated from a restored dump.
+        Be aware that running with this option combinationin a database with
+        existing statistics may cause the query optimizer choices to become
+        transiently worse due to the low statistics targets of the early
+        stages.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--force-analyze</option></term>
+      <listitem>
+       <para>
+        This option can only be used if either <option>--analyze-only</option>
+        or <option>--analyze-in-stages</option> is specified. It modifies those
+        options to not filter out relations that already have statistics.
+       </para>
+       <para>
+
+        Only calculate statistics for use by the optimizer (no vacuum),
+        like <option>--analyze-only</option>.  Run three
+        stages of analyze; the first stage uses the lowest possible statistics
+        target (see <xref linkend="guc-default-statistics-target"/>)
+        to produce usable statistics faster, and subsequent stages build the
+        full statistics.
+       </para>
+
+       <para>
+        This option was created
+       </para>
 
        <para>
         This option is only useful to analyze a database that currently has
@@ -452,6 +499,7 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+
      <varlistentry>
        <term><option>-?</option></term>
        <term><option>--help</option></term>
-- 
2.47.1

#260Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#259)
12 attachment(s)
Re: Statistics Import and Export

Per offline conversation with Jeff, adding a --no-schema to pg_dump option
both for completeness (we already have --no-data and --no-statistics), but
users who previously got the effect of --no-schema did so by specifying
--data-only, which suppresses statistics as well.

0001-0005 - changes to pg_dump/pg_upgrade
0006 - attribute stats optimization
0007-0012 - vacuumdb

On Wed, Dec 11, 2024 at 10:49 PM Corey Huinker <corey.huinker@gmail.com>
wrote:

Show quoted text

+1, assuming such an option is wanted at all. I suppose it should be

there for the unlikely (and hopefully impossible) case that statistics
are causing a problem during upgrade.

Here you go, rebased and re-ordered:

0001-0004 are the pg_dump/pg_upgrade related patches.
0005 is an optimization to the attribute stats update
0006-0011 is the still-up-for-debate vacuumdb changes.

The patch for handling the as-yet-theoretical change to default relpages
for partitioned tables got messy in the rebase, so I decided to just leave
it out for now, as the change to relpages looks increasingly unlikely.

Attachments:

v35-0004-Add-no-schema-option-to-pg_dump-etc.patchtext/x-patch; charset=US-ASCII; name=v35-0004-Add-no-schema-option-to-pg_dump-etc.patchDownload
From 27674ebea83387e0d714239effac486a30d8d70d Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 12 Dec 2024 16:58:21 -0500
Subject: [PATCH v35 04/12] Add --no-schema option to pg_dump, etc.

Previously, users could use --data-only when they wanted to suppress
schema from a dump. However, that no longer makes sense now that the
data/schema binary has become the data/schema/statistics trinary.
---
 src/bin/pg_dump/pg_backup.h          |  2 ++
 src/bin/pg_dump/pg_backup_archiver.c |  1 +
 src/bin/pg_dump/pg_dump.c            |  6 ++++--
 src/bin/pg_dump/pg_restore.c         |  6 +++++-
 doc/src/sgml/ref/pg_dump.sgml        |  9 +++++++++
 doc/src/sgml/ref/pg_restore.sgml     | 10 ++++++++++
 6 files changed, 31 insertions(+), 3 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 241855d017..24295110cf 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -112,6 +112,7 @@ typedef struct _restoreOptions
 	int			no_comments;	/* Skip comments */
 	int			no_data;			/* Skip data */
 	int			no_publications;	/* Skip publication entries */
+	int			no_schema;			/* Skip schema generation */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
 	int			no_statistics;		/* Skip statistics import */
@@ -187,6 +188,7 @@ typedef struct _dumpOptions
 	int			no_subscriptions;
 	int			no_statistics;
 	int			no_data;
+	int			no_schema;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 41001e64ac..d62f419560 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -187,6 +187,7 @@ dumpOptionsFromRestoreOptions(RestoreOptions *ropt)
 	dopt->no_security_labels = ropt->no_security_labels;
 	dopt->no_subscriptions = ropt->no_subscriptions;
 	dopt->no_data = ropt->no_data;
+	dopt->no_schema = ropt->no_schema;
 	dopt->no_statistics = ropt->no_statistics;
 	dopt->lockWaitTimeout = ropt->lockWaitTimeout;
 	dopt->include_everything = ropt->include_everything;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index e626243f3a..61f90d7f78 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -497,6 +497,7 @@ main(int argc, char **argv)
 		{"no-data", no_argument, &dopt.no_data, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-schema", no_argument, &dopt.no_schema, 1},
 		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
@@ -799,7 +800,8 @@ main(int argc, char **argv)
 
 	if (data_only && dopt.no_data)
 		pg_fatal("options -a/--data-only and --no-data cannot be used together");
-
+	if (schema_only && dopt.no_schema)
+		pg_fatal("options -s/--schema-only and --no-schema cannot be used together");
 	if (statistics_only && dopt.no_statistics)
 		pg_fatal("options -X/--statistics-only and --no-statistics cannot be used together");
 
@@ -817,7 +819,7 @@ main(int argc, char **argv)
 
 	/* set derivative flags */
 	dopt.dumpData = data_only || (!schema_only && !statistics_only && !dopt.no_data);
-	dopt.dumpSchema = schema_only || (!data_only && !statistics_only);
+	dopt.dumpSchema = schema_only || (!data_only && !statistics_only && !dopt.no_schema);
 	dopt.dumpStatistics = statistics_only || (!data_only && !schema_only && !dopt.no_statistics);
 
 	/*
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 31c3cd32de..90db03f0d4 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -72,6 +72,7 @@ main(int argc, char **argv)
 	static int	outputNoTablespaces = 0;
 	static int	use_setsessauth = 0;
 	static int	no_data = 0;
+	static int	no_schema = 0;
 	static int	no_comments = 0;
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
@@ -134,6 +135,7 @@ main(int argc, char **argv)
 		{"filter", required_argument, NULL, 4},
 		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-data", no_argument, &no_data, 1},
+		{"no-schema", no_argument, &no_schema, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -376,7 +378,7 @@ main(int argc, char **argv)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
 	/* set derivative flags */
-	opts->dumpSchema = (!data_only && !statistics_only);
+	opts->dumpSchema = (!no_schema && !data_only && !statistics_only);
 	opts->dumpData = (!no_data && !schema_only && !statistics_only);
 	opts->dumpStatistics = (!no_statistics && !data_only && !schema_only);
 
@@ -511,7 +513,9 @@ usage(const char *progname)
 	printf(_("  --no-data-for-failed-tables  do not restore data of tables that could not be\n"
 			 "                               created\n"));
 	printf(_("  --no-publications            do not restore publications\n"));
+	printf(_("  --no-schema                  do not restore schema\n"));
 	printf(_("  --no-security-labels         do not restore security labels\n"));
+	printf(_("  --no-statistics              do not restore statistics\n"));
 	printf(_("  --no-subscriptions           do not restore subscriptions\n"));
 	printf(_("  --no-table-access-method     do not restore table access methods\n"));
 	printf(_("  --no-tablespaces             do not restore tablespace assignments\n"));
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index aa4785c612..889b8d4e18 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -1113,6 +1113,15 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-schema</option></term>
+      <listitem>
+       <para>
+        Do not dump schema.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-statistics</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index ff1441a243..cc9dbb4808 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -738,6 +738,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-schema</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore schema, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-statistics</option></term>
       <listitem>
-- 
2.47.1

v35-0001-Enable-dumping-of-table-index-stats-in-pg_dump.patchtext/x-patch; charset=US-ASCII; name=v35-0001-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From a84983e60b37693b02362e1dacfda1bcca92dd91 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH v35 01/12] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will generate a statement that
calls pg_set_relation_stats(), and it will generate a series of
statements that call pg_set_attribute_stats(), one per attribute. These
statements will restore the statistics of the current system onto the
destination system.

Adds the command-line options -X / --statistics-only, which are mutually
exclusive to --schema-only and --data-only.

Statistics are not dumped when --schema-only is specified, except during
a binary upgrade.

As is the pattern with pg_dump options, staistics can be disabled using
--no-statistics.

Table statistics are dumped in the data section. This is true even if
dumping stats in a binary upgrade. Index and Materialized View
statistics are dumped in the post-data section.
---
 src/bin/pg_dump/pg_backup.h          |   4 +
 src/bin/pg_dump/pg_backup_archiver.c |   5 +
 src/bin/pg_dump/pg_dump.c            | 375 ++++++++++++++++++++++++++-
 src/bin/pg_dump/pg_dump.h            |   8 +
 src/bin/pg_dump/pg_dump_sort.c       |   5 +
 src/bin/pg_dump/pg_dumpall.c         |   5 +
 src/bin/pg_dump/pg_restore.c         |  18 +-
 src/bin/pg_dump/t/001_basic.pl       |  18 ++
 doc/src/sgml/ref/pg_dump.sgml        |  36 ++-
 doc/src/sgml/ref/pg_restore.sgml     |  31 ++-
 10 files changed, 489 insertions(+), 16 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index f0f19bb0b2..8fbb39d399 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -113,6 +113,7 @@ typedef struct _restoreOptions
 	int			no_publications;	/* Skip publication entries */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
@@ -160,6 +161,7 @@ typedef struct _restoreOptions
 	/* flags derived from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } RestoreOptions;
 
 typedef struct _dumpOptions
@@ -182,6 +184,7 @@ typedef struct _dumpOptions
 	int			no_security_labels;
 	int			no_publications;
 	int			no_subscriptions;
+	int			no_statistics;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
@@ -208,6 +211,7 @@ typedef struct _dumpOptions
 	/* flags derived from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } DumpOptions;
 
 /*
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 707a3fc844..185d7fbb7e 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2962,6 +2962,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's a stats dump, maybe ignore it */
+	if (ropt->no_statistics && strcmp(te->desc, "STATISTICS") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2991,6 +2995,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 89276524ae..8f85b04c33 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -430,6 +430,7 @@ main(int argc, char **argv)
 	DataDirSyncMethod sync_method = DATA_DIR_SYNC_METHOD_FSYNC;
 	bool		data_only = false;
 	bool		schema_only = false;
+	bool		statistics_only = false;
 
 	static DumpOptions dopt;
 
@@ -466,6 +467,7 @@ main(int argc, char **argv)
 		{"encoding", required_argument, NULL, 'E'},
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
+		{"statistics-only", no_argument, NULL, 'X'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -494,6 +496,7 @@ main(int argc, char **argv)
 		{"no-comments", no_argument, &dopt.no_comments, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -539,7 +542,7 @@ main(int argc, char **argv)
 
 	InitDumpOptions(&dopt);
 
-	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:RsS:t:T:U:vwWxZ:",
+	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:PRsS:t:T:U:vwWxXZ:",
 							long_options, &optindex)) != -1)
 	{
 		switch (c)
@@ -613,6 +616,10 @@ main(int argc, char **argv)
 				dopt.cparams.pgport = pg_strdup(optarg);
 				break;
 
+			case 'X':			/* Dump statistics only */
+				statistics_only = true;
+				break;
+
 			case 'R':
 				/* no-op, still accepted for backwards compatibility */
 				break;
@@ -784,6 +791,13 @@ main(int argc, char **argv)
 
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (schema_only && statistics_only)
+		pg_fatal("options -s/--schema-only and -X/--statistics-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
+
+	if (statistics_only && dopt.no_statistics)
+		pg_fatal("options -X/--statistics-only and --no-statistics cannot be used together");
 
 	if (schema_only && foreign_servers_include_patterns.head != NULL)
 		pg_fatal("options -s/--schema-only and --include-foreign-data cannot be used together");
@@ -798,8 +812,20 @@ main(int argc, char **argv)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
 	/* set derivative flags */
-	dopt.dumpSchema = (!data_only);
-	dopt.dumpData = (!schema_only);
+	dopt.dumpData = data_only || (!schema_only && !statistics_only);
+	dopt.dumpSchema = schema_only || (!data_only && !statistics_only);
+
+	if (statistics_only)
+		/* stats are only thing wanted */
+		dopt.dumpStatistics = true;
+	else if (dopt.no_statistics)
+		/* stats specifically excluded */
+		dopt.dumpStatistics = false;
+	else if (dopt.binary_upgrade)
+		/* binary upgrade and not specifically excluded */
+		dopt.dumpStatistics = true;
+	else
+		dopt.dumpStatistics = (!data_only && !schema_only);
 
 	/*
 	 * --inserts are already implied above if --column-inserts or
@@ -1099,6 +1125,7 @@ main(int argc, char **argv)
 	ropt->dropSchema = dopt.outputClean;
 	ropt->dumpData = dopt.dumpData;
 	ropt->dumpSchema = dopt.dumpSchema;
+	ropt->dumpStatistics = dopt.dumpStatistics;
 	ropt->if_exists = dopt.if_exists;
 	ropt->column_inserts = dopt.column_inserts;
 	ropt->dumpSections = dopt.dumpSections;
@@ -1177,7 +1204,7 @@ help(const char *progname)
 	printf(_("  -?, --help                   show this help, then exit\n"));
 
 	printf(_("\nOptions controlling the output content:\n"));
-	printf(_("  -a, --data-only              dump only the data, not the schema\n"));
+	printf(_("  -a, --data-only              dump only the data, not the schema or statistics\n"));
 	printf(_("  -b, --large-objects          include large objects in dump\n"));
 	printf(_("  --blobs                      (same as --large-objects, deprecated)\n"));
 	printf(_("  -B, --no-large-objects       exclude large objects in dump\n"));
@@ -1190,11 +1217,12 @@ help(const char *progname)
 	printf(_("  -N, --exclude-schema=PATTERN do NOT dump the specified schema(s)\n"));
 	printf(_("  -O, --no-owner               skip restoration of object ownership in\n"
 			 "                               plain-text format\n"));
-	printf(_("  -s, --schema-only            dump only the schema, no data\n"));
+	printf(_("  -s, --schema-only            dump only the schema, no data or statistics\n"));
 	printf(_("  -S, --superuser=NAME         superuser user name to use in plain-text format\n"));
 	printf(_("  -t, --table=PATTERN          dump only the specified table(s)\n"));
 	printf(_("  -T, --exclude-table=PATTERN  do NOT dump the specified table(s)\n"));
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
+	printf(_("  -X, --statistics-only        dump only the statistics, not schema or data\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
@@ -1221,6 +1249,7 @@ help(const char *progname)
 	printf(_("  --no-comments                do not dump comment commands\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -6777,6 +6806,42 @@ getFuncs(Archive *fout)
 	destroyPQExpBuffer(query);
 }
 
+/*
+ * getRelationStatistics
+ *    register the statistics object as a dependent of the relation.
+ *
+ */
+static RelStatsInfo *
+getRelationStatistics(Archive *fout, DumpableObject *rel, char relkind)
+{
+	if ((relkind == RELKIND_RELATION) ||
+		(relkind == RELKIND_PARTITIONED_TABLE) ||
+		(relkind == RELKIND_INDEX) ||
+		(relkind == RELKIND_PARTITIONED_INDEX) ||
+		(relkind == RELKIND_MATVIEW))
+	{
+		RelStatsInfo	   *info = pg_malloc0(sizeof(RelStatsInfo));
+		DumpableObject	   *dobj = &info->dobj;
+
+		dobj->objType = DO_REL_STATS;
+		dobj->catId.tableoid = 0;
+		dobj->catId.oid = 0;
+		AssignDumpId(dobj);
+		dobj->dependencies = (DumpId *) pg_malloc(sizeof(DumpId));
+		dobj->dependencies[0] = rel->dumpId;
+		dobj->nDeps = 1;
+		dobj->allocDeps = 1;
+		dobj->components |= DUMP_COMPONENT_STATISTICS;
+		dobj->dump = rel->dump;
+		dobj->name = pg_strdup(rel->name);
+		dobj->namespace = rel->namespace;
+		info->relkind = relkind;
+
+		return info;
+	}
+	return NULL;
+}
+
 /*
  * getTables
  *	  read all the tables (no indexes) in the system catalogs,
@@ -7154,6 +7219,7 @@ getTables(Archive *fout, int *numTables)
 
 		/* Tables have data */
 		tblinfo[i].dobj.components |= DUMP_COMPONENT_DATA;
+		tblinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
 
 		/* Mark whether table has an ACL */
 		if (!PQgetisnull(res, i, i_relacl))
@@ -7202,6 +7268,8 @@ getTables(Archive *fout, int *numTables)
 				}
 			}
 		}
+		if (tblinfo[i].interesting)
+			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relkind);
 	}
 
 	if (query->len != 0)
@@ -7646,11 +7714,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 		for (int c = 0; c < numinds; c++, j++)
 		{
 			char		contype;
+			char		indexkind;
+			RelStatsInfo   *relstats;
 
 			indxinfo[j].dobj.objType = DO_INDEX;
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -7673,7 +7744,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			{
 				NULL, NULL
 			};
+
+			if (indxinfo[j].parentidx == 0)
+				indexkind = RELKIND_INDEX;
+			else
+				indexkind = RELKIND_PARTITIONED_INDEX;
+
 			contype = *(PQgetvalue(res, j, i_contype));
+			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, indexkind);
 
 			if (contype == 'p' || contype == 'u' || contype == 'x')
 			{
@@ -7707,6 +7785,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				constrinfo->separate = true;
 
 				indxinfo[j].indexconstraint = constrinfo->dobj.dumpId;
+				if (relstats != NULL)
+					addObjectDependency(&relstats->dobj, constrinfo->dobj.dumpId);
 			}
 			else
 			{
@@ -10294,6 +10374,276 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * Tabular description of the parameters to pg_restore_relation_stats()
+ * param_name, param_type
+ */
+static const char *rel_stats_arginfo[][2] = {
+	{"relation", "regclass"},
+	{"version", "integer"},
+	{"relpages", "integer"},
+	{"reltuples", "real"},
+	{"relallvisible", "integer"},
+};
+
+/*
+ * Tabular description of the parameters to pg_restore_attribute_stats()
+ * param_name, param_type
+ */
+static const char *att_stats_arginfo[][2] = {
+	{"relation", "regclass"},
+	{"attname", "name"},
+	{"inherited", "boolean"},
+	{"version", "integer"},
+	{"null_frac", "float4"},
+	{"avg_width", "integer"},
+	{"n_distinct", "float4"},
+	{"most_common_vals", "text"},
+	{"most_common_freqs", "float4[]"},
+	{"histogram_bounds", "text"},
+	{"correlation", "float4"},
+	{"most_common_elems", "text"},
+	{"most_common_elem_freqs", "float4[]"},
+	{"elem_count_histogram", "float4[]"},
+	{"range_length_histogram", "text"},
+	{"range_empty_frac", "float4"},
+	{"range_bounds_histogram", "text"},
+};
+
+/*
+ * getRelStatsExportQuery --
+ *
+ * Generate a query that will fetch all relation (e.g. pg_class)
+ * stats for a given relation.
+ */
+static void
+getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "current_setting('server_version_num') AS version, "
+						 "c.relpages, c.reltuples, c.relallvisible "
+						 "FROM pg_class c "
+						 "JOIN pg_namespace n "
+						 "ON n.oid = c.relnamespace "
+						 "WHERE n.nspname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND c.relname = ");
+	appendStringLiteralAH(query, relname, fout);
+}
+
+/*
+ * getAttStatsExportQuery --
+ *
+ * Generate a query that will fetch all attribute (e.g. pg_statistic)
+ * stats for a given relation.
+ */
+static void
+getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "s.attname,"
+						 "s.inherited,"
+						 "current_setting('server_version_num') AS version, "
+						 "s.null_frac,"
+						 "s.avg_width,"
+						 "s.n_distinct,"
+						 "s.most_common_vals,"
+						 "s.most_common_freqs,"
+						 "s.histogram_bounds,"
+						 "s.correlation,"
+						 "s.most_common_elems,"
+						 "s.most_common_elem_freqs,"
+						 "s.elem_count_histogram,");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 "s.range_length_histogram,"
+							 "s.range_empty_frac,"
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(query,
+							 "NULL AS range_length_histogram,"
+							 "NULL AS range_empty_frac,"
+							 "NULL AS range_bounds_histogram ");
+
+	appendPQExpBufferStr(query,
+						 "FROM pg_stats s "
+						 "JOIN pg_namespace n "
+						 "ON n.nspname = s.schemaname "
+						 "JOIN pg_class c "
+						 "ON c.relname = s.tablename "
+						 "AND c.relnamespace = n.oid "
+						 "WHERE s.schemaname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND s.tablename = ");
+	appendStringLiteralAH(query, relname, fout);
+	appendPQExpBufferStr(query, " ORDER BY s.attname, s.inherited");
+}
+
+
+/*
+ * appendNamedArgument --
+ *
+ * Convenience routine for constructing parameters of the form:
+ * 'paraname', 'value'::type
+ */
+static void
+appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
+					const char *argval, const char *argtype)
+{
+	appendPQExpBufferStr(out, "\t");
+
+	appendStringLiteralAH(out, argname, fout);
+	appendPQExpBufferStr(out, ", ");
+
+	appendStringLiteralAH(out, argval, fout);
+	appendPQExpBuffer(out, "::%s", argtype);
+}
+
+/*
+ * appendRelStatsImport --
+ *
+ * Append a formatted pg_restore_relation_stats statement.
+ */
+static void
+appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	const char *sep = "";
+
+	if (PQntuples(res) == 0)
+		return;
+
+	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
+
+	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
+	{
+		const char *argname = rel_stats_arginfo[argno][0];
+		const char *argtype = rel_stats_arginfo[argno][1];
+		int			fieldno = PQfnumber(res, argname);
+
+		if (fieldno < 0)
+			pg_fatal("relation stats export query missing field '%s'",
+					 argname);
+
+		if (PQgetisnull(res, 0, fieldno))
+			continue;
+
+		appendPQExpBufferStr(out, sep);
+		appendNamedArgument(out, fout, argname, PQgetvalue(res, 0, fieldno), argtype);
+
+		sep = ",\n";
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+/*
+ * appendAttStatsImport --
+ *
+ * Append a series of formatted pg_restore_attribute_stats statements.
+ */
+static void
+appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	{
+		const char *sep = "";
+
+		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+		for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
+		{
+			const char *argname = att_stats_arginfo[argno][0];
+			const char *argtype = att_stats_arginfo[argno][1];
+			int			fieldno = PQfnumber(res, argname);
+
+			if (fieldno < 0)
+				pg_fatal("attribute stats export query missing field '%s'",
+						 argname);
+
+			if (PQgetisnull(res, rownum, fieldno))
+				continue;
+
+			appendPQExpBufferStr(out, sep);
+			appendNamedArgument(out, fout, argname, PQgetvalue(res, rownum, fieldno), argtype);
+			sep = ",\n";
+		}
+		appendPQExpBufferStr(out, "\n);\n");
+	}
+}
+
+/*
+ * get statistics dump section, which depends on the parent object type
+ *
+ * objects created in SECTION_PRE_DATA have stats in SECTION_DATA
+ * objects created in SECTION_POST_DATA have stats in SECTION_POST_DATA
+ */
+static teSection
+statisticsDumpSection(const RelStatsInfo *rsinfo)
+{
+
+	if ((rsinfo->relkind == RELKIND_MATVIEW) ||
+		(rsinfo->relkind == RELKIND_INDEX) ||
+		(rsinfo->relkind == RELKIND_PARTITIONED_INDEX))
+			return SECTION_POST_DATA;
+
+	return SECTION_DATA;
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	PGresult	   *res;
+	PQExpBuffer		query;
+	PQExpBuffer		out;
+	PQExpBuffer		tag;
+	DumpableObject *dobj = (DumpableObject *) &rsinfo->dobj;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", "STATISTICS DATA", fmtId(dobj->name));
+
+	query = createPQExpBuffer();
+	out = createPQExpBuffer();
+
+	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendRelStatsImport(out, fout, res);
+	PQclear(res);
+
+	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendAttStatsImport(out, fout, res);
+	PQclear(res);
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATISTICS DATA",
+							  .section = statisticsDumpSection(rsinfo),
+							  .createStmt = out->data,
+							  .deps = dobj->dependencies,
+							  .nDeps = dobj->nDeps));
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(out);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -10742,6 +11092,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_SUBSCRIPTION_REL:
 			dumpSubscriptionTable(fout, (const SubRelInfo *) dobj);
 			break;
+		case DO_REL_STATS:
+			dumpRelationStats(fout, (const RelStatsInfo *) dobj);
+			break;
 		case DO_PRE_DATA_BOUNDARY:
 		case DO_POST_DATA_BOUNDARY:
 			/* never dumped, nothing to do */
@@ -17172,6 +17525,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 		free(indstatvalsarray);
 	}
 
+	/* Comments and stats share same .dep */
+
 	/* Dump Index Comments */
 	if (indxinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
 		dumpComment(fout, "INDEX", qindxname,
@@ -18959,6 +19314,16 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 				/* must come after the pre-data boundary */
 				addObjectDependency(dobj, preDataBound->dumpId);
 				break;
+			case DO_REL_STATS:
+				/* stats section varies by parent object type, DATA or POST */
+				if (statisticsDumpSection((RelStatsInfo *) dobj) == SECTION_DATA)
+				{
+					addObjectDependency(dobj, preDataBound->dumpId);
+					addObjectDependency(postDataBound, dobj->dumpId);
+				}
+				else
+					addObjectDependency(dobj, postDataBound->dumpId);
+				break;
 		}
 	}
 }
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 2e55a0e3bb..def5f292e6 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -84,6 +84,7 @@ typedef enum
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION,
 	DO_SUBSCRIPTION_REL,		/* see note for SubRelInfo */
+	DO_REL_STATS,
 } DumpableObjectType;
 
 /*
@@ -101,6 +102,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
@@ -421,6 +423,12 @@ typedef struct _indexAttachInfo
 	IndxInfo   *partitionIdx;	/* link to index on partition */
 } IndexAttachInfo;
 
+typedef struct _relStatsInfo
+{
+	DumpableObject	dobj;
+	char			relkind;		/* 'r', 'v', 'c', etc */
+} RelStatsInfo;
+
 typedef struct _statsExtInfo
 {
 	DumpableObject dobj;
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 3c8f2eb808..989d20aa27 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -1500,6 +1500,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "POST-DATA BOUNDARY  (ID %d)",
 					 obj->dumpId);
 			return;
+		case DO_REL_STATS:
+			snprintf(buf, bufsize,
+					 "RELATION STATISTICS FOR %s  (ID %d OID %u)",
+					 obj->name, obj->dumpId, obj->catId.oid);
+			return;
 	}
 	/* shouldn't get here */
 	snprintf(buf, bufsize,
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 9a04e51c81..62e2766c09 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -103,6 +103,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -172,6 +173,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -451,6 +453,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -666,6 +670,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 88ae39d938..355f0439da 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -63,6 +63,7 @@ main(int argc, char **argv)
 	int			numWorkers = 1;
 	Archive    *AH;
 	char	   *inputFileSpec;
+	bool		statistics_only = false;
 	static int	disable_triggers = 0;
 	static int	enable_row_security = 0;
 	static int	if_exists = 0;
@@ -74,6 +75,7 @@ main(int argc, char **argv)
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
 	static int	no_subscriptions = 0;
+	static int  no_statistics = 0;
 	static int	strict_names = 0;
 	bool		data_only = false;
 	bool		schema_only = false;
@@ -108,6 +110,7 @@ main(int argc, char **argv)
 		{"username", 1, NULL, 'U'},
 		{"verbose", 0, NULL, 'v'},
 		{"single-transaction", 0, NULL, '1'},
+		{"statistics-only", no_argument, NULL, 'P'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -128,6 +131,7 @@ main(int argc, char **argv)
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
+		{"no-statistics", no_argument, &no_statistics, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -271,6 +275,10 @@ main(int argc, char **argv)
 				opts->aclsSkip = 1;
 				break;
 
+			case 'X':			/* Restore statistics only */
+				statistics_only = true;
+				break;
+
 			case '1':			/* Restore data in a single transaction */
 				opts->single_txn = true;
 				opts->exit_on_error = true;
@@ -343,6 +351,10 @@ main(int argc, char **argv)
 
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
+	if (schema_only && statistics_only)
+		pg_fatal("options -s/--schema-only and -X/--statistics-only cannot be used together");
 
 	if (data_only && opts->dropSchema)
 		pg_fatal("options -c/--clean and -a/--data-only cannot be used together");
@@ -362,8 +374,9 @@ main(int argc, char **argv)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
 	/* set derivative flags */
-	opts->dumpSchema = (!data_only);
-	opts->dumpData = (!schema_only);
+	opts->dumpSchema = (!data_only && !statistics_only);
+	opts->dumpData = (!schema_only && !statistics_only);
+	opts->dumpStatistics = (!no_statistics && !data_only && !schema_only);
 
 	opts->disable_triggers = disable_triggers;
 	opts->enable_row_security = enable_row_security;
@@ -375,6 +388,7 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
diff --git a/src/bin/pg_dump/t/001_basic.pl b/src/bin/pg_dump/t/001_basic.pl
index b9d13a0e1d..76ebf15f55 100644
--- a/src/bin/pg_dump/t/001_basic.pl
+++ b/src/bin/pg_dump/t/001_basic.pl
@@ -50,12 +50,30 @@ command_fails_like(
 	'pg_dump: options -s/--schema-only and -a/--data-only cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '-s', '-X' ],
+	qr/\Qpg_dump: error: options -s\/--schema-only and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: error: options -s/--schema-only and -X/--statistics-only cannot be used together'
+);
+
+command_fails_like(
+	[ 'pg_dump', '-a', '-X' ],
+	qr/\Qpg_dump: error: options -a\/--data-only and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: error: options -a/--data-only and -X/--statistics-only cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-s', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: options -s\/--schema-only and --include-foreign-data cannot be used together\E/,
 	'pg_dump: options -s/--schema-only and --include-foreign-data cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '--statistics-only', '--no-statistics' ],
+	qr/\Qpg_dump: error: options -X\/--statistics-only and --no-statistics cannot be used together\E/,
+	'pg_dump: options -X\/--statistics-only and --no-statistics cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-j2', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: option --include-foreign-data is not supported with parallel backup\E/,
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index d66e901f51..aa4785c612 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -123,7 +123,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Dump only the data, not the schema (data definitions).
+        Dump only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are dumped.
        </para>
 
@@ -141,8 +141,9 @@ PostgreSQL documentation
       <listitem>
        <para>
         Include large objects in the dump.  This is the default behavior
-        except when <option>--schema</option>, <option>--table</option>, or
-        <option>--schema-only</option> is specified.  The <option>-b</option>
+        except when <option>--schema</option>, <option>--table</option>,
+        <option>--schema-only</option>, or
+        <option>--statistics-only</option> is specified.  The <option>-b</option>
         switch is therefore only useful to add large objects to dumps
         where a specific schema or table has been requested.  Note that
         large objects are considered data and therefore will be included when
@@ -516,10 +517,11 @@ PostgreSQL documentation
       <term><option>--schema-only</option></term>
       <listitem>
        <para>
-        Dump only the object definitions (schema), not data.
+        Dump only the object definitions (schema), not data or statistics.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive to <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
         <option>--section=pre-data --section=post-data</option>.
@@ -652,6 +654,18 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Dump only the statistics, not the schema (data definitions) or data.
+        Statistics for tables, materialized views, and indexes are dumped.
+       </para>
+
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-Z <replaceable class="parameter">level</replaceable></option></term>
       <term><option>-Z <replaceable class="parameter">method</replaceable></option>[:<replaceable>detail</replaceable>]</term>
@@ -833,7 +847,8 @@ PostgreSQL documentation
         though you do not need the data in it.
        </para>
        <para>
-        To exclude data for all tables in the database, see <option>--schema-only</option>.
+        To exclude data for all tables in the database, see <option>--schema-only</option>
+        or <option>--statistics-only</option>.
        </para>
       </listitem>
      </varlistentry>
@@ -1098,6 +1113,15 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index b8b27e1719..ff1441a243 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -94,7 +94,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Restore only the data, not the schema (data definitions).
+        Restore only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are restored,
         if present in the archive.
        </para>
@@ -483,10 +483,11 @@ PostgreSQL documentation
         to the extent that schema entries are present in the archive.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive of <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
-        <option>--section=pre-data --section=post-data</option>.
+        <option>--section=pre-data --section=post-data --no-statistics</option>.
        </para>
        <para>
         (Do not confuse this with the <option>--schema</option> option, which
@@ -599,6 +600,20 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Restore only the statistics, not schema or data.
+       </para>
+       <para>
+        (Do not confuse this with the <option>--schema</option> option, which
+        uses the word <quote>schema</quote> in a different meaning.)
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-1</option></term>
       <term><option>--single-transaction</option></term>
@@ -723,6 +738,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore statistics, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
-- 
2.47.1

v35-0005-Add-statistics-flags-to-pg_upgrade.patchtext/x-patch; charset=US-ASCII; name=v35-0005-Add-statistics-flags-to-pg_upgrade.patchDownload
From ab4a753d50cd2fbea7e8cd3e8b1dfa37f4ac9508 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 11 Dec 2024 22:38:00 -0500
Subject: [PATCH v35 05/12] Add statistics flags to pg_upgrade

---
 src/bin/pg_upgrade/dump.c       |  6 ++++--
 src/bin/pg_upgrade/option.c     | 12 ++++++++++++
 src/bin/pg_upgrade/pg_upgrade.h |  1 +
 3 files changed, 17 insertions(+), 2 deletions(-)

diff --git a/src/bin/pg_upgrade/dump.c b/src/bin/pg_upgrade/dump.c
index ef2f14a79b..954e7bacf4 100644
--- a/src/bin/pg_upgrade/dump.c
+++ b/src/bin/pg_upgrade/dump.c
@@ -21,10 +21,11 @@ generate_old_dump(void)
 
 	/* run new pg_dumpall binary for globals */
 	exec_prog(UTILITY_LOG_FILE, NULL, true, true,
-			  "\"%s/pg_dumpall\" %s --globals-only --quote-all-identifiers "
+			  "\"%s/pg_dumpall\" %s %s --globals-only --quote-all-identifiers "
 			  "--binary-upgrade %s --no-sync -f \"%s/%s\"",
 			  new_cluster.bindir, cluster_conn_opts(&old_cluster),
 			  log_opts.verbose ? "--verbose" : "",
+			  user_opts.do_statistics ? "" : "--no-statistics",
 			  log_opts.dumpdir,
 			  GLOBALS_DUMP_FILE);
 	check_ok();
@@ -52,10 +53,11 @@ generate_old_dump(void)
 		snprintf(log_file_name, sizeof(log_file_name), DB_DUMP_LOG_FILE_MASK, old_db->db_oid);
 
 		parallel_exec_prog(log_file_name, NULL,
-						   "\"%s/pg_dump\" %s --no-data --quote-all-identifiers "
+						   "\"%s/pg_dump\" %s --no-data %s --quote-all-identifiers "
 						   "--binary-upgrade --format=custom %s --no-sync --file=\"%s/%s\" %s",
 						   new_cluster.bindir, cluster_conn_opts(&old_cluster),
 						   log_opts.verbose ? "--verbose" : "",
+						   user_opts.do_statistics ? "" : "--no-statistics",
 						   log_opts.dumpdir,
 						   sql_file_name, escaped_connstr.data);
 
diff --git a/src/bin/pg_upgrade/option.c b/src/bin/pg_upgrade/option.c
index 6f41d63eed..76a0bb2991 100644
--- a/src/bin/pg_upgrade/option.c
+++ b/src/bin/pg_upgrade/option.c
@@ -60,6 +60,8 @@ parseCommandLine(int argc, char *argv[])
 		{"copy", no_argument, NULL, 2},
 		{"copy-file-range", no_argument, NULL, 3},
 		{"sync-method", required_argument, NULL, 4},
+		{"with-statistics", no_argument, NULL, 5},
+		{"no-statistics", no_argument, NULL, 6},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -70,6 +72,7 @@ parseCommandLine(int argc, char *argv[])
 
 	user_opts.do_sync = true;
 	user_opts.transfer_mode = TRANSFER_MODE_COPY;
+	user_opts.do_statistics = true;
 
 	os_info.progname = get_progname(argv[0]);
 
@@ -212,6 +215,13 @@ parseCommandLine(int argc, char *argv[])
 				user_opts.sync_method = pg_strdup(optarg);
 				break;
 
+			case 5:
+				user_opts.do_statistics = true;
+				break;
+			case 6:
+				user_opts.do_statistics = false;
+				break;
+
 			default:
 				fprintf(stderr, _("Try \"%s --help\" for more information.\n"),
 						os_info.progname);
@@ -307,6 +317,8 @@ usage(void)
 	printf(_("  --copy                        copy files to new cluster (default)\n"));
 	printf(_("  --copy-file-range             copy files to new cluster with copy_file_range\n"));
 	printf(_("  --sync-method=METHOD          set method for syncing files to disk\n"));
+	printf(_("  --with-statisttics            import statistics from old cluster (default)\n"));
+	printf(_("  --no-statisttics              do not import statistics from old cluster\n"));
 	printf(_("  -?, --help                    show this help, then exit\n"));
 	printf(_("\n"
 			 "Before running pg_upgrade you must:\n"
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index 53f693c2d4..371f9a2d06 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -327,6 +327,7 @@ typedef struct
 	int			jobs;			/* number of processes/threads to use */
 	char	   *socketdir;		/* directory to use for Unix sockets */
 	char	   *sync_method;
+	bool		do_statistics;	/* carry over statistics from old cluster */
 } UserOpts;
 
 typedef struct
-- 
2.47.1

v35-0002-Add-no-data-option.patchtext/x-patch; charset=US-ASCII; name=v35-0002-Add-no-data-option.patchDownload
From 436f30b7bce2cd6eb9904384ca0395a7abab6bfd Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 14 Nov 2024 04:58:17 -0500
Subject: [PATCH v35 02/12] Add --no-data option.

This option is useful for situations where someone wishes to test
query plans from a production database without copying production data.
---
 src/bin/pg_dump/pg_backup.h          | 2 ++
 src/bin/pg_dump/pg_backup_archiver.c | 2 ++
 src/bin/pg_dump/pg_dump.c            | 7 ++++++-
 src/bin/pg_dump/pg_restore.c         | 5 ++++-
 4 files changed, 14 insertions(+), 2 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 8fbb39d399..241855d017 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -110,6 +110,7 @@ typedef struct _restoreOptions
 	int			column_inserts;
 	int			if_exists;
 	int			no_comments;	/* Skip comments */
+	int			no_data;			/* Skip data */
 	int			no_publications;	/* Skip publication entries */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
@@ -185,6 +186,7 @@ typedef struct _dumpOptions
 	int			no_publications;
 	int			no_subscriptions;
 	int			no_statistics;
+	int			no_data;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 185d7fbb7e..41001e64ac 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -186,6 +186,8 @@ dumpOptionsFromRestoreOptions(RestoreOptions *ropt)
 	dopt->no_publications = ropt->no_publications;
 	dopt->no_security_labels = ropt->no_security_labels;
 	dopt->no_subscriptions = ropt->no_subscriptions;
+	dopt->no_data = ropt->no_data;
+	dopt->no_statistics = ropt->no_statistics;
 	dopt->lockWaitTimeout = ropt->lockWaitTimeout;
 	dopt->include_everything = ropt->include_everything;
 	dopt->enable_row_security = ropt->enable_row_security;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 8f85b04c33..c8a0b4afdf 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -494,6 +494,7 @@ main(int argc, char **argv)
 		{"strict-names", no_argument, &strict_names, 1},
 		{"use-set-session-authorization", no_argument, &dopt.use_setsessauth, 1},
 		{"no-comments", no_argument, &dopt.no_comments, 1},
+		{"no-data", no_argument, &dopt.no_data, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
 		{"no-statistics", no_argument, &dopt.no_statistics, 1},
@@ -796,6 +797,9 @@ main(int argc, char **argv)
 	if (data_only && statistics_only)
 		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
 
+	if (data_only && dopt.no_data)
+		pg_fatal("options -a/--data-only and --no-data cannot be used together");
+
 	if (statistics_only && dopt.no_statistics)
 		pg_fatal("options -X/--statistics-only and --no-statistics cannot be used together");
 
@@ -812,7 +816,7 @@ main(int argc, char **argv)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
 	/* set derivative flags */
-	dopt.dumpData = data_only || (!schema_only && !statistics_only);
+	dopt.dumpData = data_only || (!schema_only && !statistics_only && !dopt.no_data);
 	dopt.dumpSchema = schema_only || (!data_only && !statistics_only);
 
 	if (statistics_only)
@@ -1247,6 +1251,7 @@ help(const char *progname)
 	printf(_("  --inserts                    dump data as INSERT commands, rather than COPY\n"));
 	printf(_("  --load-via-partition-root    load partitions via the root table\n"));
 	printf(_("  --no-comments                do not dump comment commands\n"));
+	printf(_("  --no-data                    do not dump data\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
 	printf(_("  --no-statistics              do not dump statistics\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 355f0439da..31c3cd32de 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -71,6 +71,7 @@ main(int argc, char **argv)
 	static int	outputNoTableAm = 0;
 	static int	outputNoTablespaces = 0;
 	static int	use_setsessauth = 0;
+	static int	no_data = 0;
 	static int	no_comments = 0;
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
@@ -132,6 +133,7 @@ main(int argc, char **argv)
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
 		{"no-statistics", no_argument, &no_statistics, 1},
+		{"no-data", no_argument, &no_data, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -375,7 +377,7 @@ main(int argc, char **argv)
 
 	/* set derivative flags */
 	opts->dumpSchema = (!data_only && !statistics_only);
-	opts->dumpData = (!schema_only && !statistics_only);
+	opts->dumpData = (!no_data && !schema_only && !statistics_only);
 	opts->dumpStatistics = (!no_statistics && !data_only && !schema_only);
 
 	opts->disable_triggers = disable_triggers;
@@ -389,6 +391,7 @@ main(int argc, char **argv)
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
 	opts->no_statistics = no_statistics;
+	opts->no_data = no_data;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
-- 
2.47.1

v35-0003-Change-pg_upgrade-s-invocation-of-pg_dump-to-use.patchtext/x-patch; charset=US-ASCII; name=v35-0003-Change-pg_upgrade-s-invocation-of-pg_dump-to-use.patchDownload
From 6b18d68111bcf25e66d76e792a2ebdc4543094cb Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 11 Dec 2024 14:28:54 -0500
Subject: [PATCH v35 03/12] Change pg_upgrade's invocation of pg_dump to use
 --no-data

---
 src/bin/pg_dump/pg_dump.c | 13 +------------
 src/bin/pg_upgrade/dump.c |  2 +-
 2 files changed, 2 insertions(+), 13 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index c8a0b4afdf..e626243f3a 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -818,18 +818,7 @@ main(int argc, char **argv)
 	/* set derivative flags */
 	dopt.dumpData = data_only || (!schema_only && !statistics_only && !dopt.no_data);
 	dopt.dumpSchema = schema_only || (!data_only && !statistics_only);
-
-	if (statistics_only)
-		/* stats are only thing wanted */
-		dopt.dumpStatistics = true;
-	else if (dopt.no_statistics)
-		/* stats specifically excluded */
-		dopt.dumpStatistics = false;
-	else if (dopt.binary_upgrade)
-		/* binary upgrade and not specifically excluded */
-		dopt.dumpStatistics = true;
-	else
-		dopt.dumpStatistics = (!data_only && !schema_only);
+	dopt.dumpStatistics = statistics_only || (!data_only && !schema_only && !dopt.no_statistics);
 
 	/*
 	 * --inserts are already implied above if --column-inserts or
diff --git a/src/bin/pg_upgrade/dump.c b/src/bin/pg_upgrade/dump.c
index 8345f55be8..ef2f14a79b 100644
--- a/src/bin/pg_upgrade/dump.c
+++ b/src/bin/pg_upgrade/dump.c
@@ -52,7 +52,7 @@ generate_old_dump(void)
 		snprintf(log_file_name, sizeof(log_file_name), DB_DUMP_LOG_FILE_MASK, old_db->db_oid);
 
 		parallel_exec_prog(log_file_name, NULL,
-						   "\"%s/pg_dump\" %s --schema-only --quote-all-identifiers "
+						   "\"%s/pg_dump\" %s --no-data --quote-all-identifiers "
 						   "--binary-upgrade --format=custom %s --no-sync --file=\"%s/%s\" %s",
 						   new_cluster.bindir, cluster_conn_opts(&old_cluster),
 						   log_opts.verbose ? "--verbose" : "",
-- 
2.47.1

v35-0007-split-out-check_conn_options.patchtext/x-patch; charset=US-ASCII; name=v35-0007-split-out-check_conn_options.patchDownload
From 4702b7b21f8b306d66650bb4e8354de9a131522c Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 6 Nov 2024 15:20:07 -0500
Subject: [PATCH v35 07/12] split out check_conn_options

---
 src/bin/scripts/vacuumdb.c | 103 ++++++++++++++++++++-----------------
 1 file changed, 57 insertions(+), 46 deletions(-)

diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index d07ab7d67e..7b97a9428a 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -10,6 +10,7 @@
  *-------------------------------------------------------------------------
  */
 
+#include "libpq-fe.h"
 #include "postgres_fe.h"
 
 #include <limits.h>
@@ -459,55 +460,11 @@ escape_quotes(const char *src)
 }
 
 /*
- * vacuum_one_database
- *
- * Process tables in the given database.  If the 'tables' list is empty,
- * process all tables in the database.
- *
- * Note that this function is only concerned with running exactly one stage
- * when in analyze-in-stages mode; caller must iterate on us if necessary.
- *
- * If concurrentCons is > 1, multiple connections are used to vacuum tables
- * in parallel.  In this case and if the table list is empty, we first obtain
- * a list of tables from the database.
+ * Check connection options for compatibility with the connected database.
  */
 static void
-vacuum_one_database(ConnParams *cparams,
-					vacuumingOptions *vacopts,
-					int stage,
-					SimpleStringList *objects,
-					int concurrentCons,
-					const char *progname, bool echo, bool quiet)
+check_conn_options(PGconn *conn, vacuumingOptions *vacopts)
 {
-	PQExpBufferData sql;
-	PQExpBufferData buf;
-	PQExpBufferData catalog_query;
-	PGresult   *res;
-	PGconn	   *conn;
-	SimpleStringListCell *cell;
-	ParallelSlotArray *sa;
-	SimpleStringList dbtables = {NULL, NULL};
-	int			i;
-	int			ntups;
-	bool		failed = false;
-	bool		objects_listed = false;
-	const char *initcmd;
-	const char *stage_commands[] = {
-		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
-		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
-		"RESET default_statistics_target;"
-	};
-	const char *stage_messages[] = {
-		gettext_noop("Generating minimal optimizer statistics (1 target)"),
-		gettext_noop("Generating medium optimizer statistics (10 targets)"),
-		gettext_noop("Generating default (full) optimizer statistics")
-	};
-
-	Assert(stage == ANALYZE_NO_STAGE ||
-		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
-
-	conn = connectDatabase(cparams, progname, echo, false, true);
-
 	if (vacopts->disable_page_skipping && PQserverVersion(conn) < 90600)
 	{
 		PQfinish(conn);
@@ -575,6 +532,60 @@ vacuum_one_database(ConnParams *cparams,
 
 	/* skip_database_stats is used automatically if server supports it */
 	vacopts->skip_database_stats = (PQserverVersion(conn) >= 160000);
+}
+
+
+/*
+ * vacuum_one_database
+ *
+ * Process tables in the given database.  If the 'tables' list is empty,
+ * process all tables in the database.
+ *
+ * Note that this function is only concerned with running exactly one stage
+ * when in analyze-in-stages mode; caller must iterate on us if necessary.
+ *
+ * If concurrentCons is > 1, multiple connections are used to vacuum tables
+ * in parallel.  In this case and if the table list is empty, we first obtain
+ * a list of tables from the database.
+ */
+static void
+vacuum_one_database(ConnParams *cparams,
+					vacuumingOptions *vacopts,
+					int stage,
+					SimpleStringList *objects,
+					int concurrentCons,
+					const char *progname, bool echo, bool quiet)
+{
+	PQExpBufferData sql;
+	PQExpBufferData buf;
+	PQExpBufferData catalog_query;
+	PGresult   *res;
+	PGconn	   *conn;
+	SimpleStringListCell *cell;
+	ParallelSlotArray *sa;
+	SimpleStringList dbtables = {NULL, NULL};
+	int			i;
+	int			ntups;
+	bool		failed = false;
+	bool		objects_listed = false;
+	const char *initcmd;
+	const char *stage_commands[] = {
+		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
+		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
+		"RESET default_statistics_target;"
+	};
+	const char *stage_messages[] = {
+		gettext_noop("Generating minimal optimizer statistics (1 target)"),
+		gettext_noop("Generating medium optimizer statistics (10 targets)"),
+		gettext_noop("Generating default (full) optimizer statistics")
+	};
+
+	Assert(stage == ANALYZE_NO_STAGE ||
+		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
+
+	conn = connectDatabase(cparams, progname, echo, false, true);
+
+	check_conn_options(conn, vacopts);
 
 	if (!quiet)
 	{
-- 
2.47.1

v35-0006-Consolidate-attribute-syscache-lookups-into-one-.patchtext/x-patch; charset=US-ASCII; name=v35-0006-Consolidate-attribute-syscache-lookups-into-one-.patchDownload
From 8dbae3174f3a6c8bb72beb577dc5c32ccb996e65 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 14 Nov 2024 03:53:33 -0500
Subject: [PATCH v35 06/12] Consolidate attribute syscache lookups into one
 call by name.

Previously we were doing one lookup by attname and one lookup by attnum,
which seems wasteful.
---
 src/backend/statistics/attribute_stats.c | 55 +++++++++++-------------
 1 file changed, 24 insertions(+), 31 deletions(-)

diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index b97ba7b0c0..6393783f8e 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -78,8 +78,8 @@ static struct StatsArgInfo attarginfo[] =
 
 static bool attribute_statistics_update(FunctionCallInfo fcinfo, int elevel);
 static Node *get_attr_expr(Relation rel, int attnum);
-static void get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
-							   Oid *atttypid, int32 *atttypmod,
+static void get_attr_stat_type(Oid reloid, Name attname, int elevel,
+							   AttrNumber *attnum, Oid *atttypid, int32 *atttypmod,
 							   char *atttyptype, Oid *atttypcoll,
 							   Oid *eq_opr, Oid *lt_opr);
 static bool get_elem_stat_type(Oid atttypid, char atttyptype, int elevel,
@@ -166,23 +166,16 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 
 	stats_check_required_arg(fcinfo, attarginfo, ATTNAME_ARG);
 	attname = PG_GETARG_NAME(ATTNAME_ARG);
-	attnum = get_attnum(reloid, NameStr(*attname));
-
-	if (attnum < 0)
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("cannot modify statistics on system column \"%s\"",
-						NameStr(*attname))));
-
-	if (attnum == InvalidAttrNumber)
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_COLUMN),
-				 errmsg("column \"%s\" of relation \"%s\" does not exist",
-						NameStr(*attname), get_rel_name(reloid))));
 
 	stats_check_required_arg(fcinfo, attarginfo, INHERITED_ARG);
 	inherited = PG_GETARG_BOOL(INHERITED_ARG);
 
+	/* derive type information from attribute */
+	get_attr_stat_type(reloid, attname, elevel,
+					   &attnum, &atttypid, &atttypmod,
+					   &atttyptype, &atttypcoll,
+					   &eq_opr, &lt_opr);
+
 	/*
 	 * Check argument sanity. If some arguments are unusable, emit at elevel
 	 * and set the corresponding argument to NULL in fcinfo.
@@ -232,12 +225,6 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 		result = false;
 	}
 
-	/* derive information from attribute */
-	get_attr_stat_type(reloid, attnum, elevel,
-					   &atttypid, &atttypmod,
-					   &atttyptype, &atttypcoll,
-					   &eq_opr, &lt_opr);
-
 	/* if needed, derive element type */
 	if (do_mcelem || do_dechist)
 	{
@@ -503,8 +490,8 @@ get_attr_expr(Relation rel, int attnum)
  * Derive type information from the attribute.
  */
 static void
-get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
-				   Oid *atttypid, int32 *atttypmod,
+get_attr_stat_type(Oid reloid, Name attname, int elevel,
+				   AttrNumber *attnum, Oid *atttypid, int32 *atttypmod,
 				   char *atttyptype, Oid *atttypcoll,
 				   Oid *eq_opr, Oid *lt_opr)
 {
@@ -514,24 +501,30 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
 	Node	   *expr;
 	TypeCacheEntry *typcache;
 
-	atup = SearchSysCache2(ATTNUM, ObjectIdGetDatum(reloid),
-						   Int16GetDatum(attnum));
+	atup = SearchSysCacheAttName(reloid, NameStr(*attname));
 
-	/* Attribute not found */
+	/* Attribute not found or is dropped */
 	if (!HeapTupleIsValid(atup))
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
-				 errmsg("attribute %d of relation \"%s\" does not exist",
-						attnum, RelationGetRelationName(rel))));
+				 errmsg("column \"%s\" of relation \"%s\" does not exist",
+						NameStr(*attname), get_rel_name(reloid))));
 
 	attr = (Form_pg_attribute) GETSTRUCT(atup);
 
-	if (attr->attisdropped)
+	if (attr->attnum < 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("cannot modify statistics on system column \"%s\"",
+						NameStr(*attname))));
+
+	if (attr->attnum == InvalidAttrNumber)
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
-				 errmsg("attribute %d of relation \"%s\" does not exist",
-						attnum, RelationGetRelationName(rel))));
+				 errmsg("column \"%s\" of relation \"%s\" does not exist",
+						NameStr(*attname), get_rel_name(reloid))));
 
+	*attnum = attr->attnum;
 	expr = get_attr_expr(rel, attr->attnum);
 
 	/*
-- 
2.47.1

v35-0008-split-out-print_processing_notice.patchtext/x-patch; charset=US-ASCII; name=v35-0008-split-out-print_processing_notice.patchDownload
From 1e405e14378c9184bad4bde4e19f2dda962df168 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 6 Nov 2024 15:46:51 -0500
Subject: [PATCH v35 08/12] split out print_processing_notice

---
 src/bin/scripts/vacuumdb.c | 41 +++++++++++++++++++++++---------------
 1 file changed, 25 insertions(+), 16 deletions(-)

diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index 7b97a9428a..e9946f79b2 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -535,6 +535,30 @@ check_conn_options(PGconn *conn, vacuumingOptions *vacopts)
 }
 
 
+/*
+ * print the processing notice for a database.
+ */
+static void
+print_processing_notice(PGconn *conn, int stage, const char *progname, bool quiet)
+{
+	const char *stage_messages[] = {
+		gettext_noop("Generating minimal optimizer statistics (1 target)"),
+		gettext_noop("Generating medium optimizer statistics (10 targets)"),
+		gettext_noop("Generating default (full) optimizer statistics")
+	};
+
+	if (!quiet)
+	{
+		if (stage != ANALYZE_NO_STAGE)
+			printf(_("%s: processing database \"%s\": %s\n"),
+				   progname, PQdb(conn), _(stage_messages[stage]));
+		else
+			printf(_("%s: vacuuming database \"%s\"\n"),
+				   progname, PQdb(conn));
+		fflush(stdout);
+	}
+}
+
 /*
  * vacuum_one_database
  *
@@ -574,11 +598,6 @@ vacuum_one_database(ConnParams *cparams,
 		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
 		"RESET default_statistics_target;"
 	};
-	const char *stage_messages[] = {
-		gettext_noop("Generating minimal optimizer statistics (1 target)"),
-		gettext_noop("Generating medium optimizer statistics (10 targets)"),
-		gettext_noop("Generating default (full) optimizer statistics")
-	};
 
 	Assert(stage == ANALYZE_NO_STAGE ||
 		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
@@ -586,17 +605,7 @@ vacuum_one_database(ConnParams *cparams,
 	conn = connectDatabase(cparams, progname, echo, false, true);
 
 	check_conn_options(conn, vacopts);
-
-	if (!quiet)
-	{
-		if (stage != ANALYZE_NO_STAGE)
-			printf(_("%s: processing database \"%s\": %s\n"),
-				   progname, PQdb(conn), _(stage_messages[stage]));
-		else
-			printf(_("%s: vacuuming database \"%s\"\n"),
-				   progname, PQdb(conn));
-		fflush(stdout);
-	}
+	print_processing_notice(conn, stage, progname, quiet);
 
 	/*
 	 * Prepare the list of tables to process by querying the catalogs.
-- 
2.47.1

v35-0009-split-out-generate_catalog_list.patchtext/x-patch; charset=US-ASCII; name=v35-0009-split-out-generate_catalog_list.patchDownload
From 10b12c50cf784741c763b3a0b6635e8d07b9c713 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 6 Nov 2024 16:15:16 -0500
Subject: [PATCH v35 09/12] split out generate_catalog_list

---
 src/bin/scripts/vacuumdb.c | 176 +++++++++++++++++++++----------------
 1 file changed, 99 insertions(+), 77 deletions(-)

diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index e9946f79b2..36f4796db0 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -560,67 +560,38 @@ print_processing_notice(PGconn *conn, int stage, const char *progname, bool quie
 }
 
 /*
- * vacuum_one_database
- *
- * Process tables in the given database.  If the 'tables' list is empty,
- * process all tables in the database.
- *
- * Note that this function is only concerned with running exactly one stage
- * when in analyze-in-stages mode; caller must iterate on us if necessary.
- *
- * If concurrentCons is > 1, multiple connections are used to vacuum tables
- * in parallel.  In this case and if the table list is empty, we first obtain
- * a list of tables from the database.
- */
-static void
-vacuum_one_database(ConnParams *cparams,
-					vacuumingOptions *vacopts,
-					int stage,
-					SimpleStringList *objects,
-					int concurrentCons,
-					const char *progname, bool echo, bool quiet)
+	* Prepare the list of tables to process by querying the catalogs.
+	*
+	* Since we execute the constructed query with the default search_path
+	* (which could be unsafe), everything in this query MUST be fully
+	* qualified.
+	*
+	* First, build a WITH clause for the catalog query if any tables were
+	* specified, with a set of values made of relation names and their
+	* optional set of columns.  This is used to match any provided column
+	* lists with the generated qualified identifiers and to filter for the
+	* tables provided via --table.  If a listed table does not exist, the
+	* catalog query will fail.
+	*/
+static SimpleStringList *
+generate_catalog_list(PGconn *conn,
+					  vacuumingOptions *vacopts,
+					  SimpleStringList *objects,
+					  bool echo,
+					  int *ntups)
 {
-	PQExpBufferData sql;
-	PQExpBufferData buf;
 	PQExpBufferData catalog_query;
-	PGresult   *res;
-	PGconn	   *conn;
+	PQExpBufferData buf;
+	SimpleStringList *dbtables;
 	SimpleStringListCell *cell;
-	ParallelSlotArray *sa;
-	SimpleStringList dbtables = {NULL, NULL};
-	int			i;
-	int			ntups;
-	bool		failed = false;
 	bool		objects_listed = false;
-	const char *initcmd;
-	const char *stage_commands[] = {
-		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
-		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
-		"RESET default_statistics_target;"
-	};
+	PGresult   *res;
+	int			i;
 
-	Assert(stage == ANALYZE_NO_STAGE ||
-		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
+	dbtables = palloc(sizeof(SimpleStringList));
+	dbtables->head = NULL;
+	dbtables->tail = NULL;
 
-	conn = connectDatabase(cparams, progname, echo, false, true);
-
-	check_conn_options(conn, vacopts);
-	print_processing_notice(conn, stage, progname, quiet);
-
-	/*
-	 * Prepare the list of tables to process by querying the catalogs.
-	 *
-	 * Since we execute the constructed query with the default search_path
-	 * (which could be unsafe), everything in this query MUST be fully
-	 * qualified.
-	 *
-	 * First, build a WITH clause for the catalog query if any tables were
-	 * specified, with a set of values made of relation names and their
-	 * optional set of columns.  This is used to match any provided column
-	 * lists with the generated qualified identifiers and to filter for the
-	 * tables provided via --table.  If a listed table does not exist, the
-	 * catalog query will fail.
-	 */
 	initPQExpBuffer(&catalog_query);
 	for (cell = objects ? objects->head : NULL; cell; cell = cell->next)
 	{
@@ -771,40 +742,91 @@ vacuum_one_database(ConnParams *cparams,
 	appendPQExpBufferStr(&catalog_query, " ORDER BY c.relpages DESC;");
 	executeCommand(conn, "RESET search_path;", echo);
 	res = executeQuery(conn, catalog_query.data, echo);
+	*ntups = PQntuples(res);
 	termPQExpBuffer(&catalog_query);
 	PQclear(executeQuery(conn, ALWAYS_SECURE_SEARCH_PATH_SQL, echo));
 
-	/*
-	 * If no rows are returned, there are no matching tables, so we are done.
-	 */
-	ntups = PQntuples(res);
-	if (ntups == 0)
-	{
-		PQclear(res);
-		PQfinish(conn);
-		return;
-	}
-
 	/*
 	 * Build qualified identifiers for each table, including the column list
 	 * if given.
 	 */
-	initPQExpBuffer(&buf);
-	for (i = 0; i < ntups; i++)
+	if (*ntups > 0)
 	{
-		appendPQExpBufferStr(&buf,
-							 fmtQualifiedId(PQgetvalue(res, i, 1),
-											PQgetvalue(res, i, 0)));
+		initPQExpBuffer(&buf);
+		for (i = 0; i < *ntups; i++)
+		{
+			appendPQExpBufferStr(&buf,
+								fmtQualifiedId(PQgetvalue(res, i, 1),
+												PQgetvalue(res, i, 0)));
 
-		if (objects_listed && !PQgetisnull(res, i, 2))
-			appendPQExpBufferStr(&buf, PQgetvalue(res, i, 2));
+			if (objects_listed && !PQgetisnull(res, i, 2))
+				appendPQExpBufferStr(&buf, PQgetvalue(res, i, 2));
 
-		simple_string_list_append(&dbtables, buf.data);
-		resetPQExpBuffer(&buf);
+			simple_string_list_append(dbtables, buf.data);
+			resetPQExpBuffer(&buf);
+		}
+		termPQExpBuffer(&buf);
 	}
-	termPQExpBuffer(&buf);
 	PQclear(res);
 
+	return dbtables;
+}
+
+/*
+ * vacuum_one_database
+ *
+ * Process tables in the given database.  If the 'tables' list is empty,
+ * process all tables in the database.
+ *
+ * Note that this function is only concerned with running exactly one stage
+ * when in analyze-in-stages mode; caller must iterate on us if necessary.
+ *
+ * If concurrentCons is > 1, multiple connections are used to vacuum tables
+ * in parallel.  In this case and if the table list is empty, we first obtain
+ * a list of tables from the database.
+ */
+static void
+vacuum_one_database(ConnParams *cparams,
+					vacuumingOptions *vacopts,
+					int stage,
+					SimpleStringList *objects,
+					int concurrentCons,
+					const char *progname, bool echo, bool quiet)
+{
+	PQExpBufferData sql;
+	PGconn	   *conn;
+	SimpleStringListCell *cell;
+	ParallelSlotArray *sa;
+	int			ntups;
+	bool		failed = false;
+	const char *initcmd;
+	SimpleStringList *dbtables;
+	const char *stage_commands[] = {
+		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
+		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
+		"RESET default_statistics_target;"
+	};
+
+	Assert(stage == ANALYZE_NO_STAGE ||
+		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
+
+	conn = connectDatabase(cparams, progname, echo, false, true);
+
+	check_conn_options(conn, vacopts);
+	print_processing_notice(conn, stage, progname, quiet);
+
+	dbtables = generate_catalog_list(conn, vacopts, objects, echo, &ntups);
+
+	/*
+	 * If no rows are returned, there are no matching tables, so we are done.
+	 */
+	if (ntups == 0)
+	{
+		PQfinish(conn);
+		return;
+	}
+
+
 	/*
 	 * Ensure concurrentCons is sane.  If there are more connections than
 	 * vacuumable relations, we don't need to use them all.
@@ -837,7 +859,7 @@ vacuum_one_database(ConnParams *cparams,
 
 	initPQExpBuffer(&sql);
 
-	cell = dbtables.head;
+	cell = dbtables->head;
 	do
 	{
 		const char *tabname = cell->val;
-- 
2.47.1

v35-0012-Add-force-analyze-to-vacuumdb.patchtext/x-patch; charset=US-ASCII; name=v35-0012-Add-force-analyze-to-vacuumdb.patchDownload
From 7626e26bb4a2fe71a34188e40167dcb5a38cea71 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Fri, 8 Nov 2024 12:27:50 -0500
Subject: [PATCH v35 12/12] Add --force-analyze to vacuumdb.

The vacuumdb options of --analyze-in-stages and --analyze-only are often
used after a restore from a dump or a pg_upgrade to quickly rebuild
stats on a databse.

However, now that stats are imported in most (but not all) cases,
running either of these commands will be at least partially redundant,
and will overwrite the stats that were just imported, which is a big
POLA violation.

We could add a new option such as --analyze-missing-in-stages, but that
wouldn't help the userbase that grown accustomed to running
--analyze-in-stages after an upgrade.

The least-bad option to handle the situation is to change the behavior
of --analyze-only and --analyze-in-stages to only analyze tables which
were missing stats before the vacuumdb started, but offer the
--force-analyze flag to restore the old behavior for those who truly
wanted it.
---
 src/bin/scripts/t/100_vacuumdb.pl |  6 +-
 src/bin/scripts/vacuumdb.c        | 91 ++++++++++++++++++++++++-------
 doc/src/sgml/ref/vacuumdb.sgml    | 48 ++++++++++++++++
 3 files changed, 125 insertions(+), 20 deletions(-)

diff --git a/src/bin/scripts/t/100_vacuumdb.pl b/src/bin/scripts/t/100_vacuumdb.pl
index 1a2bcb4959..2d669391fe 100644
--- a/src/bin/scripts/t/100_vacuumdb.pl
+++ b/src/bin/scripts/t/100_vacuumdb.pl
@@ -127,9 +127,13 @@ $node->issues_sql_like(
 	qr/statement: VACUUM \(SKIP_DATABASE_STATS, ANALYZE\) public.vactable\(a, b\);/,
 	'vacuumdb --analyze with complete column list');
 $node->issues_sql_like(
+	[ 'vacuumdb', '--analyze-only', '--force-analyze', '--table', 'vactable(b)', 'postgres' ],
+	qr/statement: ANALYZE public.vactable\(b\);/,
+	'vacuumdb --analyze-only --force-analyze with partial column list');
+$node->issues_sql_unlike(
 	[ 'vacuumdb', '--analyze-only', '--table', 'vactable(b)', 'postgres' ],
 	qr/statement: ANALYZE public.vactable\(b\);/,
-	'vacuumdb --analyze-only with partial column list');
+	'vacuumdb --analyze-only --force-analyze with partial column list skipping vacuumed tables');
 $node->command_checks_all(
 	[ 'vacuumdb', '--analyze', '--table', 'vacview', 'postgres' ],
 	0,
diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index b13f3c4224..1aa5c46af5 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -25,6 +25,7 @@
 #include "fe_utils/query_utils.h"
 #include "fe_utils/simple_list.h"
 #include "fe_utils/string_utils.h"
+#include "pqexpbuffer.h"
 
 
 /* vacuum options controlled by user flags */
@@ -47,6 +48,8 @@ typedef struct vacuumingOptions
 	bool		process_main;
 	bool		process_toast;
 	bool		skip_database_stats;
+	bool		analyze_in_stages;
+	bool		force_analyze;
 	char	   *buffer_usage_limit;
 } vacuumingOptions;
 
@@ -75,7 +78,6 @@ static void vacuum_one_database(ConnParams *cparams,
 
 static void vacuum_all_databases(ConnParams *cparams,
 								 vacuumingOptions *vacopts,
-								 bool analyze_in_stages,
 								 SimpleStringList *objects,
 								 int concurrentCons,
 								 const char *progname, bool echo, bool quiet);
@@ -140,6 +142,7 @@ main(int argc, char *argv[])
 		{"no-process-toast", no_argument, NULL, 11},
 		{"no-process-main", no_argument, NULL, 12},
 		{"buffer-usage-limit", required_argument, NULL, 13},
+		{"force-analyze", no_argument, NULL, 14},
 		{NULL, 0, NULL, 0}
 	};
 
@@ -156,7 +159,6 @@ main(int argc, char *argv[])
 	bool		echo = false;
 	bool		quiet = false;
 	vacuumingOptions vacopts;
-	bool		analyze_in_stages = false;
 	SimpleStringList objects = {NULL, NULL};
 	int			concurrentCons = 1;
 	int			tbl_count = 0;
@@ -170,6 +172,8 @@ main(int argc, char *argv[])
 	vacopts.do_truncate = true;
 	vacopts.process_main = true;
 	vacopts.process_toast = true;
+	vacopts.force_analyze = false;
+	vacopts.analyze_in_stages = false;
 
 	pg_logging_init(argv[0]);
 	progname = get_progname(argv[0]);
@@ -251,7 +255,7 @@ main(int argc, char *argv[])
 				maintenance_db = pg_strdup(optarg);
 				break;
 			case 3:
-				analyze_in_stages = vacopts.analyze_only = true;
+				vacopts.analyze_in_stages = vacopts.analyze_only = true;
 				break;
 			case 4:
 				vacopts.disable_page_skipping = true;
@@ -287,6 +291,9 @@ main(int argc, char *argv[])
 			case 13:
 				vacopts.buffer_usage_limit = escape_quotes(optarg);
 				break;
+			case 14:
+				vacopts.force_analyze = true;
+				break;
 			default:
 				/* getopt_long already emitted a complaint */
 				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -372,6 +379,14 @@ main(int argc, char *argv[])
 		pg_fatal("cannot use the \"%s\" option with the \"%s\" option",
 				 "buffer-usage-limit", "full");
 
+	/*
+	 * --force-analyze is only valid when used with --analyze-only, -analyze,
+	 * or --analyze-in-stages
+	 */
+	if (vacopts.force_analyze && !vacopts.analyze_only && !vacopts.analyze_in_stages)
+		pg_fatal("can only use the \"%s\" option with \"%s\" or \"%s\"",
+				 "--force-analyze", "-Z/--analyze-only", "--analyze-in-stages");
+
 	/* fill cparams except for dbname, which is set below */
 	cparams.pghost = host;
 	cparams.pgport = port;
@@ -390,7 +405,6 @@ main(int argc, char *argv[])
 		cparams.dbname = maintenance_db;
 
 		vacuum_all_databases(&cparams, &vacopts,
-							 analyze_in_stages,
 							 &objects,
 							 concurrentCons,
 							 progname, echo, quiet);
@@ -413,20 +427,27 @@ main(int argc, char *argv[])
 		}
 
 		cparams.dbname = dbname;
-		stage = (analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
+		stage = (vacopts.analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
 
 		conn = connectDatabase(&cparams, progname, echo, false, true);
 		check_conn_options(conn, &vacopts);
 		print_processing_notice(conn, stage, progname, quiet);
 		found_objects = generate_catalog_list(conn, &vacopts, &objects, echo, &ntup);
+		vacuum_one_database(&cparams, &vacopts, stage,
+							&objects,
+							concurrentCons,
+							conn,
+							found_objects,
+							ntup,
+							progname, echo, quiet);
 
-		if (analyze_in_stages)
+		if (stage != ANALYZE_NO_STAGE)
 		{
-			for (stage = 0; stage < ANALYZE_NUM_STAGES; stage++)
+			for (stage = 1; stage < ANALYZE_NUM_STAGES; stage++)
 			{
 				/* the last pass disconnected the conn */
-				if (stage > 0)
-					conn = connectDatabase(&cparams, progname, echo, false, true);
+				conn = connectDatabase(&cparams, progname, echo, false, true);
+				print_processing_notice(conn, stage, progname, quiet);
 
 				vacuum_one_database(&cparams, &vacopts, stage,
 									&objects,
@@ -437,14 +458,6 @@ main(int argc, char *argv[])
 									progname, echo, quiet);
 			}
 		}
-		else
-			vacuum_one_database(&cparams, &vacopts, stage,
-								&objects,
-								concurrentCons,
-								conn,
-								found_objects,
-								ntup,
-								progname, echo, quiet);
 	}
 
 	exit(0);
@@ -763,6 +776,47 @@ generate_catalog_list(PGconn *conn,
 						  vacopts->min_mxid_age);
 	}
 
+	/*
+	 * If this query is for an analyze-only or analyze-in-stages, two
+	 * upgrade-centric operations, and force-analyze is NOT set, then
+	 * exclude any relations that already have their full compliment
+	 * of attribute stats and extended stats.
+	 *
+	 *
+	 */
+	if ((vacopts->analyze_only || vacopts->analyze_in_stages) &&
+		!vacopts->force_analyze)
+	{
+		/*
+		 * The pg_class in question has no pg_statistic rows representing
+		 * user-visible columns that lack a corresponding pg_statitic row.
+		 * Currently no differentiation is made for whether the
+		 * pg_statistic.stainherit is true or false.
+		 */
+		appendPQExpBufferStr(&catalog_query,
+							 " AND NOT EXISTS (\n"
+							 " SELECT NULL FROM pg_catalog.pg_attribute AS a\n"
+							 " WHERE a.attrelid OPERATOR(pg_catalog.=) c.oid\n"
+							 " AND a.attnum OPERATOR(pg_catalog.>) 0 AND NOT a.attisdropped\n"
+							 " AND NOT EXISTS (\n"
+							 " SELECT NULL FROM pg_catalog.pg_statistic AS s\n"
+							 " WHERE s.starelid OPERATOR(pg_catalog.=) c.oid\n"
+							 " AND s.staattnum OPERATOR(pg_catalog.=) a.attnum))\n");
+
+		/*
+		 * The pg_class entry has no pg_statistic_ext rows that lack a corresponding
+		 * pg_statistic_ext_data row. Currently no differentiation is made for whether
+		 * pg_statistic_exta_data.stxdinherit is true or false.
+		 */
+		appendPQExpBufferStr(&catalog_query,
+							 " AND NOT EXISTS (\n"
+							 " SELECT NULL FROM pg_catalog.pg_statistic_ext AS e\n"
+							 " WHERE e.stxrelid OPERATOR(pg_catalog.=) c.oid\n"
+							 " AND NOT EXISTS (\n"
+							 " SELECT NULL FROM pg_catalog.pg_statistic_ext_data AS d\n"
+							 " WHERE d.stxoid OPERATOR(pg_catalog.=) e.oid))\n");
+	}
+
 	/*
 	 * Execute the catalog query.  We use the default search_path for this
 	 * query for consistency with table lookups done elsewhere by the user.
@@ -959,7 +1013,6 @@ finish:
 static void
 vacuum_all_databases(ConnParams *cparams,
 					 vacuumingOptions *vacopts,
-					 bool analyze_in_stages,
 					 SimpleStringList *objects,
 					 int concurrentCons,
 					 const char *progname, bool echo, bool quiet)
@@ -972,7 +1025,7 @@ vacuum_all_databases(ConnParams *cparams,
 	SimpleStringList  **found_objects;
 	int				   *num_tuples;
 
-	stage = (analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
+	stage = (vacopts->analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
 
 	conn = connectMaintenanceDatabase(cparams, progname, echo);
 	result = executeQuery(conn,
diff --git a/doc/src/sgml/ref/vacuumdb.sgml b/doc/src/sgml/ref/vacuumdb.sgml
index 66fccb30a2..00ec927606 100644
--- a/doc/src/sgml/ref/vacuumdb.sgml
+++ b/doc/src/sgml/ref/vacuumdb.sgml
@@ -425,6 +425,12 @@ PostgreSQL documentation
        <para>
         Only calculate statistics for use by the optimizer (no vacuum).
        </para>
+       <para>
+        By default, this operation excludes relations that already have
+        statistics generated. If the option <option>--force-analyze</option>
+        is also specified, then relations with existing stastistics are not
+        excluded.
+       </para>
       </listitem>
      </varlistentry>
 
@@ -439,6 +445,47 @@ PostgreSQL documentation
         to produce usable statistics faster, and subsequent stages build the
         full statistics.
        </para>
+       <para>
+        This option is intended to be run after a <command>pg_upgrade</command>
+        to generate statistics for relations that have no stistatics or incomplete
+        statistics (such as those with extended statistics objects, which are not
+        imported on upgrade).
+       </para>
+       <para>
+        If the option <option>--force-analyze</option> is also specified, then
+        relations with existing stastistics are not excluded.
+        This option is only useful to analyze a database that currently has
+        no statistics or has wholly incorrect ones, such as if it is newly
+        populated from a restored dump.
+        Be aware that running with this option combinationin a database with
+        existing statistics may cause the query optimizer choices to become
+        transiently worse due to the low statistics targets of the early
+        stages.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--force-analyze</option></term>
+      <listitem>
+       <para>
+        This option can only be used if either <option>--analyze-only</option>
+        or <option>--analyze-in-stages</option> is specified. It modifies those
+        options to not filter out relations that already have statistics.
+       </para>
+       <para>
+
+        Only calculate statistics for use by the optimizer (no vacuum),
+        like <option>--analyze-only</option>.  Run three
+        stages of analyze; the first stage uses the lowest possible statistics
+        target (see <xref linkend="guc-default-statistics-target"/>)
+        to produce usable statistics faster, and subsequent stages build the
+        full statistics.
+       </para>
+
+       <para>
+        This option was created
+       </para>
 
        <para>
         This option is only useful to analyze a database that currently has
@@ -452,6 +499,7 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+
      <varlistentry>
        <term><option>-?</option></term>
        <term><option>--help</option></term>
-- 
2.47.1

v35-0011-Add-issues_sql_unlike-opposite-of-issues_sql_lik.patchtext/x-patch; charset=US-ASCII; name=v35-0011-Add-issues_sql_unlike-opposite-of-issues_sql_lik.patchDownload
From 4fd29dd1ff736b6508f85e63273c0fb4fdde8625 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 7 Nov 2024 18:06:52 -0500
Subject: [PATCH v35 11/12] Add issues_sql_unlike, opposite of issues_sql_like

This is the same as issues_sql_like(), but the specified text is
prohibited from being in the output rather than required.

This became necessary to test that a command-line filter was in fact
filtering out certain output that a prior test required.
---
 src/test/perl/PostgreSQL/Test/Cluster.pm | 25 ++++++++++++++++++++++++
 1 file changed, 25 insertions(+)

diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index 508e5e3917..1281071479 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -2799,6 +2799,31 @@ sub issues_sql_like
 }
 
 =pod
+=item $node->issues_sql_unlike(cmd, prohibited_sql, test_name)
+
+Run a command on the node, then verify that $prohibited_sql does not appear
+in the server log file.
+
+=cut
+
+sub issues_sql_unlike
+{
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+	my ($self, $cmd, $prohibited_sql, $test_name) = @_;
+
+	local %ENV = $self->_get_env();
+
+	my $log_location = -s $self->logfile;
+
+	my $result = PostgreSQL::Test::Utils::run_log($cmd);
+	ok($result, "@$cmd exit code 0");
+	my $log =
+	  PostgreSQL::Test::Utils::slurp_file($self->logfile, $log_location);
+	unlike($log, $prohibited_sql, "$test_name: SQL found in server log");
+	return;
+}
+
 
 =item $node->log_content()
 
-- 
2.47.1

v35-0010-preserve-catalog-lists-across-staged-runs.patchtext/x-patch; charset=US-ASCII; name=v35-0010-preserve-catalog-lists-across-staged-runs.patchDownload
From c4b6a5d869f255ee9f5d853a25cdb2ee225c9193 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 6 Nov 2024 21:30:49 -0500
Subject: [PATCH v35 10/12] preserve catalog lists across staged runs

---
 src/bin/scripts/vacuumdb.c | 113 ++++++++++++++++++++++++++-----------
 1 file changed, 80 insertions(+), 33 deletions(-)

diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index 36f4796db0..b13f3c4224 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -68,6 +68,9 @@ static void vacuum_one_database(ConnParams *cparams,
 								int stage,
 								SimpleStringList *objects,
 								int concurrentCons,
+								PGconn *conn,
+								SimpleStringList *dbtables,
+								int ntups,
 								const char *progname, bool echo, bool quiet);
 
 static void vacuum_all_databases(ConnParams *cparams,
@@ -83,6 +86,14 @@ static void prepare_vacuum_command(PQExpBuffer sql, int serverVersion,
 static void run_vacuum_command(PGconn *conn, const char *sql, bool echo,
 							   const char *table);
 
+static void check_conn_options(PGconn *conn, vacuumingOptions *vacopts);
+
+static void
+print_processing_notice(PGconn *conn, int stage, const char *progname, bool quiet);
+
+static SimpleStringList * generate_catalog_list(PGconn *conn, vacuumingOptions *vacopts,
+												SimpleStringList *objects, bool echo, int *ntups);
+
 static void help(const char *progname);
 
 void		check_objfilter(void);
@@ -386,6 +397,11 @@ main(int argc, char *argv[])
 	}
 	else
 	{
+		PGconn	   *conn;
+		int			ntup;
+		SimpleStringList   *found_objects;
+		int			stage;
+
 		if (dbname == NULL)
 		{
 			if (getenv("PGDATABASE"))
@@ -397,25 +413,37 @@ main(int argc, char *argv[])
 		}
 
 		cparams.dbname = dbname;
+		stage = (analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
+
+		conn = connectDatabase(&cparams, progname, echo, false, true);
+		check_conn_options(conn, &vacopts);
+		print_processing_notice(conn, stage, progname, quiet);
+		found_objects = generate_catalog_list(conn, &vacopts, &objects, echo, &ntup);
 
 		if (analyze_in_stages)
 		{
-			int			stage;
-
 			for (stage = 0; stage < ANALYZE_NUM_STAGES; stage++)
 			{
-				vacuum_one_database(&cparams, &vacopts,
-									stage,
+				/* the last pass disconnected the conn */
+				if (stage > 0)
+					conn = connectDatabase(&cparams, progname, echo, false, true);
+
+				vacuum_one_database(&cparams, &vacopts, stage,
 									&objects,
 									concurrentCons,
+									conn,
+									found_objects,
+									ntup,
 									progname, echo, quiet);
 			}
 		}
 		else
-			vacuum_one_database(&cparams, &vacopts,
-								ANALYZE_NO_STAGE,
+			vacuum_one_database(&cparams, &vacopts, stage,
 								&objects,
 								concurrentCons,
+								conn,
+								found_objects,
+								ntup,
 								progname, echo, quiet);
 	}
 
@@ -791,16 +819,17 @@ vacuum_one_database(ConnParams *cparams,
 					int stage,
 					SimpleStringList *objects,
 					int concurrentCons,
+					PGconn *conn,
+					SimpleStringList *dbtables,
+					int ntups,
 					const char *progname, bool echo, bool quiet)
 {
 	PQExpBufferData sql;
-	PGconn	   *conn;
 	SimpleStringListCell *cell;
 	ParallelSlotArray *sa;
-	int			ntups;
 	bool		failed = false;
 	const char *initcmd;
-	SimpleStringList *dbtables;
+
 	const char *stage_commands[] = {
 		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
 		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
@@ -810,13 +839,6 @@ vacuum_one_database(ConnParams *cparams,
 	Assert(stage == ANALYZE_NO_STAGE ||
 		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
 
-	conn = connectDatabase(cparams, progname, echo, false, true);
-
-	check_conn_options(conn, vacopts);
-	print_processing_notice(conn, stage, progname, quiet);
-
-	dbtables = generate_catalog_list(conn, vacopts, objects, echo, &ntups);
-
 	/*
 	 * If no rows are returned, there are no matching tables, so we are done.
 	 */
@@ -928,7 +950,7 @@ finish:
 }
 
 /*
- * Vacuum/analyze all connectable databases.
+ * Vacuum/analyze all ccparams->override_dbname = PQgetvalue(result, i, 0);onnectable databases.
  *
  * In analyze-in-stages mode, we process all databases in one stage before
  * moving on to the next stage.  That ensure minimal stats are available
@@ -944,8 +966,13 @@ vacuum_all_databases(ConnParams *cparams,
 {
 	PGconn	   *conn;
 	PGresult   *result;
-	int			stage;
 	int			i;
+	int			stage;
+
+	SimpleStringList  **found_objects;
+	int				   *num_tuples;
+
+	stage = (analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
 
 	conn = connectMaintenanceDatabase(cparams, progname, echo);
 	result = executeQuery(conn,
@@ -953,7 +980,33 @@ vacuum_all_databases(ConnParams *cparams,
 						  echo);
 	PQfinish(conn);
 
-	if (analyze_in_stages)
+	/*
+	 * connect to each database, check validity of options,
+	 * build the list of found objects per database,
+	 * and run the first/only vacuum stage
+	 */
+	found_objects = palloc(PQntuples(result) * sizeof(SimpleStringList *));
+	num_tuples = palloc(PQntuples(result) * sizeof (int));
+
+	for (i = 0; i < PQntuples(result); i++)
+	{
+		cparams->override_dbname = PQgetvalue(result, i, 0);
+		conn = connectDatabase(cparams, progname, echo, false, true);
+		check_conn_options(conn, vacopts);
+		print_processing_notice(conn, stage, progname, quiet);
+		found_objects[i] = generate_catalog_list(conn, vacopts, objects, echo, &num_tuples[i]);
+
+		vacuum_one_database(cparams, vacopts,
+							stage,
+							objects,
+							concurrentCons,
+							conn,
+							found_objects[i],
+							num_tuples[i],
+							progname, echo, quiet);
+	}
+
+	if (stage != ANALYZE_NO_STAGE)
 	{
 		/*
 		 * When analyzing all databases in stages, we analyze them all in the
@@ -963,35 +1016,29 @@ vacuum_all_databases(ConnParams *cparams,
 		 * This means we establish several times as many connections, but
 		 * that's a secondary consideration.
 		 */
-		for (stage = 0; stage < ANALYZE_NUM_STAGES; stage++)
+		for (stage = 1; stage < ANALYZE_NUM_STAGES; stage++)
 		{
 			for (i = 0; i < PQntuples(result); i++)
 			{
 				cparams->override_dbname = PQgetvalue(result, i, 0);
+				conn = connectDatabase(cparams, progname, echo, false, true);
+				print_processing_notice(conn, stage, progname, quiet);
 
 				vacuum_one_database(cparams, vacopts,
 									stage,
 									objects,
 									concurrentCons,
+									conn,
+									found_objects[i],
+									num_tuples[i],
 									progname, echo, quiet);
 			}
 		}
 	}
-	else
-	{
-		for (i = 0; i < PQntuples(result); i++)
-		{
-			cparams->override_dbname = PQgetvalue(result, i, 0);
-
-			vacuum_one_database(cparams, vacopts,
-								ANALYZE_NO_STAGE,
-								objects,
-								concurrentCons,
-								progname, echo, quiet);
-		}
-	}
 
 	PQclear(result);
+	pfree(found_objects);
+	pfree(num_tuples);
 }
 
 /*
-- 
2.47.1

#261Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#260)
1 attachment(s)
Re: Statistics Import and Export

On Fri, 2024-12-13 at 00:22 -0500, Corey Huinker wrote:

Per offline conversation with Jeff, adding a --no-schema to pg_dump
option both for completeness (we already have --no-data and --no-
statistics), but users who previously got the effect of --no-schema
did so by specifying --data-only, which suppresses statistics as
well.

0001-0005 - changes to pg_dump/pg_upgrade

Attached is a version 36j where I consolidated these patches and
cleaned up the documentation. It doesn't make a lot of sense to commit
them separately, because as soon as the pg_dump changes are there, the
pg_upgrade test starts showing a difference until it starts using the -
-no-data option.

The biggest functional change is the way dependencies are handled for
matview stats. Materialized views ordinarily end up in
SECITON_PRE_DATA, but in some cases they can be postponed to
SECTION_POST_DATA. You solved that by always putting the matview stats
in SECTION_POST_DATA.

I took a different approach here and, when the matview is postponed,
also postpone the matview stats. It's slightly more code, but it felt
closer to the rest of the structure, where postponing is a special case
(that we might be able to remove in the future).

Regards,
Jeff Davis

Attachments:

v36j-0001-Dump-table-index-stats-in-pg_dump-and-transfer-.patchtext/x-patch; charset=UTF-8; name=v36j-0001-Dump-table-index-stats-in-pg_dump-and-transfer-.patchDownload
From 136ed6586bed7b9cd4415fc882bb30a19fa2063e Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH v36j] Dump table/index stats in pg_dump and transfer in
 pg_upgrade.

For each table/matview/index dumped, pg_dump will generate a statement
that calls pg_set_relation_stats(), and a series of statements that
call pg_set_attribute_stats(), one per attribute.

During restore, these statements will recreate the statistics of the
source system in the destination system.

For pg_dump, adds the command-line options --statistics-only (-X),
--no-schema, --no-statistics, and --no-data to enable the various
combinations of schema, statistics, and data.

Table statistics are dumped in the data section. Index and
Materialized View statistics are dumped in the post-data section.

Add options --with-statistics/--no-statistics to pg_upgrade to
enable/disable transferring of statistics to the upgraded cluster. The
default is --with-statistics.

Author: Corey Huinker
Discussion: https://postgr.es/m/CADkLM=fyJ-Y-DNk1aW09btZYdXDXS79xT8oFPTQ6sspWHaqdog@mail.gmail.com
---
 doc/src/sgml/ref/pg_dump.sgml        |  77 ++++--
 doc/src/sgml/ref/pg_dumpall.sgml     |  38 +++
 doc/src/sgml/ref/pg_restore.sgml     |  47 +++-
 doc/src/sgml/ref/pgupgrade.sgml      |  17 ++
 src/bin/pg_dump/pg_backup.h          |  10 +-
 src/bin/pg_dump/pg_backup_archiver.c |   8 +
 src/bin/pg_dump/pg_dump.c            | 382 ++++++++++++++++++++++++++-
 src/bin/pg_dump/pg_dump.h            |   9 +
 src/bin/pg_dump/pg_dump_sort.c       |  33 ++-
 src/bin/pg_dump/pg_dumpall.c         |   5 +
 src/bin/pg_dump/pg_restore.c         |  27 +-
 src/bin/pg_dump/t/001_basic.pl       |  18 ++
 src/bin/pg_upgrade/dump.c            |   6 +-
 src/bin/pg_upgrade/option.c          |  12 +
 src/bin/pg_upgrade/pg_upgrade.h      |   1 +
 src/tools/pgindent/typedefs.list     |   1 +
 16 files changed, 655 insertions(+), 36 deletions(-)

diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index d66e901f51b..9340aa70a77 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -123,14 +123,9 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Dump only the data, not the schema (data definitions).
+        Dump only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are dumped.
        </para>
-
-       <para>
-        This option is similar to, but for historical reasons not identical
-        to, specifying <option>--section=data</option>.
-       </para>
       </listitem>
      </varlistentry>
 
@@ -141,13 +136,11 @@ PostgreSQL documentation
       <listitem>
        <para>
         Include large objects in the dump.  This is the default behavior
-        except when <option>--schema</option>, <option>--table</option>, or
-        <option>--schema-only</option> is specified.  The <option>-b</option>
-        switch is therefore only useful to add large objects to dumps
-        where a specific schema or table has been requested.  Note that
-        large objects are considered data and therefore will be included when
-        <option>--data-only</option> is used, but not
-        when <option>--schema-only</option> is.
+        except when <option>--schema</option>, <option>--table</option>,
+        <option>--schema-only</option>, <option>--statistics-only</option>, or
+        <option>--no-data</option> is specified.  The <option>-b</option>
+        switch is therefore only useful to add large objects to dumps where a
+        specific schema or table has been requested.
        </para>
       </listitem>
      </varlistentry>
@@ -516,10 +509,11 @@ PostgreSQL documentation
       <term><option>--schema-only</option></term>
       <listitem>
        <para>
-        Dump only the object definitions (schema), not data.
+        Dump only the object definitions (schema), not data or statistics.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive to <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
         <option>--section=pre-data --section=post-data</option>.
@@ -652,6 +646,17 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Dump only the statistics, not the schema (data definitions) or data.
+        Statistics for tables, materialized views, and indexes are dumped.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-Z <replaceable class="parameter">level</replaceable></option></term>
       <term><option>-Z <replaceable class="parameter">method</replaceable></option>[:<replaceable>detail</replaceable>]</term>
@@ -824,16 +829,17 @@ PostgreSQL documentation
       <term><option>--exclude-table-data=<replaceable class="parameter">pattern</replaceable></option></term>
       <listitem>
        <para>
-        Do not dump data for any tables matching <replaceable
-        class="parameter">pattern</replaceable>. The pattern is
-        interpreted according to the same rules as for <option>-t</option>.
+        Do not dump data or statistics for any tables matching <replaceable
+        class="parameter">pattern</replaceable>. The pattern is interpreted
+        according to the same rules as for <option>-t</option>.
         <option>--exclude-table-data</option> can be given more than once to
-        exclude tables matching any of several patterns. This option is
-        useful when you need the definition of a particular table even
-        though you do not need the data in it.
+        exclude tables matching any of several patterns. This option is useful
+        when you need the definition of a particular table even though you do
+        not need the data in it.
        </para>
        <para>
-        To exclude data for all tables in the database, see <option>--schema-only</option>.
+        To exclude data for all tables in the database, see <option>--schema-only</option>
+        or <option>--statistics-only</option>.
        </para>
       </listitem>
      </varlistentry>
@@ -1080,6 +1086,15 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-data</option></term>
+      <listitem>
+       <para>
+        Do not dump data.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-publications</option></term>
       <listitem>
@@ -1089,6 +1104,15 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-schema</option></term>
+      <listitem>
+       <para>
+        Do not dump schema (data definitions).
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-security-labels</option></term>
       <listitem>
@@ -1098,6 +1122,15 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_dumpall.sgml b/doc/src/sgml/ref/pg_dumpall.sgml
index 014f2792589..d423153a93a 100644
--- a/doc/src/sgml/ref/pg_dumpall.sgml
+++ b/doc/src/sgml/ref/pg_dumpall.sgml
@@ -265,6 +265,17 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Dump only the statistics, not the schema (data definitions) or data.
+        Statistics for tables, materialized views, and indexes are dumped.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--binary-upgrade</option></term>
       <listitem>
@@ -422,6 +433,15 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-data</option></term>
+      <listitem>
+       <para>
+        Do not dump data.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-publications</option></term>
       <listitem>
@@ -447,6 +467,15 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-schema</option></term>
+      <listitem>
+       <para>
+        Do not dump schema (data definitions).
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-security-labels</option></term>
       <listitem>
@@ -456,6 +485,15 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index b8b27e1719e..3c381db1aa7 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -94,7 +94,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Restore only the data, not the schema (data definitions).
+        Restore only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are restored,
         if present in the archive.
        </para>
@@ -483,10 +483,11 @@ PostgreSQL documentation
         to the extent that schema entries are present in the archive.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive of <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
-        <option>--section=pre-data --section=post-data</option>.
+        <option>--section=pre-data --section=post-data --no-statistics</option>.
        </para>
        <para>
         (Do not confuse this with the <option>--schema</option> option, which
@@ -599,6 +600,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Restore only the statistics, not schema (data definitions) or data.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-1</option></term>
       <term><option>--single-transaction</option></term>
@@ -681,6 +692,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-data</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore data, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-data-for-failed-tables</option></term>
       <listitem>
@@ -713,6 +734,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-schema</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore schema (data definitions), even if
+        the archive contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-security-labels</option></term>
       <listitem>
@@ -723,6 +754,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore statistics, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pgupgrade.sgml b/doc/src/sgml/ref/pgupgrade.sgml
index 4777381dac2..0a36db40e97 100644
--- a/doc/src/sgml/ref/pgupgrade.sgml
+++ b/doc/src/sgml/ref/pgupgrade.sgml
@@ -145,6 +145,23 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--with-statistics</option></term>
+      <listitem>
+       <para>
+        Restore statistics from the old cluster into the new cluster.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not restore statistics from the old cluster into the new cluster.
+       </para>
+      </listitem>
+     </varlistentry>
      <varlistentry>
       <term><option>-o</option> <replaceable class="parameter">options</replaceable></term>
       <term><option>--old-options</option> <replaceable class="parameter">options</replaceable></term>
diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index f0f19bb0b29..6e17bd28934 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -110,8 +110,11 @@ typedef struct _restoreOptions
 	int			column_inserts;
 	int			if_exists;
 	int			no_comments;	/* Skip comments */
+	int			no_data;		/* Skip data */
 	int			no_publications;	/* Skip publication entries */
+	int			no_schema;		/* Skip schema generation */
 	int			no_security_labels; /* Skip security label entries */
+	int			no_statistics;	/* Skip statistics import */
 	int			no_subscriptions;	/* Skip subscription entries */
 	int			strict_names;
 
@@ -160,6 +163,7 @@ typedef struct _restoreOptions
 	/* flags derived from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } RestoreOptions;
 
 typedef struct _dumpOptions
@@ -179,8 +183,11 @@ typedef struct _dumpOptions
 	int			column_inserts;
 	int			if_exists;
 	int			no_comments;
-	int			no_security_labels;
+	int			no_data;
 	int			no_publications;
+	int			no_schema;
+	int			no_security_labels;
+	int			no_statistics;
 	int			no_subscriptions;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
@@ -208,6 +215,7 @@ typedef struct _dumpOptions
 	/* flags derived from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } DumpOptions;
 
 /*
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 707a3fc844c..d62f419560d 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -186,6 +186,9 @@ dumpOptionsFromRestoreOptions(RestoreOptions *ropt)
 	dopt->no_publications = ropt->no_publications;
 	dopt->no_security_labels = ropt->no_security_labels;
 	dopt->no_subscriptions = ropt->no_subscriptions;
+	dopt->no_data = ropt->no_data;
+	dopt->no_schema = ropt->no_schema;
+	dopt->no_statistics = ropt->no_statistics;
 	dopt->lockWaitTimeout = ropt->lockWaitTimeout;
 	dopt->include_everything = ropt->include_everything;
 	dopt->enable_row_security = ropt->enable_row_security;
@@ -2962,6 +2965,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's a stats dump, maybe ignore it */
+	if (ropt->no_statistics && strcmp(te->desc, "STATISTICS") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2991,6 +2998,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 19969e400fc..e6eff0c3297 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -430,6 +430,7 @@ main(int argc, char **argv)
 	DataDirSyncMethod sync_method = DATA_DIR_SYNC_METHOD_FSYNC;
 	bool		data_only = false;
 	bool		schema_only = false;
+	bool		statistics_only = false;
 
 	static DumpOptions dopt;
 
@@ -466,6 +467,7 @@ main(int argc, char **argv)
 		{"encoding", required_argument, NULL, 'E'},
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
+		{"statistics-only", no_argument, NULL, 'X'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -492,8 +494,11 @@ main(int argc, char **argv)
 		{"strict-names", no_argument, &strict_names, 1},
 		{"use-set-session-authorization", no_argument, &dopt.use_setsessauth, 1},
 		{"no-comments", no_argument, &dopt.no_comments, 1},
+		{"no-data", no_argument, &dopt.no_data, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
+		{"no-schema", no_argument, &dopt.no_schema, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -539,7 +544,7 @@ main(int argc, char **argv)
 
 	InitDumpOptions(&dopt);
 
-	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:RsS:t:T:U:vwWxZ:",
+	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:PRsS:t:T:U:vwWxXZ:",
 							long_options, &optindex)) != -1)
 	{
 		switch (c)
@@ -613,6 +618,10 @@ main(int argc, char **argv)
 				dopt.cparams.pgport = pg_strdup(optarg);
 				break;
 
+			case 'X':			/* Dump statistics only */
+				statistics_only = true;
+				break;
+
 			case 'R':
 				/* no-op, still accepted for backwards compatibility */
 				break;
@@ -784,6 +793,17 @@ main(int argc, char **argv)
 
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (schema_only && statistics_only)
+		pg_fatal("options -s/--schema-only and -X/--statistics-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
+
+	if (data_only && dopt.no_data)
+		pg_fatal("options -a/--data-only and --no-data cannot be used together");
+	if (schema_only && dopt.no_schema)
+		pg_fatal("options -s/--schema-only and --no-schema cannot be used together");
+	if (statistics_only && dopt.no_statistics)
+		pg_fatal("options -X/--statistics-only and --no-statistics cannot be used together");
 
 	if (schema_only && foreign_servers_include_patterns.head != NULL)
 		pg_fatal("options -s/--schema-only and --include-foreign-data cannot be used together");
@@ -798,8 +818,9 @@ main(int argc, char **argv)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
 	/* set derivative flags */
-	dopt.dumpSchema = (!data_only);
-	dopt.dumpData = (!schema_only);
+	dopt.dumpData = data_only || (!schema_only && !statistics_only && !dopt.no_data);
+	dopt.dumpSchema = schema_only || (!data_only && !statistics_only && !dopt.no_schema);
+	dopt.dumpStatistics = statistics_only || (!data_only && !schema_only && !dopt.no_statistics);
 
 	/*
 	 * --inserts are already implied above if --column-inserts or
@@ -1099,6 +1120,7 @@ main(int argc, char **argv)
 	ropt->dropSchema = dopt.outputClean;
 	ropt->dumpData = dopt.dumpData;
 	ropt->dumpSchema = dopt.dumpSchema;
+	ropt->dumpStatistics = dopt.dumpStatistics;
 	ropt->if_exists = dopt.if_exists;
 	ropt->column_inserts = dopt.column_inserts;
 	ropt->dumpSections = dopt.dumpSections;
@@ -1177,7 +1199,7 @@ help(const char *progname)
 	printf(_("  -?, --help                   show this help, then exit\n"));
 
 	printf(_("\nOptions controlling the output content:\n"));
-	printf(_("  -a, --data-only              dump only the data, not the schema\n"));
+	printf(_("  -a, --data-only              dump only the data, not the schema or statistics\n"));
 	printf(_("  -b, --large-objects          include large objects in dump\n"));
 	printf(_("  --blobs                      (same as --large-objects, deprecated)\n"));
 	printf(_("  -B, --no-large-objects       exclude large objects in dump\n"));
@@ -1190,11 +1212,12 @@ help(const char *progname)
 	printf(_("  -N, --exclude-schema=PATTERN do NOT dump the specified schema(s)\n"));
 	printf(_("  -O, --no-owner               skip restoration of object ownership in\n"
 			 "                               plain-text format\n"));
-	printf(_("  -s, --schema-only            dump only the schema, no data\n"));
+	printf(_("  -s, --schema-only            dump only the schema, no data or statistics\n"));
 	printf(_("  -S, --superuser=NAME         superuser user name to use in plain-text format\n"));
 	printf(_("  -t, --table=PATTERN          dump only the specified table(s)\n"));
 	printf(_("  -T, --exclude-table=PATTERN  do NOT dump the specified table(s)\n"));
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
+	printf(_("  -X, --statistics-only        dump only the statistics, not schema or data\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
@@ -1219,8 +1242,11 @@ help(const char *progname)
 	printf(_("  --inserts                    dump data as INSERT commands, rather than COPY\n"));
 	printf(_("  --load-via-partition-root    load partitions via the root table\n"));
 	printf(_("  --no-comments                do not dump comment commands\n"));
+	printf(_("  --no-data                    do not dump data\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
+	printf(_("  --no-schema                  do not dump schema\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -6777,6 +6803,43 @@ getFuncs(Archive *fout)
 	destroyPQExpBuffer(query);
 }
 
+/*
+ * getRelationStatistics
+ *    register the statistics object as a dependent of the relation.
+ */
+static RelStatsInfo *
+getRelationStatistics(Archive *fout, DumpableObject *rel, char relkind)
+{
+	if (relkind == RELKIND_RELATION ||
+		relkind == RELKIND_PARTITIONED_TABLE ||
+		relkind == RELKIND_MATVIEW ||
+		relkind == RELKIND_INDEX ||
+		relkind == RELKIND_PARTITIONED_INDEX)
+	{
+		RelStatsInfo *info = pg_malloc0(sizeof(RelStatsInfo));
+		DumpableObject *dobj = &info->dobj;
+
+		dobj->objType = DO_REL_STATS;
+		dobj->catId.tableoid = 0;
+		dobj->catId.oid = 0;
+		AssignDumpId(dobj);
+		dobj->dependencies = (DumpId *) pg_malloc(sizeof(DumpId));
+		dobj->dependencies[0] = rel->dumpId;
+		dobj->nDeps = 1;
+		dobj->allocDeps = 1;
+		dobj->components |= DUMP_COMPONENT_STATISTICS;
+		dobj->dump = rel->dump;
+		dobj->name = pg_strdup(rel->name);
+		dobj->namespace = rel->namespace;
+		info->relkind = relkind;
+		info->postponed_def = false;
+
+		return info;
+	}
+
+	return NULL;
+}
+
 /*
  * getTables
  *	  read all the tables (no indexes) in the system catalogs,
@@ -7154,6 +7217,7 @@ getTables(Archive *fout, int *numTables)
 
 		/* Tables have data */
 		tblinfo[i].dobj.components |= DUMP_COMPONENT_DATA;
+		tblinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
 
 		/* Mark whether table has an ACL */
 		if (!PQgetisnull(res, i, i_relacl))
@@ -7202,6 +7266,8 @@ getTables(Archive *fout, int *numTables)
 				}
 			}
 		}
+		if (tblinfo[i].interesting)
+			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relkind);
 	}
 
 	if (query->len != 0)
@@ -7648,11 +7714,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 		for (int c = 0; c < numinds; c++, j++)
 		{
 			char		contype;
+			char		indexkind;
+			RelStatsInfo *relstats;
 
 			indxinfo[j].dobj.objType = DO_INDEX;
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -7675,7 +7744,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			{
 				NULL, NULL
 			};
+
+			if (indxinfo[j].parentidx == 0)
+				indexkind = RELKIND_INDEX;
+			else
+				indexkind = RELKIND_PARTITIONED_INDEX;
+
 			contype = *(PQgetvalue(res, j, i_contype));
+			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, indexkind);
 
 			if (contype == 'p' || contype == 'u' || contype == 'x')
 			{
@@ -7709,6 +7785,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				constrinfo->separate = true;
 
 				indxinfo[j].indexconstraint = constrinfo->dobj.dumpId;
+				if (relstats != NULL)
+					addObjectDependency(&relstats->dobj, constrinfo->dobj.dumpId);
 			}
 			else
 			{
@@ -10296,6 +10374,287 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * Tabular description of the parameters to pg_restore_relation_stats()
+ * param_name, param_type
+ */
+static const char *rel_stats_arginfo[][2] = {
+	{"relation", "regclass"},
+	{"version", "integer"},
+	{"relpages", "integer"},
+	{"reltuples", "real"},
+	{"relallvisible", "integer"},
+};
+
+/*
+ * Tabular description of the parameters to pg_restore_attribute_stats()
+ * param_name, param_type
+ */
+static const char *att_stats_arginfo[][2] = {
+	{"relation", "regclass"},
+	{"attname", "name"},
+	{"inherited", "boolean"},
+	{"version", "integer"},
+	{"null_frac", "float4"},
+	{"avg_width", "integer"},
+	{"n_distinct", "float4"},
+	{"most_common_vals", "text"},
+	{"most_common_freqs", "float4[]"},
+	{"histogram_bounds", "text"},
+	{"correlation", "float4"},
+	{"most_common_elems", "text"},
+	{"most_common_elem_freqs", "float4[]"},
+	{"elem_count_histogram", "float4[]"},
+	{"range_length_histogram", "text"},
+	{"range_empty_frac", "float4"},
+	{"range_bounds_histogram", "text"},
+};
+
+/*
+ * getRelStatsExportQuery --
+ *
+ * Generate a query that will fetch all relation (e.g. pg_class)
+ * stats for a given relation.
+ */
+static void
+getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "current_setting('server_version_num') AS version, "
+						 "c.relpages, c.reltuples, c.relallvisible "
+						 "FROM pg_class c "
+						 "JOIN pg_namespace n "
+						 "ON n.oid = c.relnamespace "
+						 "WHERE n.nspname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND c.relname = ");
+	appendStringLiteralAH(query, relname, fout);
+}
+
+/*
+ * getAttStatsExportQuery --
+ *
+ * Generate a query that will fetch all attribute (e.g. pg_statistic)
+ * stats for a given relation.
+ */
+static void
+getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "s.attname,"
+						 "s.inherited,"
+						 "current_setting('server_version_num') AS version, "
+						 "s.null_frac,"
+						 "s.avg_width,"
+						 "s.n_distinct,"
+						 "s.most_common_vals,"
+						 "s.most_common_freqs,"
+						 "s.histogram_bounds,"
+						 "s.correlation,"
+						 "s.most_common_elems,"
+						 "s.most_common_elem_freqs,"
+						 "s.elem_count_histogram,");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 "s.range_length_histogram,"
+							 "s.range_empty_frac,"
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(query,
+							 "NULL AS range_length_histogram,"
+							 "NULL AS range_empty_frac,"
+							 "NULL AS range_bounds_histogram ");
+
+	appendPQExpBufferStr(query,
+						 "FROM pg_stats s "
+						 "JOIN pg_namespace n "
+						 "ON n.nspname = s.schemaname "
+						 "JOIN pg_class c "
+						 "ON c.relname = s.tablename "
+						 "AND c.relnamespace = n.oid "
+						 "WHERE s.schemaname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND s.tablename = ");
+	appendStringLiteralAH(query, relname, fout);
+	appendPQExpBufferStr(query, " ORDER BY s.attname, s.inherited");
+}
+
+
+/*
+ * appendNamedArgument --
+ *
+ * Convenience routine for constructing parameters of the form:
+ * 'paraname', 'value'::type
+ */
+static void
+appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
+					const char *argval, const char *argtype)
+{
+	appendPQExpBufferStr(out, "\t");
+
+	appendStringLiteralAH(out, argname, fout);
+	appendPQExpBufferStr(out, ", ");
+
+	appendStringLiteralAH(out, argval, fout);
+	appendPQExpBuffer(out, "::%s", argtype);
+}
+
+/*
+ * appendRelStatsImport --
+ *
+ * Append a formatted pg_restore_relation_stats statement.
+ */
+static void
+appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	const char *sep = "";
+
+	if (PQntuples(res) == 0)
+		return;
+
+	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
+
+	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
+	{
+		const char *argname = rel_stats_arginfo[argno][0];
+		const char *argtype = rel_stats_arginfo[argno][1];
+		int			fieldno = PQfnumber(res, argname);
+
+		if (fieldno < 0)
+			pg_fatal("relation stats export query missing field '%s'",
+					 argname);
+
+		if (PQgetisnull(res, 0, fieldno))
+			continue;
+
+		appendPQExpBufferStr(out, sep);
+		appendNamedArgument(out, fout, argname, PQgetvalue(res, 0, fieldno), argtype);
+
+		sep = ",\n";
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+/*
+ * appendAttStatsImport --
+ *
+ * Append a series of formatted pg_restore_attribute_stats statements.
+ */
+static void
+appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	{
+		const char *sep = "";
+
+		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+		for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
+		{
+			const char *argname = att_stats_arginfo[argno][0];
+			const char *argtype = att_stats_arginfo[argno][1];
+			int			fieldno = PQfnumber(res, argname);
+
+			if (fieldno < 0)
+				pg_fatal("attribute stats export query missing field '%s'",
+						 argname);
+
+			if (PQgetisnull(res, rownum, fieldno))
+				continue;
+
+			appendPQExpBufferStr(out, sep);
+			appendNamedArgument(out, fout, argname, PQgetvalue(res, rownum, fieldno), argtype);
+			sep = ",\n";
+		}
+		appendPQExpBufferStr(out, "\n);\n");
+	}
+}
+
+/*
+ * Decide which section to use based on the relkind of the parent object.
+ *
+ * NB: materialized views may be postponed from SECTION_PRE_DATA to
+ * SECTION_POST_DATA to resolve some kinds of dependency problems. If so, the
+ * matview stats will also be postponed to SECTION_POST_DATA. See
+ * repairMatViewBoundaryMultiLoop().
+ */
+static teSection
+statisticsDumpSection(const RelStatsInfo *rsinfo)
+{
+	switch (rsinfo->relkind)
+	{
+		case RELKIND_RELATION:
+		case RELKIND_PARTITIONED_TABLE:
+		case RELKIND_MATVIEW:
+			return SECTION_DATA;
+		case RELKIND_INDEX:
+		case RELKIND_PARTITIONED_INDEX:
+			return SECTION_POST_DATA;
+		default:
+			pg_fatal("cannot dump statistics for relation kind '%c'",
+					 rsinfo->relkind);
+	}
+
+	return 0;					/* keep compiler quiet */
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	PGresult   *res;
+	PQExpBuffer query;
+	PQExpBuffer out;
+	PQExpBuffer tag;
+	DumpableObject *dobj = (DumpableObject *) &rsinfo->dobj;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", "STATISTICS DATA", fmtId(dobj->name));
+
+	query = createPQExpBuffer();
+	out = createPQExpBuffer();
+
+	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendRelStatsImport(out, fout, res);
+	PQclear(res);
+
+	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendAttStatsImport(out, fout, res);
+	PQclear(res);
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATISTICS DATA",
+							  .section = rsinfo->postponed_def ?
+							  SECTION_POST_DATA : statisticsDumpSection(rsinfo),
+							  .createStmt = out->data,
+							  .deps = dobj->dependencies,
+							  .nDeps = dobj->nDeps));
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(out);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -10744,6 +11103,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_SUBSCRIPTION_REL:
 			dumpSubscriptionTable(fout, (const SubRelInfo *) dobj);
 			break;
+		case DO_REL_STATS:
+			dumpRelationStats(fout, (const RelStatsInfo *) dobj);
+			break;
 		case DO_PRE_DATA_BOUNDARY:
 		case DO_POST_DATA_BOUNDARY:
 			/* never dumped, nothing to do */
@@ -18970,6 +19332,16 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 				/* must come after the pre-data boundary */
 				addObjectDependency(dobj, preDataBound->dumpId);
 				break;
+			case DO_REL_STATS:
+				/* stats section varies by parent object type, DATA or POST */
+				if (statisticsDumpSection((RelStatsInfo *) dobj) == SECTION_DATA)
+				{
+					addObjectDependency(dobj, preDataBound->dumpId);
+					addObjectDependency(postDataBound, dobj->dumpId);
+				}
+				else
+					addObjectDependency(dobj, postDataBound->dumpId);
+				break;
 		}
 	}
 }
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 9c5ddd20cf7..b107a163d28 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -84,6 +84,7 @@ typedef enum
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION,
 	DO_SUBSCRIPTION_REL,		/* see note for SubRelInfo */
+	DO_REL_STATS,
 } DumpableObjectType;
 
 /*
@@ -109,6 +110,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
@@ -429,6 +431,13 @@ typedef struct _indexAttachInfo
 	IndxInfo   *partitionIdx;	/* link to index on partition */
 } IndexAttachInfo;
 
+typedef struct _relStatsInfo
+{
+	DumpableObject dobj;
+	char		relkind;		/* 'r', 'v', 'c', etc */
+	bool		postponed_def;
+} RelStatsInfo;
+
 typedef struct _statsExtInfo
 {
 	DumpableObject dobj;
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 3c8f2eb808d..400236ca95e 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -801,11 +801,22 @@ repairMatViewBoundaryMultiLoop(DumpableObject *boundaryobj,
 {
 	/* remove boundary's dependency on object after it in loop */
 	removeObjectDependency(boundaryobj, nextobj->dumpId);
-	/* if that object is a matview, mark it as postponed into post-data */
+
+	/*
+	 * If that object is a matview or matview stats, mark it as postponed into
+	 * post-data.
+	 */
 	if (nextobj->objType == DO_TABLE)
 	{
 		TableInfo  *nextinfo = (TableInfo *) nextobj;
 
+		if (nextinfo->relkind == RELKIND_MATVIEW)
+			nextinfo->postponed_def = true;
+	}
+	else if (nextobj->objType == DO_REL_STATS)
+	{
+		RelStatsInfo *nextinfo = (RelStatsInfo *) nextobj;
+
 		if (nextinfo->relkind == RELKIND_MATVIEW)
 			nextinfo->postponed_def = true;
 	}
@@ -1018,6 +1029,21 @@ repairDependencyLoop(DumpableObject **loop,
 					{
 						DumpableObject *nextobj;
 
+						nextobj = (j < nLoop - 1) ? loop[j + 1] : loop[0];
+						repairMatViewBoundaryMultiLoop(loop[j], nextobj);
+						return;
+					}
+				}
+			}
+			else if (loop[i]->objType == DO_REL_STATS &&
+					 ((RelStatsInfo *) loop[i])->relkind == RELKIND_MATVIEW)
+			{
+				for (j = 0; j < nLoop; j++)
+				{
+					if (loop[j]->objType == DO_POST_DATA_BOUNDARY)
+					{
+						DumpableObject *nextobj;
+
 						nextobj = (j < nLoop - 1) ? loop[j + 1] : loop[0];
 						repairMatViewBoundaryMultiLoop(loop[j], nextobj);
 						return;
@@ -1500,6 +1526,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "POST-DATA BOUNDARY  (ID %d)",
 					 obj->dumpId);
 			return;
+		case DO_REL_STATS:
+			snprintf(buf, bufsize,
+					 "RELATION STATISTICS FOR %s  (ID %d OID %u)",
+					 obj->name, obj->dumpId, obj->catId.oid);
+			return;
 	}
 	/* shouldn't get here */
 	snprintf(buf, bufsize,
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 9a04e51c81a..62e2766c094 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -103,6 +103,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -172,6 +173,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -451,6 +453,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -666,6 +670,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 88ae39d938a..c98a8d08dd4 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -63,6 +63,7 @@ main(int argc, char **argv)
 	int			numWorkers = 1;
 	Archive    *AH;
 	char	   *inputFileSpec;
+	bool		statistics_only = false;
 	static int	disable_triggers = 0;
 	static int	enable_row_security = 0;
 	static int	if_exists = 0;
@@ -71,8 +72,11 @@ main(int argc, char **argv)
 	static int	outputNoTablespaces = 0;
 	static int	use_setsessauth = 0;
 	static int	no_comments = 0;
+	static int	no_data = 0;
 	static int	no_publications = 0;
+	static int	no_schema = 0;
 	static int	no_security_labels = 0;
+	static int	no_statistics = 0;
 	static int	no_subscriptions = 0;
 	static int	strict_names = 0;
 	bool		data_only = false;
@@ -108,6 +112,7 @@ main(int argc, char **argv)
 		{"username", 1, NULL, 'U'},
 		{"verbose", 0, NULL, 'v'},
 		{"single-transaction", 0, NULL, '1'},
+		{"statistics-only", no_argument, NULL, 'P'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -124,8 +129,11 @@ main(int argc, char **argv)
 		{"transaction-size", required_argument, NULL, 5},
 		{"use-set-session-authorization", no_argument, &use_setsessauth, 1},
 		{"no-comments", no_argument, &no_comments, 1},
+		{"no-data", no_argument, &no_data, 1},
 		{"no-publications", no_argument, &no_publications, 1},
+		{"no-schema", no_argument, &no_schema, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
 
@@ -271,6 +279,10 @@ main(int argc, char **argv)
 				opts->aclsSkip = 1;
 				break;
 
+			case 'X':			/* Restore statistics only */
+				statistics_only = true;
+				break;
+
 			case '1':			/* Restore data in a single transaction */
 				opts->single_txn = true;
 				opts->exit_on_error = true;
@@ -343,6 +355,10 @@ main(int argc, char **argv)
 
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
+	if (schema_only && statistics_only)
+		pg_fatal("options -s/--schema-only and -X/--statistics-only cannot be used together");
 
 	if (data_only && opts->dropSchema)
 		pg_fatal("options -c/--clean and -a/--data-only cannot be used together");
@@ -362,8 +378,9 @@ main(int argc, char **argv)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
 	/* set derivative flags */
-	opts->dumpSchema = (!data_only);
-	opts->dumpData = (!schema_only);
+	opts->dumpSchema = (!no_schema && !data_only && !statistics_only);
+	opts->dumpData = (!no_data && !schema_only && !statistics_only);
+	opts->dumpStatistics = (!no_statistics && !data_only && !schema_only);
 
 	opts->disable_triggers = disable_triggers;
 	opts->enable_row_security = enable_row_security;
@@ -375,6 +392,8 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
+	opts->no_data = no_data;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
@@ -484,6 +503,7 @@ usage(const char *progname)
 	printf(_("  -t, --table=NAME             restore named relation (table, view, etc.)\n"));
 	printf(_("  -T, --trigger=NAME           restore named trigger\n"));
 	printf(_("  -x, --no-privileges          skip restoration of access privileges (grant/revoke)\n"));
+	printf(_("  -X, --statistics-only        restore only the statistics, not schema or data\n"));
 	printf(_("  -1, --single-transaction     restore as a single transaction\n"));
 	printf(_("  --disable-triggers           disable triggers during data-only restore\n"));
 	printf(_("  --enable-row-security        enable row security\n"));
@@ -491,10 +511,13 @@ usage(const char *progname)
 			 "                               in FILENAME\n"));
 	printf(_("  --if-exists                  use IF EXISTS when dropping objects\n"));
 	printf(_("  --no-comments                do not restore comment commands\n"));
+	printf(_("  --no-data                    do not restore data\n"));
 	printf(_("  --no-data-for-failed-tables  do not restore data of tables that could not be\n"
 			 "                               created\n"));
 	printf(_("  --no-publications            do not restore publications\n"));
+	printf(_("  --no-schema                  do not restore schema\n"));
 	printf(_("  --no-security-labels         do not restore security labels\n"));
+	printf(_("  --no-statistics              do not restore statistics\n"));
 	printf(_("  --no-subscriptions           do not restore subscriptions\n"));
 	printf(_("  --no-table-access-method     do not restore table access methods\n"));
 	printf(_("  --no-tablespaces             do not restore tablespace assignments\n"));
diff --git a/src/bin/pg_dump/t/001_basic.pl b/src/bin/pg_dump/t/001_basic.pl
index b9d13a0e1de..76ebf15f55b 100644
--- a/src/bin/pg_dump/t/001_basic.pl
+++ b/src/bin/pg_dump/t/001_basic.pl
@@ -50,12 +50,30 @@ command_fails_like(
 	'pg_dump: options -s/--schema-only and -a/--data-only cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '-s', '-X' ],
+	qr/\Qpg_dump: error: options -s\/--schema-only and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: error: options -s/--schema-only and -X/--statistics-only cannot be used together'
+);
+
+command_fails_like(
+	[ 'pg_dump', '-a', '-X' ],
+	qr/\Qpg_dump: error: options -a\/--data-only and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: error: options -a/--data-only and -X/--statistics-only cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-s', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: options -s\/--schema-only and --include-foreign-data cannot be used together\E/,
 	'pg_dump: options -s/--schema-only and --include-foreign-data cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '--statistics-only', '--no-statistics' ],
+	qr/\Qpg_dump: error: options -X\/--statistics-only and --no-statistics cannot be used together\E/,
+	'pg_dump: options -X\/--statistics-only and --no-statistics cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-j2', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: option --include-foreign-data is not supported with parallel backup\E/,
diff --git a/src/bin/pg_upgrade/dump.c b/src/bin/pg_upgrade/dump.c
index 8345f55be8a..954e7bacf4f 100644
--- a/src/bin/pg_upgrade/dump.c
+++ b/src/bin/pg_upgrade/dump.c
@@ -21,10 +21,11 @@ generate_old_dump(void)
 
 	/* run new pg_dumpall binary for globals */
 	exec_prog(UTILITY_LOG_FILE, NULL, true, true,
-			  "\"%s/pg_dumpall\" %s --globals-only --quote-all-identifiers "
+			  "\"%s/pg_dumpall\" %s %s --globals-only --quote-all-identifiers "
 			  "--binary-upgrade %s --no-sync -f \"%s/%s\"",
 			  new_cluster.bindir, cluster_conn_opts(&old_cluster),
 			  log_opts.verbose ? "--verbose" : "",
+			  user_opts.do_statistics ? "" : "--no-statistics",
 			  log_opts.dumpdir,
 			  GLOBALS_DUMP_FILE);
 	check_ok();
@@ -52,10 +53,11 @@ generate_old_dump(void)
 		snprintf(log_file_name, sizeof(log_file_name), DB_DUMP_LOG_FILE_MASK, old_db->db_oid);
 
 		parallel_exec_prog(log_file_name, NULL,
-						   "\"%s/pg_dump\" %s --schema-only --quote-all-identifiers "
+						   "\"%s/pg_dump\" %s --no-data %s --quote-all-identifiers "
 						   "--binary-upgrade --format=custom %s --no-sync --file=\"%s/%s\" %s",
 						   new_cluster.bindir, cluster_conn_opts(&old_cluster),
 						   log_opts.verbose ? "--verbose" : "",
+						   user_opts.do_statistics ? "" : "--no-statistics",
 						   log_opts.dumpdir,
 						   sql_file_name, escaped_connstr.data);
 
diff --git a/src/bin/pg_upgrade/option.c b/src/bin/pg_upgrade/option.c
index 6f41d63eed4..e2f564f5d58 100644
--- a/src/bin/pg_upgrade/option.c
+++ b/src/bin/pg_upgrade/option.c
@@ -60,6 +60,8 @@ parseCommandLine(int argc, char *argv[])
 		{"copy", no_argument, NULL, 2},
 		{"copy-file-range", no_argument, NULL, 3},
 		{"sync-method", required_argument, NULL, 4},
+		{"with-statistics", no_argument, NULL, 5},
+		{"no-statistics", no_argument, NULL, 6},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -70,6 +72,7 @@ parseCommandLine(int argc, char *argv[])
 
 	user_opts.do_sync = true;
 	user_opts.transfer_mode = TRANSFER_MODE_COPY;
+	user_opts.do_statistics = true;
 
 	os_info.progname = get_progname(argv[0]);
 
@@ -212,6 +215,13 @@ parseCommandLine(int argc, char *argv[])
 				user_opts.sync_method = pg_strdup(optarg);
 				break;
 
+			case 5:
+				user_opts.do_statistics = true;
+				break;
+			case 6:
+				user_opts.do_statistics = false;
+				break;
+
 			default:
 				fprintf(stderr, _("Try \"%s --help\" for more information.\n"),
 						os_info.progname);
@@ -306,7 +316,9 @@ usage(void)
 	printf(_("  --clone                       clone instead of copying files to new cluster\n"));
 	printf(_("  --copy                        copy files to new cluster (default)\n"));
 	printf(_("  --copy-file-range             copy files to new cluster with copy_file_range\n"));
+	printf(_("  --no-statisttics              do not import statistics from old cluster\n"));
 	printf(_("  --sync-method=METHOD          set method for syncing files to disk\n"));
+	printf(_("  --with-statisttics            import statistics from old cluster (default)\n"));
 	printf(_("  -?, --help                    show this help, then exit\n"));
 	printf(_("\n"
 			 "Before running pg_upgrade you must:\n"
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index 53f693c2d4b..371f9a2d063 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -327,6 +327,7 @@ typedef struct
 	int			jobs;			/* number of processes/threads to use */
 	char	   *socketdir;		/* directory to use for Unix sockets */
 	char	   *sync_method;
+	bool		do_statistics;	/* carry over statistics from old cluster */
 } UserOpts;
 
 typedef struct
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index fbdb932e6b6..d5aa045db4f 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2393,6 +2393,7 @@ RelMapFile
 RelMapping
 RelOptInfo
 RelOptKind
+RelStatsInfo
 RelToCheck
 RelToCluster
 RelabelType
-- 
2.34.1

#262Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#261)
Re: Statistics Import and Export

The biggest functional change is the way dependencies are handled for
matview stats. Materialized views ordinarily end up in
SECITON_PRE_DATA, but in some cases they can be postponed to
SECTION_POST_DATA. You solved that by always putting the matview stats
in SECTION_POST_DATA.

Accurate.

I took a different approach here and, when the matview is postponed,
also postpone the matview stats. It's slightly more code, but it felt
closer to the rest of the structure, where postponing is a special case
(that we might be able to remove in the future).

+1. The fact that this quirk was a knock-on effect of the postponing-quirk,
which could go away, makes this change compelling.

#263Jeff Davis
pgsql@j-davis.com
In reply to: Jeff Davis (#261)
Re: Statistics Import and Export

On Thu, 2024-12-19 at 21:23 -0800, Jeff Davis wrote:

0001-0005 - changes to pg_dump/pg_upgrade

Attached is a version 36j...

The testing can use some work here. I noticed that if I take out the
stats entirely, the tests still pass, because pg_upgrade still gets the
same before/after result.

Also, we need some testing of the output and ordering of pg_dump.
Granted, in most cases problems would result in errors during the
reload. But we have those tests for other kinds of objects, so we
should have the tests for stats, too.

I like the description "STATISTICS DATA" because it differentiates from
the extended stats definitions. It might be worth differentiating
between "RELATION STATISTICS DATA" and "ATTRIBUTE STATISTICS DATA" but
I'm not sure if there's value in that.

But how did you determine what to use for the .tag and prefix? In the
output, it uses the form:

Name: STATISTICS DATA <name>; Type: STATISTICS DATA; ...

Should that be:

Name: <name>; Type: STATISTICS DATA; ...

Or:

Data for Name: ...; Name: ...; Type: STATISTICS DATA; ...

Or:

Statistics for Name: ...; Name: ...; Type: STATISTICS DATA; ...

Regards,
Jeff Davis

#264Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#263)
Re: Statistics Import and Export

I like the description "STATISTICS DATA" because it differentiates from

You have Tomas to thank for that:
/messages/by-id/bf724b21-914a-4497-84e3-49944f9776f6@enterprisedb.com

the extended stats definitions. It might be worth differentiating

between "RELATION STATISTICS DATA" and "ATTRIBUTE STATISTICS DATA" but
I'm not sure if there's value in that.

I have no objection to such a change, even if we don't currently have a use
for the differentiation, someone in the future might.

But how did you determine what to use for the .tag and prefix? In the
output, it uses the form:

It was the minimal change needed to meet Tomas's suggestion.

Statistics for Name: ...; Name: ...; Type: STATISTICS DATA; ...

I like this one best, because it clarifies the meaning of STATISTICS DATA.

#265Bruce Momjian
bruce@momjian.us
In reply to: Jeff Davis (#261)
Re: Statistics Import and Export

On Thu, Dec 19, 2024 at 09:23:20PM -0800, Jeff Davis wrote:

On Fri, 2024-12-13 at 00:22 -0500, Corey Huinker wrote:

Per offline conversation with Jeff, adding a --no-schema to pg_dump
option both for completeness (we already have --no-data and --no-
statistics), but users who previously got the effect of --no-schema
did so by specifying --data-only, which suppresses statistics as
well.

0001-0005 - changes to pg_dump/pg_upgrade

Attached is a version 36j where I consolidated these patches and
cleaned up the documentation. It doesn't make a lot of sense to commit
them separately, because as soon as the pg_dump changes are there, the
pg_upgrade test starts showing a difference until it starts using the -
-no-data option.

The biggest functional change is the way dependencies are handled for
matview stats. Materialized views ordinarily end up in
SECITON_PRE_DATA, but in some cases they can be postponed to
SECTION_POST_DATA. You solved that by always putting the matview stats
in SECTION_POST_DATA.

I took a different approach here and, when the matview is postponed,
also postpone the matview stats. It's slightly more code, but it felt
closer to the rest of the structure, where postponing is a special case
(that we might be able to remove in the future).

I am confused by this:

Add options --with-statistics/--no-statistics to pg_upgrade
to enable/disable transferring of statistics to the upgraded
cluster. The default is --with-statistics.

If statistics is the default for pg_upgrade, why would we need a
--with-statistics option?

Also, I see a misspelling:

+       printf(_("  --no-statisttics              do not import statistics from old cluster\n"));
                               --

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

Do not let urgent matters crowd out time for investment in the future.

#266Bruce Momjian
bruce@momjian.us
In reply to: Corey Huinker (#259)
Re: Statistics Import and Export

On Wed, Dec 11, 2024 at 10:49:53PM -0500, Corey Huinker wrote:

From cf4e731db9ffaa4e89d7c5d14b32668529c8c89a Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Fri, 8 Nov 2024 12:27:50 -0500
Subject: [PATCH v34 11/11] Add --force-analyze to vacuumdb.

The vacuumdb options of --analyze-in-stages and --analyze-only are often
used after a restore from a dump or a pg_upgrade to quickly rebuild
stats on a databse.

However, now that stats are imported in most (but not all) cases,
running either of these commands will be at least partially redundant,
and will overwrite the stats that were just imported, which is a big
POLA violation.

We could add a new option such as --analyze-missing-in-stages, but that
wouldn't help the userbase that grown accustomed to running
--analyze-in-stages after an upgrade.

The least-bad option to handle the situation is to change the behavior
of --analyze-only and --analyze-in-stages to only analyze tables which
were missing stats before the vacuumdb started, but offer the
--force-analyze flag to restore the old behavior for those who truly
wanted it.

I am _again_ not happy with this part of the patch. Please reply to the
criticism in my November 19th email:

/messages/by-id/Zz0T1BENIFDnXmwf@momjian.us

rather than ignoring it and posting the same version of the patch.

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

Do not let urgent matters crowd out time for investment in the future.

#267Jeff Davis
pgsql@j-davis.com
In reply to: Bruce Momjian (#266)
Re: Statistics Import and Export

On Thu, 2024-12-26 at 13:54 -0500, Bruce Momjian wrote:

I am _again_ not happy with this part of the patch.  Please reply to
the
criticism in my November 19th email:

        
/messages/by-id/Zz0T1BENIFDnXmwf@momjian.us

rather than ignoring it and posting the same version of the patch.

I suggest that we make a new thread about the vacuumdb changes and
focus this thread and patch series on the pg_dump changes (and minor
flag adjustments to pg_upgrade).

Unless you think that the pg_dump changes should block on the vacuumdb
changes? In which case please let me know because the pg_dump changes
are otherwise close to commit.

Regards,
Jeff Davis

#268Bruce Momjian
bruce@momjian.us
In reply to: Jeff Davis (#267)
Re: Statistics Import and Export

On Mon, Dec 30, 2024 at 12:02:47PM -0800, Jeff Davis wrote:

On Thu, 2024-12-26 at 13:54 -0500, Bruce Momjian wrote:

I am _again_ not happy with this part of the patch.  Please reply to
the
criticism in my November 19th email:

        
/messages/by-id/Zz0T1BENIFDnXmwf@momjian.us

rather than ignoring it and posting the same version of the patch.

I suggest that we make a new thread about the vacuumdb changes and
focus this thread and patch series on the pg_dump changes (and minor
flag adjustments to pg_upgrade).

Unless you think that the pg_dump changes should block on the vacuumdb
changes? In which case please let me know because the pg_dump changes
are otherwise close to commit.

I think that is a good idea. I don't see vacuumdb blocking this.

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

Do not let urgent matters crowd out time for investment in the future.

#269Nathan Bossart
nathandbossart@gmail.com
In reply to: Bruce Momjian (#268)
Re: Statistics Import and Export

On Mon, Dec 30, 2024 at 03:45:03PM -0500, Bruce Momjian wrote:

On Mon, Dec 30, 2024 at 12:02:47PM -0800, Jeff Davis wrote:

I suggest that we make a new thread about the vacuumdb changes and
focus this thread and patch series on the pg_dump changes (and minor
flag adjustments to pg_upgrade).

Unless you think that the pg_dump changes should block on the vacuumdb
changes? In which case please let me know because the pg_dump changes
are otherwise close to commit.

I think that is a good idea. I don't see vacuumdb blocking this.

+1, I've been reviewing the vacuumdb portion and am planning to start a new
thread in the near future. IIUC the bulk of the vacuumdb changes are
relatively noncontroversial, we just haven't reached consensus on the user
interface.

--
nathan

#270Corey Huinker
corey.huinker@gmail.com
In reply to: Nathan Bossart (#269)
11 attachment(s)
Re: Statistics Import and Export

+1, I've been reviewing the vacuumdb portion and am planning to start a new
thread in the near future. IIUC the bulk of the vacuumdb changes are
relatively noncontroversial, we just haven't reached consensus on the user
interface.

--
nathan

Attached is the latest (and probably last) unified patchset before parts
get spun off into their own threads.

0001 - This is the unified changes to pg_dump, pg_restore, pg_dumpall, and
pg_upgrade.

It incorporates most of what Jeff changed when he unified v36j, with typo
fixes spotted by Bruce. There was interest in splitting STATISTICS DATA
into RELATION STATISTICS DATA and ATTRIBUTE STATISTICS DATA. To do that,
we'd need to create separate TOC entries, and that doesn't seem worth it to
me.

There was also interest in changing the prefix for STATISTICS DATA.
However, the only special case for prefixes currently relies on an isData
flag. Since there is no isStatistics flag, we would either have to create
one, or do strcmps on te->description looking for "STATISTICS DATA". It's
do-able, but I'm not sure it's worth it.

0002-0005 are for extended stats.

0002 - This makes the input function for pg_ndistinct functional.
0003 - This makes the input function for pg_dependencies functional.
0004 - Makes several static functions in attribute_stats.c public for use
by extended stats. One of those is get_stat_attr_type(), which in the last
patchset was modified to take an attribute name rather than attnum, thus
saving a syscache lookup. However, extended stats identifies attributes by
attnum not name, so that optimization had to be set aside, at least
temporarily.

0005 - These implement the functions pg_set_extended_stats(),
pg_clear_extended_stats(), and pg_restore_extended_stats() and behave like
their relation/attribute equivalents. If we can get these committed and
used by pg_dump, then we don't have to debate how to handle post-upgrade
steps for users who happen to have extended stats vs the approximately
99.75% of users who do not have extended stats.

0006-0011 - These are the currently on-ice vacuumdb changes. 0006-0009 are
"do no harm" reorganizations which add no functionality but make 0011
clearer. 0010 introduces a new test check to TAP. This group will be spun
out into their own thread where we can focus on what if any changes happen
to vacuumdb. Any changes to vacuumdb are dependent on 0001 being committed,
and any changes to vacuumdb would likewise be informed by the commit status
of 0002-0005.

Work still to be done, in the near and less-near term:

* Adding statistic import for extended statistics objects (i.e. CREATE
STATISTICS) to pg_dump. Blocked by 0002-0005.
* Implementing optional statistics import in postgres_fdw. Blocked by 0001.

Attachments:

v37-0004-Expose-attribute-statistics-functions-for-use-in.patchtext/x-patch; charset=US-ASCII; name=v37-0004-Expose-attribute-statistics-functions-for-use-in.patchDownload
From 43a4e7891f939b68d7f5e8091e7df3af766fdd8c Mon Sep 17 00:00:00 2001
From: Corey Huinker <chuinker@amazon.com>
Date: Thu, 26 Dec 2024 05:02:06 -0500
Subject: [PATCH v37 04/11] Expose attribute statistics functions for use in
 extended_stats.

Many of the operations of attribute stats have analogous operations in
extended stats.

* get_attr_stat_type()
* init_empty_stats_tuple()
* text_to_stavalues()
* get_elem_stat_type()
---
 src/include/statistics/statistics.h      | 17 +++++++++++++++++
 src/backend/statistics/attribute_stats.c | 24 +++++-------------------
 2 files changed, 22 insertions(+), 19 deletions(-)

diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 7dd0f97554..f47f192dff 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -127,4 +127,21 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												int nclauses);
 extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
 
+extern void get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
+							   Oid *atttypid, int32 *atttypmod,
+							   char *atttyptype, Oid *atttypcoll,
+							   Oid *eq_opr, Oid *lt_opr);
+extern void init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
+								   Datum *values, bool *nulls, bool *replaces);
+
+extern void set_stats_slot(Datum *values, bool *nulls, bool *replaces,
+						   int16 stakind, Oid staop, Oid stacoll,
+						   Datum stanumbers, bool stanumbers_isnull,
+						   Datum stavalues, bool stavalues_isnull);
+
+extern Datum text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d,
+							   Oid typid, int32 typmod, int elevel, bool *ok);
+extern bool get_elem_stat_type(Oid atttypid, char atttyptype, int elevel,
+							   Oid *elemtypid, Oid *elem_eq_opr);
+
 #endif							/* STATISTICS_H */
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index 94f7dd63a0..f617165386 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -78,23 +78,9 @@ static struct StatsArgInfo attarginfo[] =
 
 static bool attribute_statistics_update(FunctionCallInfo fcinfo, int elevel);
 static Node *get_attr_expr(Relation rel, int attnum);
-static void get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
-							   Oid *atttypid, int32 *atttypmod,
-							   char *atttyptype, Oid *atttypcoll,
-							   Oid *eq_opr, Oid *lt_opr);
-static bool get_elem_stat_type(Oid atttypid, char atttyptype, int elevel,
-							   Oid *elemtypid, Oid *elem_eq_opr);
-static Datum text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d,
-							   Oid typid, int32 typmod, int elevel, bool *ok);
-static void set_stats_slot(Datum *values, bool *nulls, bool *replaces,
-						   int16 stakind, Oid staop, Oid stacoll,
-						   Datum stanumbers, bool stanumbers_isnull,
-						   Datum stavalues, bool stavalues_isnull);
 static void upsert_pg_statistic(Relation starel, HeapTuple oldtup,
 								Datum *values, bool *nulls, bool *replaces);
 static bool delete_pg_statistic(Oid reloid, AttrNumber attnum, bool stainherit);
-static void init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
-								   Datum *values, bool *nulls, bool *replaces);
 
 /*
  * Insert or Update Attribute Statistics
@@ -502,7 +488,7 @@ get_attr_expr(Relation rel, int attnum)
 /*
  * Derive type information from the attribute.
  */
-static void
+void
 get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
 				   Oid *atttypid, int32 *atttypmod,
 				   char *atttyptype, Oid *atttypcoll,
@@ -584,7 +570,7 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
 /*
  * Derive element type information from the attribute type.
  */
-static bool
+bool
 get_elem_stat_type(Oid atttypid, char atttyptype, int elevel,
 				   Oid *elemtypid, Oid *elem_eq_opr)
 {
@@ -624,7 +610,7 @@ get_elem_stat_type(Oid atttypid, char atttyptype, int elevel,
  * to false. If the resulting array contains NULLs, raise an error at elevel
  * and set ok to false. Otherwise, set ok to true.
  */
-static Datum
+Datum
 text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid typid,
 				  int32 typmod, int elevel, bool *ok)
 {
@@ -678,7 +664,7 @@ text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid typid,
  * Find and update the slot with the given stakind, or use the first empty
  * slot.
  */
-static void
+void
 set_stats_slot(Datum *values, bool *nulls, bool *replaces,
 			   int16 stakind, Oid staop, Oid stacoll,
 			   Datum stanumbers, bool stanumbers_isnull,
@@ -802,7 +788,7 @@ delete_pg_statistic(Oid reloid, AttrNumber attnum, bool stainherit)
 /*
  * Initialize values and nulls for a new stats tuple.
  */
-static void
+void
 init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
 					   Datum *values, bool *nulls, bool *replaces)
 {
-- 
2.47.1

v37-0002-Add-working-input-function-for-pg_ndistinct.patchtext/x-patch; charset=US-ASCII; name=v37-0002-Add-working-input-function-for-pg_ndistinct.patchDownload
From 658234311aaba0063fabc3d6bfb76f3f5f28012b Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Tue, 17 Dec 2024 03:30:55 -0500
Subject: [PATCH v37 02/11] Add working input function for pg_ndistinct.

This is needed to import extended statistics.
---
 src/backend/statistics/mvdistinct.c     | 270 +++++++++++++++++++++++-
 src/test/regress/expected/stats_ext.out |   7 +
 src/test/regress/sql/stats_ext.sql      |   3 +
 3 files changed, 274 insertions(+), 6 deletions(-)

diff --git a/src/backend/statistics/mvdistinct.c b/src/backend/statistics/mvdistinct.c
index 7e7a63405c..13ca2a9fd1 100644
--- a/src/backend/statistics/mvdistinct.c
+++ b/src/backend/statistics/mvdistinct.c
@@ -27,10 +27,18 @@
 
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_statistic_ext_data.h"
+#include "common/jsonapi.h"
+#include "fmgr.h"
 #include "lib/stringinfo.h"
+#include "mb/pg_wchar.h"
+#include "nodes/miscnodes.h"
+#include "nodes/pg_list.h"
 #include "statistics/extended_stats_internal.h"
 #include "statistics/statistics.h"
+#include "utils/builtins.h"
+#include "utils/float.h"
 #include "utils/fmgrprotos.h"
+#include "utils/palloc.h"
 #include "utils/syscache.h"
 #include "utils/typcache.h"
 #include "varatt.h"
@@ -328,21 +336,271 @@ statext_ndistinct_deserialize(bytea *data)
 	return ndistinct;
 }
 
+typedef struct
+{
+	const char *str;
+	bool		found_only_object;
+	List	   *distinct_items;
+	Node	   *escontext;
+
+	MVNDistinctItem *current_item;
+}			ndistinctParseState;
+
+/*
+ * Invoked at the start of each object in the JSON document.
+ * The entire JSON document should be one object with no sub-objects.
+ *
+ * If we're anywhere else in the document, it's an error.
+ */
+static JsonParseErrorType
+ndistinct_object_start(void *state)
+{
+	ndistinctParseState *parse = state;
+
+	if (parse->found_only_object == true)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+				 errdetail("Must begin with \"{\"")));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	parse->found_only_object = true;
+	return JSON_SUCCESS;
+}
+
+/*
+ * ndsitinct input format does not have arrays, so any array elements encountered
+ * are an error.
+ */
+static JsonParseErrorType
+ndistinct_array_start(void *state)
+{
+	ndistinctParseState *parse = state;
+
+	ereturn(parse->escontext, (Datum) 0,
+			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+			 errdetail("All ndistinct count values are scalar doubles.")));
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * The object keys are themselves comma-separated lists of attnums
+ * with negative attnums representing one of the expressions defined
+ * in the extened statistics object.
+ */
+static JsonParseErrorType
+ndistinct_object_field_start(void *state, char *fname, bool isnull)
+{
+	ndistinctParseState *parse = state;
+	char	   *token;
+	char	   *saveptr;
+	const char *delim = ", ";
+	char	   *scratch;
+	List	   *attnum_list = NIL;
+	int			natts = 0;
+	MVNDistinctItem *item;
+
+	if (isnull || fname == NULL)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+				 errdetail("All ndistinct attnum lists must be a comma separated list of attnums.")));
+
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	scratch = pstrdup(fname);
+
+	token = strtok_r(scratch, delim, &saveptr);
+
+	while (token != NULL)
+	{
+		attnum_list = lappend(attnum_list, (void *) token);
+
+		token = strtok_r(NULL, delim, &saveptr);
+	}
+	natts = attnum_list->length;
+
+	/*
+	 * We need at least 2 attnums for a ndistinct item, anything less is
+	 * malformed.
+	 */
+	if (natts < 2)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+				 errdetail("All ndistinct attnum lists must be a comma separated list of attnums.")));
+
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	item = palloc(sizeof(MVNDistinctItem));
+	item->nattributes = natts;
+	item->attributes = palloc(natts * sizeof(AttrNumber));
+
+	for (int i = 0; i < natts; i++)
+	{
+		char	   *s = (char *) attnum_list->elements[i].ptr_value;
+
+		item->attributes[i] = pg_strtoint16_safe(s, parse->escontext);
+
+		if (SOFT_ERROR_OCCURRED(parse->escontext))
+			return JSON_SEM_ACTION_FAILED;
+	}
+
+	list_free(attnum_list);
+	pfree(scratch);
+
+	/* add ndistinct-less MVNDistinctItem to the list */
+	parse->current_item = item;
+	parse->distinct_items = lappend(parse->distinct_items, (void *) item);
+	return JSON_SUCCESS;
+}
+
+/*
+ * ndsitinct input format does not have arrays, so any array elements encountered
+ * are an error.
+ */
+static JsonParseErrorType
+ndistinct_array_element_start(void *state, bool isnull)
+{
+	ndistinctParseState *parse = state;
+
+	ereturn(parse->escontext, (Datum) 0,
+			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+			 errdetail("Cannot contain array elements.")));
+
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * Handle scalar events from the ndistinct input parser.
+ *
+ * There is only one case where we will encounter a scalar, and that is the
+ * ndsitinct value for the previous object key.
+ */
+static JsonParseErrorType
+ndistinct_scalar(void *state, char *token, JsonTokenType tokentype)
+{
+	ndistinctParseState *parse = state;
+
+	/* if the entire json is just one scalar, that's wrong */
+	if (parse->found_only_object != true)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_ndistinct: \"%s\"", parse->str),
+				 errdetail("Must begin with \"{\"")));
+
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	Assert(parse->current_item != NULL);
+
+	parse->current_item->ndistinct = float8in_internal(token, NULL, "double",
+													   token, parse->escontext);
+
+	if (SOFT_ERROR_OCCURRED(parse->escontext))
+		return JSON_SEM_ACTION_FAILED;
+
+	/* mark us done with this item */
+	parse->current_item = NULL;
+	return JSON_SUCCESS;
+}
+
 /*
  * pg_ndistinct_in
  *		input routine for type pg_ndistinct
  *
- * pg_ndistinct is real enough to be a table column, but it has no
- * operations of its own, and disallows input (just like pg_node_tree).
+ * example input: {"6, -1": 14, "6, -2": 9143, "-1, -2": 13454, "6, -1, -2": 14549}
+ *
+ * This import format is clearly a specific subset of JSON, therefore it makes
+ * sense to leverage those parsing utilities, and further validate it from there.
  */
 Datum
 pg_ndistinct_in(PG_FUNCTION_ARGS)
 {
-	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot accept a value of type %s", "pg_ndistinct")));
+	char	   *str = PG_GETARG_CSTRING(0);
 
-	PG_RETURN_VOID();			/* keep compiler quiet */
+	ndistinctParseState parse_state;
+	JsonParseErrorType result;
+	JsonLexContext *lex;
+	JsonSemAction sem_action;
+
+	/* initialize semantic state */
+	parse_state.str = str;
+	parse_state.found_only_object = false;
+	parse_state.distinct_items = NIL;
+	parse_state.escontext = fcinfo->context;
+	parse_state.current_item = NULL;
+
+	/* set callbacks */
+	sem_action.semstate = (void *) &parse_state;
+	sem_action.object_start = ndistinct_object_start;
+	sem_action.object_end = NULL;
+	sem_action.array_start = ndistinct_array_start;
+	sem_action.array_end = NULL;
+	sem_action.object_field_start = ndistinct_object_field_start;
+	sem_action.object_field_end = NULL;
+	sem_action.array_element_start = ndistinct_array_element_start;
+	sem_action.array_element_end = NULL;
+	sem_action.scalar = ndistinct_scalar;
+
+	lex = makeJsonLexContextCstringLen(NULL, str, strlen(str),
+									   PG_UTF8, true);
+	result = pg_parse_json(lex, &sem_action);
+	freeJsonLexContext(lex);
+	if (result == JSON_SUCCESS)
+	{
+		MVNDistinct *ndistinct;
+		int			nitems = parse_state.distinct_items->length;
+		bytea	   *bytes;
+
+		ndistinct = palloc(offsetof(MVNDistinct, items) +
+						   nitems * sizeof(MVNDistinctItem));
+
+		ndistinct->magic = STATS_NDISTINCT_MAGIC;
+		ndistinct->type = STATS_NDISTINCT_TYPE_BASIC;
+		ndistinct->nitems = nitems;
+
+		for (int i = 0; i < nitems; i++)
+		{
+			MVNDistinctItem *item = parse_state.distinct_items->elements[i].ptr_value;
+
+			ndistinct->items[i].ndistinct = item->ndistinct;
+			ndistinct->items[i].nattributes = item->nattributes;
+			ndistinct->items[i].attributes = item->attributes;
+
+			/*
+			 * free the MVNDistinctItem, but not the attributes we're still
+			 * using
+			 */
+			pfree(item);
+		}
+		bytes = statext_ndistinct_serialize(ndistinct);
+
+		list_free(parse_state.distinct_items);
+		for (int i = 0; i < nitems; i++)
+			pfree(ndistinct->items[i].attributes);
+		pfree(ndistinct);
+
+		PG_RETURN_BYTEA_P(bytes);
+	}
+	else if (result == JSON_SEM_ACTION_FAILED)
+		PG_RETURN_NULL();		/* escontext already set */
+
+	/* Anything else is a generic JSON parse error */
+	ereturn(parse_state.escontext, (Datum) 0,
+			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			 errmsg("malformed pg_ndistinct: \"%s\"", str),
+			 errdetail("Must be valid JSON.")));
+	PG_RETURN_NULL();
 }
 
 /*
diff --git a/src/test/regress/expected/stats_ext.out b/src/test/regress/expected/stats_ext.out
index a4c7be487e..6f3da85101 100644
--- a/src/test/regress/expected/stats_ext.out
+++ b/src/test/regress/expected/stats_ext.out
@@ -3335,6 +3335,13 @@ SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
  s_expr          | {1}
 (2 rows)
 
+-- new input functions
+SELECT '{"6, -1": 14, "6, -2": 9143, "-1, -2": 13454, "6, -1, -2": 14549}'::pg_ndistinct;
+                           pg_ndistinct                            
+-------------------------------------------------------------------
+ {"6, -1": 14, "6, -2": 9143, "-1, -2": 13454, "6, -1, -2": 14549}
+(1 row)
+
 -- Tidy up
 DROP OPERATOR <<< (int, int);
 DROP FUNCTION op_leak(int, int);
diff --git a/src/test/regress/sql/stats_ext.sql b/src/test/regress/sql/stats_ext.sql
index 5c786b16c6..a53564bed5 100644
--- a/src/test/regress/sql/stats_ext.sql
+++ b/src/test/regress/sql/stats_ext.sql
@@ -1686,6 +1686,9 @@ SELECT statistics_name, most_common_vals FROM pg_stats_ext x
 SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
     WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
 
+-- new input functions
+SELECT '{"6, -1": 14, "6, -2": 9143, "-1, -2": 13454, "6, -1, -2": 14549}'::pg_ndistinct;
+
 -- Tidy up
 DROP OPERATOR <<< (int, int);
 DROP FUNCTION op_leak(int, int);
-- 
2.47.1

v37-0003-Add-working-input-function-for-pg_dependencies.patchtext/x-patch; charset=US-ASCII; name=v37-0003-Add-working-input-function-for-pg_dependencies.patchDownload
From a7128a0fe5a14e09bbd5a832eac4b8914dc717b2 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Tue, 17 Dec 2024 19:47:43 -0500
Subject: [PATCH v37 03/11] Add working input function for pg_dependencies.

This is needed to import extended statistics.
---
 src/backend/statistics/dependencies.c   | 322 +++++++++++++++++++++++-
 src/test/regress/expected/stats_ext.out |   6 +
 src/test/regress/sql/stats_ext.sql      |   1 +
 3 files changed, 319 insertions(+), 10 deletions(-)

diff --git a/src/backend/statistics/dependencies.c b/src/backend/statistics/dependencies.c
index eb2fc4366b..a26f73d063 100644
--- a/src/backend/statistics/dependencies.c
+++ b/src/backend/statistics/dependencies.c
@@ -13,18 +13,26 @@
  */
 #include "postgres.h"
 
+#include "access/attnum.h"
 #include "access/htup_details.h"
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_statistic_ext_data.h"
+#include "common/jsonapi.h"
+#include "fmgr.h"
 #include "lib/stringinfo.h"
+#include "mb/pg_wchar.h"
+#include "nodes/miscnodes.h"
 #include "nodes/nodeFuncs.h"
 #include "nodes/nodes.h"
 #include "nodes/pathnodes.h"
+#include "nodes/pg_list.h"
 #include "optimizer/clauses.h"
 #include "optimizer/optimizer.h"
 #include "parser/parsetree.h"
 #include "statistics/extended_stats_internal.h"
 #include "statistics/statistics.h"
+#include "utils/builtins.h"
+#include "utils/float.h"
 #include "utils/fmgroids.h"
 #include "utils/fmgrprotos.h"
 #include "utils/lsyscache.h"
@@ -643,24 +651,318 @@ statext_dependencies_load(Oid mvoid, bool inh)
 	return result;
 }
 
+typedef struct
+{
+	const char *str;
+	bool		found_only_object;
+	List	   *dependency_list;
+	Node	   *escontext;
+
+	MVDependency *current_dependency;
+}			dependenciesParseState;
+
+/*
+ * Invoked at the start of each object in the JSON document.
+ * The entire JSON document should be one object with no sub-objects.
+ *
+ * If we're anywhere else in the document, it's an error.
+ */
+static JsonParseErrorType
+dependencies_object_start(void *state)
+{
+	dependenciesParseState *parse = state;
+
+	if (parse->found_only_object == true)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				 errdetail("Must begin with \"{\"")));
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	parse->found_only_object = true;
+	return JSON_SUCCESS;
+}
+
+/*
+ * dependencies input format does not have arrays, so any array elements encountered
+ * are an error.
+ */
+static JsonParseErrorType
+dependencies_array_start(void *state)
+{
+	dependenciesParseState *parse = state;
+
+	ereturn(parse->escontext, (Datum) 0,
+			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+			 errdetail("All dependencies count values are scalar doubles.")));
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/* TODO COPY START */
+
+
+/*
+ * The object keys are themselves comma-separated lists of attnums
+ * with negative attnums representing one of the expressions defined
+ * in the extened statistics object, followed by a => and a final attnum.
+ *
+ * example: "-1, 2 => -1"
+ */
+static JsonParseErrorType
+dependencies_object_field_start(void *state, char *fname, bool isnull)
+{
+	dependenciesParseState *parse = state;
+	char	   *token;
+	char	   *saveptr;
+	const char *delim = ", ";
+	const char *arrow_delim = " => ";
+	char	   *scratch;
+	char	   *arrow_p;
+	char	   *after_arrow_p;
+	List	   *attnum_list = NIL;
+	int			natts = 0;
+	AttrNumber	final_attnum;
+	MVDependency *dep;
+
+	if (isnull || fname == NULL)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				 errdetail("All dependencies attnum lists must be a comma separated list of attnums with a final => attnum.")));
+
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	scratch = pstrdup(fname);
+
+	/* The subtring ' => ' must occur exactly once */
+	arrow_p = strstr(scratch, arrow_delim);
+	if (arrow_p == NULL)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				 errdetail("All dependencies attnum lists must be a comma separated list of attnums with a final => attnum.")));
+
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	/*
+	 * Everything to the left of the arrow is the attribute list, so split
+	 * that off into its own string.
+	 *
+	 * Everything to the right should be the lone target attribute.
+	 */
+	*arrow_p = '\0';
+
+	/* look for the character immediately beyond the delimiter we just found */
+	after_arrow_p = arrow_p + strlen(arrow_delim);
+
+	/* We should not find another arrow delim */
+	if (strstr(after_arrow_p, arrow_delim) != NULL)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				 errdetail("All dependencies attnum lists must be a comma separated list of attnums with a final => attnum.")));
+
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	/* what is left should be exactly one attnum */
+	final_attnum = pg_strtoint16_safe(after_arrow_p, parse->escontext);
+
+	if (SOFT_ERROR_OCCURRED(parse->escontext))
+		return JSON_SEM_ACTION_FAILED;
+
+	/* Left of the arrow is just regular attnums */
+	token = strtok_r(scratch, delim, &saveptr);
+
+	while (token != NULL)
+	{
+		attnum_list = lappend(attnum_list, (void *) token);
+
+		token = strtok_r(NULL, delim, &saveptr);
+	}
+	natts = attnum_list->length;
+
+	/*
+	 * We need at least 2 attnums left of the arrow for a dependencies item,
+	 * anything less is malformed.
+	 */
+	if (natts < 1)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				 errdetail("All dependencies attnum lists must be a comma separated list of attnums.")));
+
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	/*
+	 * Allocate enough space for the dependency, the attnums in the list, plus
+	 * the final attnum
+	 */
+	dep = palloc0(offsetof(MVDependency, attributes) + ((natts + 1) * sizeof(AttrNumber)));
+	dep->nattributes = natts + 1;
+	dep->attributes[natts] = final_attnum;
+
+	for (int i = 0; i < natts; i++)
+	{
+		char	   *s = (char *) attnum_list->elements[i].ptr_value;
+
+		dep->attributes[i] = pg_strtoint16_safe(s, parse->escontext);
+
+		if (SOFT_ERROR_OCCURRED(parse->escontext))
+			return JSON_SEM_ACTION_FAILED;
+	}
+
+	list_free(attnum_list);
+	pfree(scratch);
+
+	/* add dependencies-less MVdependenciesItem to the list */
+	parse->current_dependency = dep;
+	parse->dependency_list = lappend(parse->dependency_list, (void *) dep);
+	return JSON_SUCCESS;
+}
+
+/*
+ * ndsitinct input format does not have arrays, so any array elements encountered
+ * are an error.
+ */
+static JsonParseErrorType
+dependencies_array_element_start(void *state, bool isnull)
+{
+	dependenciesParseState *parse = state;
+
+	ereturn(parse->escontext, (Datum) 0,
+			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+			 errdetail("Cannot contain array elements.")));
+
+	return JSON_SEM_ACTION_FAILED;
+}
+
+/*
+ * Handle scalar events from the dependencies input parser.
+ *
+ * There is only one case where we will encounter a scalar, and that is the
+ * dependency degree for the previous object key.
+ */
+static JsonParseErrorType
+dependencies_scalar(void *state, char *token, JsonTokenType tokentype)
+{
+	dependenciesParseState *parse = state;
+
+	/* if the entire json is just one scalar, that's wrong */
+	if (parse->found_only_object != true)
+	{
+		ereturn(parse->escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				 errmsg("malformed pg_dependencies: \"%s\"", parse->str),
+				 errdetail("Must begin with \"{\"")));
+
+		return JSON_SEM_ACTION_FAILED;
+	}
+
+	Assert(parse->current_dependency != NULL);
+
+	parse->current_dependency->degree = float8in_internal(token, NULL, "double",
+														  token, parse->escontext);
+
+	if (SOFT_ERROR_OCCURRED(parse->escontext))
+		return JSON_SEM_ACTION_FAILED;
+
+	/* mark us done with this dependency */
+	parse->current_dependency = NULL;
+	return JSON_SUCCESS;
+}
+
 /*
  * pg_dependencies_in		- input routine for type pg_dependencies.
  *
- * pg_dependencies is real enough to be a table column, but it has no operations
- * of its own, and disallows input too
+ * example input:
+ *     {"-2 => 6": 0.292508,
+ *      "-2 => -1": 0.113999,
+ *      "6, -2 => -1": 0.348479,
+ *      "-1, -2 => 6": 0.839691}
+ *
+ * This import format is clearly a specific subset of JSON, therefore it makes
+ * sense to leverage those parsing utilities, and further validate it from there.
  */
 Datum
 pg_dependencies_in(PG_FUNCTION_ARGS)
 {
-	/*
-	 * pg_node_list stores the data in binary form and parsing text input is
-	 * not needed, so disallow this.
-	 */
-	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot accept a value of type %s", "pg_dependencies")));
+	char	   *str = PG_GETARG_CSTRING(0);
 
-	PG_RETURN_VOID();			/* keep compiler quiet */
+	dependenciesParseState parse_state;
+	JsonParseErrorType result;
+	JsonLexContext *lex;
+	JsonSemAction sem_action;
+
+	/* initialize the semantic state */
+	parse_state.str = str;
+	parse_state.found_only_object = false;
+	parse_state.dependency_list = NIL;
+	parse_state.escontext = fcinfo->context;
+	parse_state.current_dependency = NULL;
+
+	/* set callbacks */
+	sem_action.semstate = (void *) &parse_state;
+	sem_action.object_start = dependencies_object_start;
+	sem_action.object_end = NULL;
+	sem_action.array_start = dependencies_array_start;
+	sem_action.array_end = NULL;
+	sem_action.array_element_start = dependencies_array_element_start;
+	sem_action.array_element_end = NULL;
+	sem_action.object_field_start = dependencies_object_field_start;
+	sem_action.object_field_end = NULL;
+	sem_action.scalar = dependencies_scalar;
+
+	lex = makeJsonLexContextCstringLen(NULL, str, strlen(str), PG_UTF8, true);
+
+	result = pg_parse_json(lex, &sem_action);
+	freeJsonLexContext(lex);
+
+	if (result == JSON_SUCCESS)
+	{
+		List	   *list = parse_state.dependency_list;
+		int			ndeps = list->length;
+		MVDependencies *mvdeps;
+		bytea	   *bytes;
+
+		mvdeps = palloc0(offsetof(MVDependencies, deps) + ndeps * sizeof(MVDependency));
+		mvdeps->magic = STATS_DEPS_MAGIC;
+		mvdeps->type = STATS_DEPS_TYPE_BASIC;
+		mvdeps->ndeps = ndeps;
+
+		/* copy MVDependency structs out of the list into the MVDependencies */
+		for (int i = 0; i < ndeps; i++)
+			mvdeps->deps[i] = list->elements[i].ptr_value;
+		bytes = statext_dependencies_serialize(mvdeps);
+
+		list_free(list);
+		for (int i = 0; i < ndeps; i++)
+			pfree(mvdeps->deps[i]);
+		pfree(mvdeps);
+
+		PG_RETURN_BYTEA_P(bytes);
+	}
+	else if (result == JSON_SEM_ACTION_FAILED)
+		PG_RETURN_NULL();
+
+	/* Anything else is a generic JSON parse error */
+	ereturn(parse_state.escontext, (Datum) 0,
+			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+			 errmsg("malformed pg_dependencies: \"%s\"", str),
+			 errdetail("Must be valid JSON.")));
+
+	PG_RETURN_NULL();			/* keep compiler quiet */
 }
 
 /*
diff --git a/src/test/regress/expected/stats_ext.out b/src/test/regress/expected/stats_ext.out
index 6f3da85101..4dda2d8b9c 100644
--- a/src/test/regress/expected/stats_ext.out
+++ b/src/test/regress/expected/stats_ext.out
@@ -3342,6 +3342,12 @@ SELECT '{"6, -1": 14, "6, -2": 9143, "-1, -2": 13454, "6, -1, -2": 14549}'::pg_n
  {"6, -1": 14, "6, -2": 9143, "-1, -2": 13454, "6, -1, -2": 14549}
 (1 row)
 
+SELECT '{"-2 => 6": 0.292508, "-2 => -1": 0.113999, "6, -2 => -1": 0.348479, "-1, -2 => 6": 0.839691}'::pg_dependencies;
+                                        pg_dependencies                                        
+-----------------------------------------------------------------------------------------------
+ {"-2 => 6": 0.292508, "-2 => -1": 0.113999, "6, -2 => -1": 0.348479, "-1, -2 => 6": 0.839691}
+(1 row)
+
 -- Tidy up
 DROP OPERATOR <<< (int, int);
 DROP FUNCTION op_leak(int, int);
diff --git a/src/test/regress/sql/stats_ext.sql b/src/test/regress/sql/stats_ext.sql
index a53564bed5..26375e6e46 100644
--- a/src/test/regress/sql/stats_ext.sql
+++ b/src/test/regress/sql/stats_ext.sql
@@ -1688,6 +1688,7 @@ SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
 
 -- new input functions
 SELECT '{"6, -1": 14, "6, -2": 9143, "-1, -2": 13454, "6, -1, -2": 14549}'::pg_ndistinct;
+SELECT '{"-2 => 6": 0.292508, "-2 => -1": 0.113999, "6, -2 => -1": 0.348479, "-1, -2 => 6": 0.839691}'::pg_dependencies;
 
 -- Tidy up
 DROP OPERATOR <<< (int, int);
-- 
2.47.1

v37-0005-Add-extended-statistics-support-functions.patchtext/x-patch; charset=US-ASCII; name=v37-0005-Add-extended-statistics-support-functions.patchDownload
From 31b3c5a94c9820745924f862f6daa800edadb19f Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Fri, 3 Jan 2025 13:43:29 -0500
Subject: [PATCH v37 05/11] Add extended statistics support functions.

Add pg_set_extended_stats(), pg_clear_extended_stats(), and
pg_restore_extended_stats(). These function closely mirror their
relation and attribute counterparts, but for extended statistics (i.e.
CREATE STATISTICS) objects.
---
 src/include/catalog/pg_proc.dat            |   23 +
 src/backend/catalog/system_functions.sql   |   17 +
 src/backend/statistics/extended_stats.c    | 1237 +++++++++++++++++++-
 src/test/regress/expected/stats_import.out |  330 ++++++
 src/test/regress/sql/stats_import.sql      |  244 ++++
 doc/src/sgml/func.sgml                     |  124 ++
 6 files changed, 1973 insertions(+), 2 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index b37e8a6f88..96ec30cd6e 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12428,5 +12428,28 @@
   proname => 'gist_stratnum_identity', prorettype => 'int2',
   proargtypes => 'int2',
   prosrc => 'gist_stratnum_identity' },
+{ oid => '9947',
+  descr => 'restore statistics on extended statistics object',
+  proname => 'pg_restore_extended_stats', provolatile => 'v', proisstrict => 'f',
+  provariadic => 'any',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'any',
+  proargnames => '{kwargs}',
+  proargmodes => '{v}',
+  prosrc => 'pg_restore_extended_stats' },
+{ oid => '9948',
+  descr => 'set statistics on extended statistics object',
+  proname => 'pg_set_extended_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'void',
+  proargtypes => 'regnamespace name bool pg_ndistinct pg_dependencies _text _bool _float8 _float8 _text',
+  proargnames => '{statistics_schemaname,statistics_name,inherited,n_distinct,dependencies,most_common_vals,most_common_val_nulls,most_common_freqs,most_common_base_freqs,exprs}',
+  prosrc => 'pg_set_extended_stats' },
+{ oid => '9949',
+  descr => 'clear statistics on extended statistics object',
+  proname => 'pg_clear_extended_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'void',
+  proargtypes => 'regnamespace name bool',
+  proargnames => '{statistics_schemaname,statistics_name,inherited}',
+  prosrc => 'pg_clear_extended_stats' },
 
 ]
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 591157b1d1..82901bd8e1 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -668,6 +668,23 @@ LANGUAGE INTERNAL
 CALLED ON NULL INPUT VOLATILE
 AS 'pg_set_attribute_stats';
 
+CREATE OR REPLACE FUNCTION
+  pg_set_extended_stats(statistics_schemaname regnamespace,
+                        statistics_name name,
+                        inherited bool,
+                        n_distinct pg_ndistinct DEFAULT NULL,
+                        dependencies pg_dependencies DEFAULT NULL,
+                        most_common_vals text[] DEFAULT NULL,
+                        most_common_val_nulls boolean[] DEFAULT NULL,
+                        most_common_freqs double precision[] DEFAULT NULL,
+                        most_common_base_freqs double precision[] DEFAULT NULL,
+                        exprs text[] DEFAULT NULL)
+RETURNS void
+LANGUAGE INTERNAL
+CALLED ON NULL INPUT VOLATILE
+AS 'pg_set_extended_stats';
+
+
 --
 -- The default permissions for functions mean that anyone can execute them.
 -- A number of functions shouldn't be executable by just anyone, but rather
diff --git a/src/backend/statistics/extended_stats.c b/src/backend/statistics/extended_stats.c
index 34dcb535e1..3d949cf552 100644
--- a/src/backend/statistics/extended_stats.c
+++ b/src/backend/statistics/extended_stats.c
@@ -18,22 +18,35 @@
 
 #include "access/detoast.h"
 #include "access/genam.h"
+#include "access/heapam.h"
+#include "access/htup.h"
 #include "access/htup_details.h"
 #include "access/table.h"
+#include "c.h"
 #include "catalog/indexing.h"
+#include "catalog/pg_collation.h"
+#include "catalog/pg_statistic_d.h"
 #include "catalog/pg_statistic_ext.h"
+#include "catalog/pg_statistic_ext_d.h"
 #include "catalog/pg_statistic_ext_data.h"
+#include "catalog/pg_statistic_ext_data_d.h"
+#include "catalog/pg_type_d.h"
 #include "commands/defrem.h"
 #include "commands/progress.h"
+#include "commands/vacuum.h"
 #include "executor/executor.h"
+#include "fmgr.h"
 #include "miscadmin.h"
+#include "nodes/miscnodes.h"
 #include "nodes/nodeFuncs.h"
 #include "optimizer/optimizer.h"
 #include "parser/parsetree.h"
 #include "pgstat.h"
 #include "postmaster/autovacuum.h"
 #include "statistics/extended_stats_internal.h"
+#include "statistics/stat_utils.h"
 #include "statistics/statistics.h"
+#include "storage/lockdefs.h"
 #include "utils/acl.h"
 #include "utils/array.h"
 #include "utils/attoptcache.h"
@@ -42,9 +55,11 @@
 #include "utils/fmgroids.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
+#include "utils/palloc.h"
 #include "utils/rel.h"
 #include "utils/selfuncs.h"
 #include "utils/syscache.h"
+#include "utils/typcache.h"
 
 /*
  * To avoid consuming too much memory during analysis and/or too much space
@@ -72,6 +87,71 @@ typedef struct StatExtEntry
 	List	   *exprs;			/* expressions */
 } StatExtEntry;
 
+enum extended_stats_argnum
+{
+	STATSCHEMA_ARG = 0,
+	STATNAME_ARG,
+	INHERITED_ARG,
+	NDISTINCT_ARG,
+	DEPENDENCIES_ARG,
+	MOST_COMMON_VALS_ARG,
+	MOST_COMMON_VAL_NULLS_ARG,
+	MOST_COMMON_FREQS_ARG,
+	MOST_COMMON_BASE_FREQS_ARG,
+	EXPRESSIONS_ARG,
+	NUM_EXTENDED_STATS_ARGS
+};
+
+static struct StatsArgInfo extarginfo[] =
+{
+	[STATSCHEMA_ARG] = {"statistics_schemaname", REGNAMESPACEOID},
+	[STATNAME_ARG] = {"statistics_name", NAMEOID},
+	[INHERITED_ARG] = {"inherited", BOOLOID},
+	[NDISTINCT_ARG] = {"n_distinct", PG_NDISTINCTOID},
+	[DEPENDENCIES_ARG] = {"dependencies", PG_DEPENDENCIESOID},
+	[MOST_COMMON_VALS_ARG] = {"most_common_vals", TEXTARRAYOID},
+	[MOST_COMMON_VAL_NULLS_ARG] = {"most_common_val_nulls", BOOLARRAYOID},
+	[MOST_COMMON_FREQS_ARG] = {"most_common_freqs", FLOAT8ARRAYOID},
+	[MOST_COMMON_BASE_FREQS_ARG] = {"most_common_base_freqs", FLOAT8ARRAYOID},
+	[EXPRESSIONS_ARG] = {"exprs", TEXTARRAYOID},
+	[NUM_EXTENDED_STATS_ARGS] = {0}
+};
+
+/*
+ * NOTE: the RANGE_LENGTH & RANGE_BOUNDS stats are not yet reflected in any
+ * version of pg_stat_ext_exprs.
+ */
+enum extended_stats_exprs_element
+{
+	NULL_FRAC_ELEM = 0,
+	AVG_WIDTH_ELEM,
+	N_DISTINCT_ELEM,
+	MOST_COMMON_VALS_ELEM,
+	MOST_COMMON_FREQS_ELEM,
+	HISTOGRAM_BOUNDS_ELEM,
+	CORRELATION_ELEM,
+	MOST_COMMON_ELEMS_ELEM,
+	MOST_COMMON_ELEM_FREQS_ELEM,
+	ELEM_COUNT_HISTOGRAM_ELEM,
+	NUM_ATTRIBUTE_STATS_ELEMS
+};
+
+static struct StatsArgInfo extexprarginfo[] =
+{
+	[NULL_FRAC_ELEM] = {"null_frac", FLOAT4OID},
+	[AVG_WIDTH_ELEM] = {"avg_width", INT4OID},
+	[N_DISTINCT_ELEM] = {"n_distinct", FLOAT4OID},
+	[MOST_COMMON_VALS_ELEM] = {"most_common_vals", TEXTOID},
+	[MOST_COMMON_FREQS_ELEM] = {"most_common_freqs", FLOAT4ARRAYOID},
+	[HISTOGRAM_BOUNDS_ELEM] = {"histogram_bounds", TEXTOID},
+	[CORRELATION_ELEM] = {"correlation", FLOAT4OID},
+	[MOST_COMMON_ELEMS_ELEM] = {"most_common_elems", TEXTOID},
+	[MOST_COMMON_ELEM_FREQS_ELEM] = {"most_common_elem_freqs", FLOAT4ARRAYOID},
+	[ELEM_COUNT_HISTOGRAM_ELEM] = {"elem_count_histogram", FLOAT4ARRAYOID},
+	[NUM_ATTRIBUTE_STATS_ELEMS] = {0}
+};
+
+static bool extended_statistics_update(FunctionCallInfo fcinfo, int elevel);
 
 static List *fetch_statentries_for_relation(Relation pg_statext, Oid relid);
 static VacAttrStats **lookup_var_attr_stats(Bitmapset *attrs, List *exprs,
@@ -99,6 +179,32 @@ static StatsBuildData *make_build_data(Relation rel, StatExtEntry *stat,
 									   int numrows, HeapTuple *rows,
 									   VacAttrStats **stats, int stattarget);
 
+static HeapTuple get_pg_statistic_ext(Relation pg_stext, Oid nspoid, Name stxname);
+static bool delete_pg_statistic_ext_data(Oid stxoid, bool inherited);
+
+typedef struct
+{
+	bool	ndistinct;
+	bool	dependencies;
+	bool	mcv;
+	bool	expressions;
+}	stakindFlags;
+
+static void expand_stxkind(HeapTuple tup, stakindFlags * enabled);
+static void upsert_pg_statistic_ext_data(Datum *values, bool *nulls, bool *replaces);
+static bool check_mcvlist_array(ArrayType *arr, int argindex,
+								int required_ndimss, int mcv_length,
+								int elevel);
+static Datum import_mcvlist(HeapTuple tup, int elevel, int numattrs,
+							Oid *atttypids, int32 *atttypmods, Oid *atttypcolls,
+							ArrayType *mcv_arr, ArrayType *nulls_arr,
+							ArrayType *freqs_arr, ArrayType *base_freqs_arr);
+static Datum import_expressions(Relation pgsd, int elevel, int numexprs,
+								Oid *atttypids, int32 *atttypmods,
+								Oid *atttypcolls, ArrayType *exprs_arr);
+static bool text_to_float4(Datum input, Datum *output);
+static bool text_to_int4(Datum input, Datum *output);
+
 
 /*
  * Compute requested extended stats, using the rows sampled for the plain
@@ -2099,7 +2205,6 @@ examine_opclause_args(List *args, Node **exprp, Const **cstp,
 	return true;
 }
 
-
 /*
  * Compute statistics about expressions of a relation.
  */
@@ -2239,7 +2344,6 @@ compute_expr_stats(Relation onerel, AnlExprData *exprdata, int nexprs,
 	MemoryContextDelete(expr_context);
 }
 
-
 /*
  * Fetch function for analyzing statistics object expressions.
  *
@@ -2631,3 +2735,1132 @@ make_build_data(Relation rel, StatExtEntry *stat, int numrows, HeapTuple *rows,
 
 	return result;
 }
+
+static HeapTuple
+get_pg_statistic_ext(Relation pg_stext, Oid nspoid, Name stxname)
+{
+	ScanKeyData key[2];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	Oid			stxoid = InvalidOid;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_statistic_ext_stxname,
+				BTEqualStrategyNumber,
+				F_NAMEEQ,
+				NameGetDatum(stxname));
+	ScanKeyInit(&key[1],
+				Anum_pg_statistic_ext_stxnamespace,
+				BTEqualStrategyNumber,
+				F_OIDEQ,
+				ObjectIdGetDatum(nspoid));
+
+	/*
+	 * Try to find matching pg_statistic_ext row.
+	 */
+	scan = systable_beginscan(pg_stext,
+							  StatisticExtNameIndexId,
+							  true,
+							  NULL,
+							  2,
+							  key);
+
+	/* Unique index, either we get a tuple or we don't. */
+	tup = systable_getnext(scan);
+
+	if (HeapTupleIsValid(tup))
+		stxoid = ((Form_pg_statistic_ext) GETSTRUCT(tup))->oid;
+
+	systable_endscan(scan);
+
+	if (!OidIsValid(stxoid))
+		return NULL;
+
+	return SearchSysCacheCopy1(STATEXTOID, ObjectIdGetDatum(stxoid)); 
+}
+
+/*
+ * Decode the stxkind column so that we know which stats types to expect.
+ */
+static void
+expand_stxkind(HeapTuple tup, stakindFlags * enabled)
+{
+	Datum		datum;
+	ArrayType  *arr;
+	char	   *kinds;
+
+	datum = SysCacheGetAttrNotNull(STATEXTOID,
+								   tup,
+								   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");
+
+	kinds = (char *) ARR_DATA_PTR(arr);
+
+	for (int i = 0; i < ARR_DIMS(arr)[0]; i++)
+		if (kinds[i] == STATS_EXT_NDISTINCT)
+			enabled->ndistinct = true;
+		else if (kinds[i] == STATS_EXT_DEPENDENCIES)
+			enabled->dependencies = true;
+		else if (kinds[i] == STATS_EXT_MCV)
+			enabled->mcv = true;
+		else if (kinds[i] == STATS_EXT_EXPRESSIONS)
+			enabled->expressions = true;
+}
+
+static void
+upsert_pg_statistic_ext_data(Datum *values, bool *nulls, bool *replaces)
+{
+	Relation	pg_stextdata;
+	HeapTuple	stxdtup;
+	HeapTuple	newtup;
+
+	pg_stextdata = table_open(StatisticExtDataRelationId, RowExclusiveLock);
+
+	stxdtup = SearchSysCache2(STATEXTDATASTXOID,
+							  values[Anum_pg_statistic_ext_data_stxoid - 1],
+							  values[Anum_pg_statistic_ext_data_stxdinherit - 1]);
+
+	if (HeapTupleIsValid(stxdtup))
+	{
+		newtup = heap_modify_tuple(stxdtup,
+								   RelationGetDescr(pg_stextdata),
+								   values,
+								   nulls,
+								   replaces);
+		CatalogTupleUpdate(pg_stextdata, &newtup->t_self, newtup);
+		ReleaseSysCache(stxdtup);
+	}
+	else
+	{
+		newtup = heap_form_tuple(RelationGetDescr(pg_stextdata), values, nulls);
+		CatalogTupleInsert(pg_stextdata, newtup);
+	}
+
+	heap_freetuple(newtup);
+
+	CommandCounterIncrement();
+
+	table_close(pg_stextdata, RowExclusiveLock);
+}
+
+/*
+ * Insert or Update Extended Statistics
+ *
+ * Major errors, such as the table not existing, the statistics object not
+ * existing, or a permissions failure are always reported at ERROR. Other
+ * errors, such as a conversion failure on one statistic kind, are reported
+ * at 'elevel', and other statistic kinds may still be updated.
+ */
+static bool
+extended_statistics_update(FunctionCallInfo fcinfo, int elevel)
+{
+	Oid			nspoid;
+	Name		stxname;
+	bool		inherited;
+	Relation	pg_stext;
+	HeapTuple	tup = NULL;
+
+	stakindFlags enabled;
+	stakindFlags has;
+
+	Form_pg_statistic_ext stxform;
+
+	Datum		values[Natts_pg_statistic_ext_data];
+	bool		nulls[Natts_pg_statistic_ext_data];
+	bool		replaces[Natts_pg_statistic_ext_data];
+
+	bool		success = true;
+
+	int			numattnums = 0;
+	int			numexprs = 0;
+	int			numattrs = 0;
+
+	/* arrays of type info, if we need them */
+	Oid		   *atttypids = NULL;
+	int32	   *atttypmods = NULL;
+	Oid		   *atttypcolls = NULL;
+
+	memset(nulls, false, sizeof(nulls));
+	memset(values, 0, sizeof(values));
+	memset(replaces, 0, sizeof(replaces));
+	memset(&enabled, 0, sizeof(enabled));
+
+	has.mcv = (!PG_ARGISNULL(MOST_COMMON_VALS_ARG) &&
+			   !PG_ARGISNULL(MOST_COMMON_VAL_NULLS_ARG) &&
+			   !PG_ARGISNULL(MOST_COMMON_FREQS_ARG) &&
+			   !PG_ARGISNULL(MOST_COMMON_BASE_FREQS_ARG));
+	has.ndistinct = !PG_ARGISNULL(NDISTINCT_ARG);
+	has.dependencies = !PG_ARGISNULL(DEPENDENCIES_ARG);
+	has.expressions = !PG_ARGISNULL(EXPRESSIONS_ARG);
+
+	if (RecoveryInProgress())
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("recovery is in progress"),
+				 errhint("Statistics cannot be modified during recovery.")));
+
+	stats_check_required_arg(fcinfo, extarginfo, STATSCHEMA_ARG);
+	nspoid = PG_GETARG_OID(STATSCHEMA_ARG);
+	stats_check_required_arg(fcinfo, extarginfo, STATNAME_ARG);
+	stxname = PG_GETARG_NAME(STATNAME_ARG);
+	stats_check_required_arg(fcinfo, extarginfo, INHERITED_ARG);
+	inherited = PG_GETARG_NAME(INHERITED_ARG);
+
+	pg_stext = table_open(StatisticExtRelationId, RowExclusiveLock);
+	tup = get_pg_statistic_ext(pg_stext, nspoid, stxname);
+
+	if (!HeapTupleIsValid(tup))
+	{
+		table_close(pg_stext, RowExclusiveLock);
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("Extended Statistics Object \"%s\".\"%s\" not found.",
+						get_namespace_name(nspoid),
+						NameStr(*stxname))));
+		return false;
+	}
+
+	stxform = (Form_pg_statistic_ext) GETSTRUCT(tup);
+	expand_stxkind(tup, &enabled);
+
+	/* lock table */
+	stats_lock_check_privileges(stxform->stxrelid);
+
+	if (has.mcv)
+	{
+		if (!enabled.mcv)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("MCV parameters \"%s\", \"%s\", \"%s\", and \"%s\" were all "
+							"specified for extended statistics object that does not expect MCV ",
+							extarginfo[MOST_COMMON_VALS_ARG].argname,
+							extarginfo[MOST_COMMON_VAL_NULLS_ARG].argname,
+							extarginfo[MOST_COMMON_FREQS_ARG].argname,
+							extarginfo[MOST_COMMON_BASE_FREQS_ARG].argname)));
+			has.mcv = false;
+			success = false;
+		}
+	}
+	else
+	{
+		/* The MCV args must all be NULL */
+		if (!PG_ARGISNULL(MOST_COMMON_VALS_ARG) ||
+			!PG_ARGISNULL(MOST_COMMON_VAL_NULLS_ARG) ||
+			!PG_ARGISNULL(MOST_COMMON_FREQS_ARG) ||
+			!PG_ARGISNULL(MOST_COMMON_BASE_FREQS_ARG))
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("MCV parameters \"%s\", \"%s\", \"%s\", and \"%s\" must be all specified if any are specified",
+							extarginfo[MOST_COMMON_VALS_ARG].argname,
+							extarginfo[MOST_COMMON_VAL_NULLS_ARG].argname,
+							extarginfo[MOST_COMMON_FREQS_ARG].argname,
+							extarginfo[MOST_COMMON_BASE_FREQS_ARG].argname)));
+	}
+
+	if (has.ndistinct && !enabled.ndistinct)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Parameters \"%s\" was specified for extended statistics object "
+						"that does not expect \"%s\"",
+						extarginfo[NDISTINCT_ARG].argname,
+						extarginfo[NDISTINCT_ARG].argname)));
+		has.ndistinct = false;
+		success = false;
+	}
+
+	if (has.dependencies && !enabled.dependencies)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Parameters \"%s\" was specified for extended statistics object "
+						"that does not expect \"%s\"",
+						extarginfo[DEPENDENCIES_ARG].argname,
+						extarginfo[DEPENDENCIES_ARG].argname)));
+		has.dependencies = false;
+		success = false;
+	}
+
+	if (has.expressions && !enabled.expressions)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Parameters \"%s\" was specified for extended statistics object "
+						"that does not expect \"%s\"",
+						extarginfo[DEPENDENCIES_ARG].argname,
+						extarginfo[DEPENDENCIES_ARG].argname)));
+		has.expressions = false;
+		success = false;
+	}
+
+	/*
+	 * Either of these statsistic types requires that we supply
+	 * semi-filled-out VacAttrStatP array.
+	 *
+	 *
+	 * It is not possible to use the existing lookup_var_attr_stats() and
+	 * examine_attribute() because these functions will skip attributes for
+	 * which attstattarget is 0, and we may have stats to import for those
+	 * attributes.
+	 */
+	if (has.mcv || has.expressions)
+	{
+		Datum		exprdatum;
+		bool		isnull;
+		List	   *exprs = NIL;
+
+		/* decode expression (if any) */
+		exprdatum = SysCacheGetAttr(STATEXTOID,
+									tup,
+									Anum_pg_statistic_ext_stxexprs,
+									&isnull);
+
+		if (!isnull)
+		{
+			char	   *s;
+
+			s = TextDatumGetCString(exprdatum);
+			exprs = (List *) stringToNode(s);
+			pfree(s);
+
+			/*
+			 * 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);
+		}
+
+		numattnums = stxform->stxkeys.dim1;
+		numexprs = list_length(exprs);
+		numattrs = numattnums + numexprs;
+
+		atttypids = palloc0(numattrs * sizeof(Oid));
+		atttypmods = palloc0(numattrs * sizeof(int32));
+		atttypcolls = palloc0(numattrs * sizeof(Oid));
+
+		for (int i = 0; i < numattnums; i++)
+		{
+			AttrNumber	attnum = stxform->stxkeys.values[i];
+
+			Oid			lt_opr;
+			Oid			eq_opr;
+			char		typetype;
+
+			/*
+			 * fetch attribute entries the same as are done for attribute
+			 * stats
+			 */
+			get_attr_stat_type(stxform->stxrelid,
+							   attnum,
+							   elevel,
+							   &atttypids[i],
+							   &atttypmods[i],
+							   &typetype,
+							   &atttypcolls[i],
+							   &lt_opr,
+							   &eq_opr);
+		}
+
+		for (int i = numattnums; i < numattrs; i++)
+		{
+			Node	   *expr = list_nth(exprs, i - numattnums);
+
+			atttypids[i] = exprType(expr);
+			atttypmods[i] = exprTypmod(expr);
+			atttypcolls[i] = exprCollation(expr);
+
+			/*
+			 * Duplicate logic from get_attr_stat_type
+			 */ 
+
+			/*
+			* If it's a multirange, step down to the range type, as is done by
+			* multirange_typanalyze().
+			*/
+			if (type_is_multirange(atttypids[i]))
+				atttypids[i] = get_multirange_range(atttypids[i]);
+
+			/*
+			* Special case: collation for tsvector is DEFAULT_COLLATION_OID. See
+			* compute_tsvector_stats().
+			*/
+			if (atttypids[i] == TSVECTOROID)
+				atttypcolls[i] = DEFAULT_COLLATION_OID;
+
+		}
+	}
+
+	/* Primary Key: cannot be NULL or replaced. */
+	values[Anum_pg_statistic_ext_data_stxoid - 1] = ObjectIdGetDatum(stxform->oid);
+	values[Anum_pg_statistic_ext_data_stxdinherit - 1] = BoolGetDatum(inherited);
+
+	if (has.ndistinct)
+	{
+		values[Anum_pg_statistic_ext_data_stxdndistinct - 1] = PG_GETARG_DATUM(NDISTINCT_ARG);
+		replaces[Anum_pg_statistic_ext_data_stxdndistinct - 1] = true;
+	}
+	else
+		nulls[Anum_pg_statistic_ext_data_stxdndistinct - 1] = true;
+
+	if (has.dependencies)
+	{
+		values[Anum_pg_statistic_ext_data_stxddependencies - 1] = PG_GETARG_DATUM(DEPENDENCIES_ARG);
+		replaces[Anum_pg_statistic_ext_data_stxddependencies - 1] = true;
+	}
+	else
+		nulls[Anum_pg_statistic_ext_data_stxddependencies - 1] = true;
+
+	if (has.mcv)
+	{
+		Datum	datum;
+
+		datum = import_mcvlist(tup, elevel, numattrs,
+							   atttypids, atttypmods, atttypcolls,
+							   PG_GETARG_ARRAYTYPE_P(MOST_COMMON_VALS_ARG),
+							   PG_GETARG_ARRAYTYPE_P(MOST_COMMON_VAL_NULLS_ARG),
+							   PG_GETARG_ARRAYTYPE_P(MOST_COMMON_FREQS_ARG),
+							   PG_GETARG_ARRAYTYPE_P(MOST_COMMON_BASE_FREQS_ARG));
+
+		values[Anum_pg_statistic_ext_data_stxdmcv - 1] = datum;
+		replaces[Anum_pg_statistic_ext_data_stxdmcv - 1] = true;
+	}
+	else
+		nulls[Anum_pg_statistic_ext_data_stxdmcv - 1] = true;
+
+	if (has.expressions)
+	{
+		Datum			datum;
+		Relation		pgsd;
+
+		pgsd = table_open(StatisticRelationId, RowExclusiveLock);
+
+		datum = import_expressions(pgsd, elevel, numexprs, 
+							 	   &atttypids[numattnums], &atttypmods[numattnums],
+							 	   &atttypcolls[numattnums],
+								   PG_GETARG_ARRAYTYPE_P(EXPRESSIONS_ARG));
+
+		table_close(pgsd, RowExclusiveLock);
+
+		values[Anum_pg_statistic_ext_data_stxdexpr - 1] = datum;
+		replaces[Anum_pg_statistic_ext_data_stxdexpr - 1] = true;
+	}
+	else
+		nulls[Anum_pg_statistic_ext_data_stxdexpr - 1] = true;
+
+	upsert_pg_statistic_ext_data(values, nulls, replaces);
+
+	heap_freetuple(tup);
+	table_close(pg_stext, RowExclusiveLock);
+
+	if (atttypids != NULL)
+		pfree(atttypids);
+	if (atttypmods != NULL)
+		pfree(atttypmods);
+	if (atttypcolls != NULL)
+		pfree(atttypcolls);
+	return success;
+}
+
+ /*
+  * The MCV is an array of records, but this is expected as 4 separate arrays.
+  * It is not possible to have a generic input function for pg_mcv_list
+  * because the most_common_values is a composite type with element types
+  * defined by the specific statistics object.
+  */
+static Datum
+import_mcvlist(HeapTuple tup, int elevel, int numattrs, Oid *atttypids,
+			   int32 *atttypmods, Oid *atttypcolls,
+			   ArrayType *mcv_arr, ArrayType *nulls_arr, ArrayType *freqs_arr,
+			   ArrayType *base_freqs_arr)
+{
+	int			nitems;
+
+	MCVList    *mcvlist;
+	bytea	   *bytes;
+
+	Datum	   *mcv_elems;
+	bool	   *mcv_nulls;
+	int			check_nummcv;
+
+	bool	   *mcv_elem_nulls;
+	float8	   *freqs;
+	float8	   *base_freqs;
+
+	HeapTuple	   *vatuples;
+	VacAttrStats  **vastats;
+
+	/*
+	 * The mcv_arr is an array of arrays of text, and we use it as the reference
+	 * array for checking the lengths of the other 3 arrays.
+	 */
+	if (ARR_NDIM(mcv_arr) != 2)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Parameters \"%s\" must be a text array of 2 dimensions.",
+						extarginfo[MOST_COMMON_VALS_ARG].argname)));
+		return (Datum) 0;
+	}
+
+	nitems = ARR_DIMS(mcv_arr)[0];
+
+	/* fixed length arrays that cannot contain NULLs */
+	if (!check_mcvlist_array(nulls_arr, MOST_COMMON_VAL_NULLS_ARG,
+							 2, nitems, elevel) ||
+		!check_mcvlist_array(freqs_arr, MOST_COMMON_FREQS_ARG,
+							 1, nitems, elevel ) ||
+		!check_mcvlist_array(base_freqs_arr, MOST_COMMON_BASE_FREQS_ARG,
+							 1, nitems, elevel ))
+		return (Datum) 0;
+
+	mcv_elem_nulls = (bool *) ARR_DATA_PTR(nulls_arr);
+	freqs = (float8 *) ARR_DATA_PTR(freqs_arr);
+	base_freqs = (float8 *) ARR_DATA_PTR(base_freqs_arr);
+
+	/*
+	 * Allocate the MCV list structure, set the global parameters.
+	 */
+	mcvlist = (MCVList *) palloc0(offsetof(MCVList, items) +
+								  (sizeof(MCVItem) * nitems));
+
+	mcvlist->magic = STATS_MCV_MAGIC;
+	mcvlist->type = STATS_MCV_TYPE_BASIC;
+	mcvlist->ndimensions = numattrs;
+	mcvlist->nitems = nitems;
+
+	deconstruct_array_builtin(mcv_arr, TEXTOID, &mcv_elems,
+							  &mcv_nulls, &check_nummcv);
+
+	Assert(check_nummcv == (nitems*numattrs));
+
+	/* Set the values for the 1-D arrays and allocate space for the 2-D arrays */
+	for (int i = 0; i < nitems; i++)
+	{
+		MCVItem    *item = &mcvlist->items[i];
+
+		item->frequency = freqs[i];
+		item->base_frequency = base_freqs[i];
+		item->values = (Datum *) palloc0(sizeof(Datum) * numattrs);
+		item->isnull = (bool *) palloc0(sizeof(bool) * numattrs);
+	}
+
+	/* Walk through each dimension */
+	for (int j = 0; j < numattrs; j++)
+	{
+		FmgrInfo	finfo;
+		Oid			ioparam;
+		Oid			infunc;
+	  	int			index = j;
+
+		getTypeInputInfo(atttypids[j], &infunc, &ioparam);
+		fmgr_info(infunc, &finfo);
+
+		/* store info about data type OIDs */
+		mcvlist->types[j] = atttypids[j];
+
+		for (int i = 0; i < nitems; i++)
+		{
+			MCVItem    *item = &mcvlist->items[i];
+
+			/* These should be in agreement, but just to be safe check both */
+			if (mcv_elem_nulls[index] || mcv_nulls[index])
+			{
+				item->values[j] = (Datum) 0;
+				item->isnull[j] = true;
+			}
+			else
+			{
+				char *s = TextDatumGetCString(mcv_elems[index]);
+				ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+				if (!InputFunctionCallSafe(&finfo, s, ioparam, atttypmods[j], 
+										   (fmNodePtr) &escontext, &item->values[j]))
+				{
+					ereport(elevel,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							errmsg("MCV elemement \"%s\" does not match expected input type.", s)));
+					return (Datum) 0;
+				}
+
+				pfree(s);
+			}
+
+			index += numattrs;
+		}
+	}
+
+	/*
+	 * The function statext_mcv_serialize() requires an array of pointers
+	 * to VacAttrStats records, but only a few fields within those records
+	 * have to be filled out.
+	 */
+	vastats = (VacAttrStats **) palloc0(numattrs * sizeof(VacAttrStats));
+	vatuples = (HeapTuple *) palloc0(numattrs * sizeof(HeapTuple));
+
+	for (int i = 0; i < numattrs; i++)
+	{
+		Oid			typid = atttypids[i];
+		HeapTuple	typtuple;
+
+		typtuple = SearchSysCacheCopy1(TYPEOID, ObjectIdGetDatum(typid));
+
+		if (!HeapTupleIsValid(typtuple))
+			elog(ERROR, "cache lookup failed for type %u", typid);
+
+		vatuples[i] = typtuple;
+
+		vastats[i] = palloc0(sizeof(VacAttrStats));
+
+		vastats[i]->attrtype = (Form_pg_type) GETSTRUCT(typtuple);
+		vastats[i]->attrtypid = typid;
+		vastats[i]->attrcollid = atttypcolls[i];
+	}
+
+	bytes = statext_mcv_serialize(mcvlist, vastats);
+
+	for (int i = 0; i < numattrs; i++)
+	{
+		pfree(vatuples[i]);
+		pfree(vastats[i]);
+	}
+	pfree((void *) vatuples);
+	pfree((void *) vastats);
+
+	if (bytes == NULL)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("Unable to import mcv list")));
+		return (Datum) 0;
+	}
+
+	for (int i = 0; i < nitems; i++)
+	{
+		MCVItem    *item = &mcvlist->items[i];
+
+		pfree(item->values);
+		pfree(item->isnull);
+	}
+	pfree(mcvlist);
+	pfree(mcv_elems);
+	pfree(mcv_nulls);
+
+	return PointerGetDatum(bytes);
+}
+
+/*
+ * Consistency checks to ensure that other mcvlist arrays are in alignment
+ * with the mcv array.
+ */
+static
+bool check_mcvlist_array(ArrayType *arr, int argindex, int required_ndims,
+						 int mcv_length, int elevel)
+{
+	if (ARR_NDIM(arr) != required_ndims)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Parameter \"%s\" must be an array of %d dimensions.",
+						extarginfo[argindex].argname, required_ndims)));
+		return false;
+	}
+
+	if (array_contains_nulls(arr))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Array \"%s\" cannot contain NULLs.",
+						extarginfo[argindex].argname)));
+		return false;
+	}
+
+	if (ARR_DIMS(arr)[0] != mcv_length)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Parameters \"%s\" must have the same number of elements as \"%s\"",
+						extarginfo[argindex].argname,
+						extarginfo[MOST_COMMON_VALS_ARG].argname)));
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Create the stxdexprs datum using the user input in an array of array of
+ * text, referenced against the datatypes for the expressions.
+ */
+static Datum
+import_expressions(Relation pgsd, int elevel, int numexprs,
+								Oid *atttypids, int32 *atttypmods,
+								Oid *atttypcolls, ArrayType *exprs_arr)
+{
+	Datum	   *exprs_elems;
+	bool	   *exprs_nulls;
+	int			check_numexprs;
+	int			offset = 0;
+
+	FmgrInfo	array_in_fn;
+
+	Oid			pgstypoid = get_rel_type_id(StatisticRelationId);
+
+	ArrayBuildState *astate = NULL;
+
+
+	if (ARR_NDIM(exprs_arr) != 2)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Parameter \"%s\" must be a text array of 2 dimensions.",
+						extarginfo[EXPRESSIONS_ARG].argname)));
+		return (Datum) 0;
+	}
+
+	if (ARR_DIMS(exprs_arr)[0] != numexprs)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Parameter \"%s\" must have an outer dimension of %d elements.",
+						extarginfo[EXPRESSIONS_ARG].argname, numexprs)));
+		return (Datum) 0;
+	}
+	if (ARR_DIMS(exprs_arr)[1] != NUM_ATTRIBUTE_STATS_ELEMS)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Parameter \"%s\" must have an inner dimension of %d elements.",
+						extarginfo[EXPRESSIONS_ARG].argname,
+						NUM_ATTRIBUTE_STATS_ELEMS)));
+		return (Datum) 0;
+	}
+
+	fmgr_info(F_ARRAY_IN, &array_in_fn);
+
+	deconstruct_array_builtin(exprs_arr, TEXTOID, &exprs_elems,
+							  &exprs_nulls, &check_numexprs);
+
+	for (int i = 0; i < numexprs; i++)
+	{
+		Oid				typid = atttypids[i];
+		int32			typmod = atttypmods[i];
+		Oid				stacoll = atttypcolls[i];
+		TypeCacheEntry *typcache;
+
+		Oid			elemtypid = InvalidOid;
+		Oid			elem_eq_opr = InvalidOid;
+
+		bool		ok;
+
+		Datum		values[Natts_pg_statistic];
+		bool		nulls[Natts_pg_statistic];
+		bool		replaces[Natts_pg_statistic];
+
+		HeapTuple	pgstup;
+		Datum		pgstdat;
+
+		/* finds the right operators even if atttypid is a domain */
+		typcache = lookup_type_cache(typid, TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR);
+
+		init_empty_stats_tuple(InvalidOid, InvalidAttrNumber, false,
+							   values, nulls, replaces);
+
+		if (!exprs_nulls[offset + NULL_FRAC_ELEM])
+		{
+			ok = text_to_float4(exprs_elems[offset + NULL_FRAC_ELEM], 
+								&values[Anum_pg_statistic_stanullfrac - 1]);
+
+			if (!ok)
+			{
+				char *s = TextDatumGetCString(exprs_elems[offset + NULL_FRAC_ELEM]);
+
+				ereport(elevel,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						errmsg("Expression %s element \"%s\" does not match expected input type.",
+							   extexprarginfo[NULL_FRAC_ELEM].argname, s)));
+				pfree(s);
+				return (Datum) 0;
+			}
+		}
+
+		if (!exprs_nulls[offset + AVG_WIDTH_ELEM])
+		{
+			ok = text_to_int4(exprs_elems[offset + AVG_WIDTH_ELEM],
+							  &values[Anum_pg_statistic_stawidth -1 ]);
+
+			if (!ok)
+			{
+				char *s = TextDatumGetCString(exprs_elems[offset + NULL_FRAC_ELEM]);
+
+				ereport(elevel,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						errmsg("Expression %s element \"%s\" does not match expected input type.",
+							   extexprarginfo[AVG_WIDTH_ELEM].argname, s)));
+				pfree(s);
+				return (Datum) 0;
+			}
+		}
+
+		if (!exprs_nulls[offset + N_DISTINCT_ELEM])
+		{
+			ok = text_to_float4(exprs_elems[offset + N_DISTINCT_ELEM],
+							    &values[Anum_pg_statistic_stadistinct - 1]);
+
+			if (!ok)
+			{
+				char *s = TextDatumGetCString(exprs_elems[offset + NULL_FRAC_ELEM]);
+
+				ereport(elevel,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						errmsg("Expression %s element \"%s\" does not match expected input type.",
+							   extexprarginfo[N_DISTINCT_ELEM].argname, s)));
+				pfree(s);
+				return (Datum) 0;
+			}
+		}
+
+		/*
+		 * The STAKIND statistics are the same as the ones found in attribute stats.
+		 * However, these are all derived from text columns, whereas the ones
+		 * derived for attribute stats are a mix of datatypes. This limits the
+		 * opportunities for code sharing between the two.
+		 */
+
+		/* STATISTIC_KIND_MCV */
+		if (exprs_nulls[offset + MOST_COMMON_VALS_ELEM] !=
+			exprs_nulls[offset + MOST_COMMON_FREQS_ELEM])
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("Expression %s and %s must both be NOT NULL or both NULL.",
+							extexprarginfo[MOST_COMMON_VALS_ELEM].argname,
+							extexprarginfo[MOST_COMMON_FREQS_ELEM].argname)));
+			return (Datum) 0;
+		}
+
+		if (!exprs_nulls[offset + MOST_COMMON_VALS_ELEM])
+		{
+			Datum		stavalues;
+			Datum		stanumbers;
+
+			stavalues = text_to_stavalues(extexprarginfo[MOST_COMMON_VALS_ELEM].argname,
+										  &array_in_fn, exprs_elems[offset + MOST_COMMON_VALS_ELEM],
+										  typid, typmod, elevel, &ok);
+
+			if (!ok)
+				return (Datum) 0;
+
+			stanumbers = text_to_stavalues(extexprarginfo[MOST_COMMON_VALS_ELEM].argname,
+										   &array_in_fn, exprs_elems[offset + MOST_COMMON_FREQS_ELEM],
+										   FLOAT4OID, -1, elevel, &ok);
+
+			if (!ok)
+				return (Datum) 0;
+
+			set_stats_slot(values, nulls, replaces,
+						   STATISTIC_KIND_MCV,
+						   typcache->eq_opr, stacoll,
+						   stanumbers, false, stavalues, false);
+		}
+
+		/* STATISTIC_KIND_HISTOGRAM */
+		if (!exprs_nulls[offset + HISTOGRAM_BOUNDS_ELEM])
+		{
+			Datum		stavalues;
+
+			stavalues = text_to_stavalues(extexprarginfo[HISTOGRAM_BOUNDS_ELEM].argname,
+										  &array_in_fn, exprs_elems[offset + HISTOGRAM_BOUNDS_ELEM],
+										  typid, typmod, elevel, &ok);
+
+			if (!ok)
+				return (Datum) 0;
+
+			set_stats_slot(values, nulls, replaces,
+						   STATISTIC_KIND_HISTOGRAM,
+						   typcache->lt_opr, stacoll,
+						   0, true, stavalues, false);
+		}
+
+		/* STATISTIC_KIND_CORRELATION */
+		if (!exprs_nulls[offset + CORRELATION_ELEM])
+		{
+			Datum		corr[] = {(Datum) 0};
+			ArrayType  *arry;
+			Datum		stanumbers;
+
+			ok = text_to_float4(exprs_elems[offset + CORRELATION_ELEM], &corr[0]);
+
+			if (!ok)
+			{
+				char *s = TextDatumGetCString(exprs_elems[offset + CORRELATION_ELEM]);
+
+				ereport(elevel,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						errmsg("Expression %s element \"%s\" does not match expected input type.",
+							   extexprarginfo[CORRELATION_ELEM].argname, s)));
+				return (Datum) 0;
+			}
+
+			arry = construct_array_builtin(corr, 1, FLOAT4OID);
+
+			stanumbers = PointerGetDatum(arry);
+
+			set_stats_slot(values, nulls, replaces,
+						STATISTIC_KIND_CORRELATION,
+						typcache->lt_opr, stacoll,
+						stanumbers, false, 0, true);
+		}
+
+		/* STATISTIC_KIND_MCELEM */
+		if (exprs_nulls[offset + MOST_COMMON_ELEMS_ELEM] !=
+			exprs_nulls[offset + MOST_COMMON_ELEM_FREQS_ELEM])
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("Expression %s and %s must both be NOT NULL or both NULL.",
+							extexprarginfo[MOST_COMMON_ELEMS_ELEM].argname,
+							extexprarginfo[MOST_COMMON_ELEM_FREQS_ELEM].argname)));
+			return (Datum) 0;
+		}
+
+		/*
+		 * We only need to fetch element type and eq operator if we have a stat of
+		 * type MCELEM or DECHIST.
+		 */
+		if (!exprs_nulls[offset + MOST_COMMON_ELEMS_ELEM] ||
+			!exprs_nulls[offset + ELEM_COUNT_HISTOGRAM_ELEM])
+		{
+			if (!get_elem_stat_type(typid, typcache->typtype,
+									elevel, &elemtypid, &elem_eq_opr))
+			{
+				ereport(elevel,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						(errmsg("unable to determine element type of expression"))));
+				return (Datum) 0;
+			}
+		}
+
+		if (!exprs_nulls[offset + MOST_COMMON_ELEMS_ELEM])
+		{
+			Datum		stavalues;
+			Datum		stanumbers;
+
+			stavalues = text_to_stavalues(extexprarginfo[MOST_COMMON_ELEMS_ELEM].argname,
+										  &array_in_fn,
+									   exprs_elems[offset + MOST_COMMON_ELEMS_ELEM],
+										  elemtypid, typmod,
+										  elevel, &ok);
+
+			if (!ok)
+				return (Datum) 0;
+
+			stanumbers = text_to_stavalues(extexprarginfo[MOST_COMMON_ELEM_FREQS_ELEM].argname,
+										   &array_in_fn, 
+									    exprs_elems[offset + MOST_COMMON_ELEM_FREQS_ELEM],
+										   FLOAT4OID, -1, elevel, &ok);
+
+			if (!ok)
+				return (Datum) 0;
+
+			set_stats_slot(values, nulls, replaces,
+						   STATISTIC_KIND_MCELEM,
+						   elem_eq_opr, stacoll,
+						   stanumbers, false, stavalues, false);
+		}
+
+		if (!exprs_nulls[offset + ELEM_COUNT_HISTOGRAM_ELEM])
+		{
+			Datum		stanumbers;
+
+			stanumbers = text_to_stavalues(extexprarginfo[ELEM_COUNT_HISTOGRAM_ELEM].argname,
+										   &array_in_fn, 
+										exprs_elems[offset + ELEM_COUNT_HISTOGRAM_ELEM],
+										   FLOAT4OID, -1, elevel, &ok);
+
+			if (!ok)
+				return (Datum) 0;
+
+			set_stats_slot(values, nulls, replaces, STATISTIC_KIND_DECHIST,
+						   elem_eq_opr, stacoll,
+						   stanumbers, false, 0, true);
+		}
+
+		/*
+		 * Currently there are no extended stats exports of the statistic kinds
+		 * STATISTIC_KIND_BOUNDS_HISTOGRAM or STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM
+		 * so these cannot be imported. These may be added in the future.
+		 */
+
+		pgstup = heap_form_tuple(RelationGetDescr(pgsd), values, nulls);
+		pgstdat = heap_copy_tuple_as_datum(pgstup, RelationGetDescr(pgsd));
+		astate = accumArrayResult(astate, pgstdat, false, pgstypoid,
+								  CurrentMemoryContext);
+
+		offset += NUM_ATTRIBUTE_STATS_ELEMS;
+	}
+
+	pfree(exprs_elems);
+	pfree(exprs_nulls);
+
+	return makeArrayResult(astate, CurrentMemoryContext);
+}
+
+static
+bool text_to_float4(Datum input, Datum *output)
+{
+	ErrorSaveContext	escontext = {T_ErrorSaveContext};
+
+	char	   *s;
+	bool		ok;
+
+	s = TextDatumGetCString(input);
+	ok = DirectInputFunctionCallSafe(float4in, s, InvalidOid, -1,
+									 (Node *) &escontext, output);
+
+	pfree(s);
+	return ok;
+}
+
+
+static
+bool text_to_int4(Datum input, Datum *output)
+{
+	ErrorSaveContext	escontext = {T_ErrorSaveContext};
+
+	char	   *s;
+	bool		ok;
+
+	s = TextDatumGetCString(input);
+	ok = DirectInputFunctionCallSafe(int4in, s, InvalidOid, -1,
+									 (Node *) &escontext, output);
+
+	pfree(s);
+	return ok;
+}
+
+static
+bool delete_pg_statistic_ext_data(Oid stxoid, bool inherited)
+{
+	Relation	sed = table_open(StatisticExtDataRelationId, RowExclusiveLock);
+	HeapTuple	oldtup;
+	bool		result = false;
+
+	/* Is there already a pg_statistic tuple for this attribute? */
+	oldtup = SearchSysCache2(STATEXTDATASTXOID,
+							 ObjectIdGetDatum(stxoid),
+							 BoolGetDatum(inherited));
+
+	if (HeapTupleIsValid(oldtup))
+	{
+		CatalogTupleDelete(sed, &oldtup->t_self);
+		ReleaseSysCache(oldtup);
+		result = true;
+	}
+
+	table_close(sed, RowExclusiveLock);
+
+	CommandCounterIncrement();
+
+	return result;
+}
+
+ /*
+  * Import statistics for a given statistics object.
+  *
+  * Inserts or replaces a row in pg_statistic_ext_data for the given relation
+  * and statistic object schema+name. It takes input parameters that
+  * correspond to columns in the view pg_stats_ext and pg_stats_ext_exprs.
+  *
+  * Parameters are only superficially validated. Omitting a parameter or
+  * passing NULL leaves the statistic unchanged.
+  *
+  */
+Datum
+pg_set_extended_stats(PG_FUNCTION_ARGS)
+{
+	extended_statistics_update(fcinfo, ERROR);
+	PG_RETURN_VOID();
+}
+
+
+Datum
+pg_restore_extended_stats(PG_FUNCTION_ARGS)
+{
+	LOCAL_FCINFO(positional_fcinfo, NUM_EXTENDED_STATS_ARGS);
+	bool		result = true;
+
+	InitFunctionCallInfoData(*positional_fcinfo, NULL, NUM_EXTENDED_STATS_ARGS,
+							 InvalidOid, NULL, NULL);
+
+	if (!stats_fill_fcinfo_from_arg_pairs(fcinfo, positional_fcinfo,
+										  extarginfo, WARNING))
+		result = false;
+
+	if (!extended_statistics_update(positional_fcinfo, WARNING))
+		result = false;
+
+	PG_RETURN_BOOL(result);
+}
+
+/*
+ * Delete statistics for the given statistics object.
+ */
+Datum
+pg_clear_extended_stats(PG_FUNCTION_ARGS)
+{
+	Oid			nspoid;
+	Name		stxname;
+	bool		inherited;
+	Relation	pg_stext;
+	HeapTuple	tup;
+
+	Form_pg_statistic_ext stxform;
+
+
+	stats_check_required_arg(fcinfo, extarginfo, STATSCHEMA_ARG);
+	nspoid = PG_GETARG_OID(STATSCHEMA_ARG);
+	stats_check_required_arg(fcinfo, extarginfo, STATNAME_ARG);
+	stxname = PG_GETARG_NAME(STATNAME_ARG);
+	stats_check_required_arg(fcinfo, extarginfo, INHERITED_ARG);
+	inherited = PG_GETARG_NAME(INHERITED_ARG);
+
+	if (RecoveryInProgress())
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("recovery is in progress"),
+				 errhint("Statistics cannot be modified during recovery.")));
+
+	pg_stext = table_open(StatisticExtRelationId, RowExclusiveLock);
+	tup = get_pg_statistic_ext(pg_stext, nspoid, stxname);
+
+	if (!HeapTupleIsValid(tup))
+	{
+		table_close(pg_stext, RowExclusiveLock);
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("Extended Statistics Object \"%s\".\"%s\" not found.",
+						get_namespace_name(nspoid),
+						NameStr(*stxname))));
+		return false;
+	}
+
+	stxform = (Form_pg_statistic_ext) GETSTRUCT(tup);
+
+	stats_lock_check_privileges(stxform->stxrelid);
+
+	delete_pg_statistic_ext_data(stxform->oid, inherited);
+	heap_freetuple(tup);
+	table_close(pg_stext, RowExclusiveLock);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index fb50da1cd8..9f3b45fc55 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -1414,11 +1414,15 @@ SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import.complex_type,
 UNION ALL
 SELECT 4, 'four', NULL, int4range(0,100), NULL;
 CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
+CREATE STATISTICS stats_import.test_stat ON name, comp, lower(arange), array_length(tags,1)
+FROM stats_import.test;
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 CREATE TABLE stats_import.test_clone ( LIKE stats_import.test )
     WITH (autovacuum_enabled = false);
 CREATE INDEX is_odd_clone ON stats_import.test_clone(((comp).a % 2 = 1));
+CREATE STATISTICS stats_import.test_stat_clone ON name, comp, lower(arange), array_length(tags,1)
+FROM stats_import.test_clone;
 --
 -- Copy stats from test to test_clone, and is_odd to is_odd_clone
 --
@@ -1801,6 +1805,332 @@ WHERE s.starelid = 'stats_import.is_odd'::regclass;
 ---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
 (0 rows)
 
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        n_distinct => '{"2, 3": 4, "2, -1": 4, "2, -2": 4, "3, -1": 4, "3, -2": 4, "-1, -2": 3, "2, 3, -1": 4, "2, 3, -2": 4, "2, -1, -2": 4, "3, -1, -2": 4, "2, 3, -1, -2": 4}'::pg_ndistinct
+      );
+ pg_set_extended_stats 
+-----------------------
+ 
+(1 row)
+
+SELECT
+    e.n_distinct, e.dependencies, e.most_common_vals, e.most_common_val_nulls,
+    e.most_common_freqs, e.most_common_base_freqs
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false
+\gx
+-[ RECORD 1 ]----------+----------------------------------------------------------------------------------------------------------------------------------------------------------
+n_distinct             | {"2, 3": 4, "2, -1": 4, "2, -2": 4, "3, -1": 4, "3, -2": 4, "-1, -2": 3, "2, 3, -1": 4, "2, 3, -2": 4, "2, -1, -2": 4, "3, -1, -2": 4, "2, 3, -1, -2": 4}
+dependencies           | 
+most_common_vals       | 
+most_common_val_nulls  | 
+most_common_freqs      | 
+most_common_base_freqs | 
+
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        dependencies => '{"2 => 3": 1.000000, "2 => -1": 1.000000, "2 => -2": 1.000000, "3 => 2": 1.000000, "3 => -1": 1.000000, "3 => -2": 1.000000, "-1 => 2": 0.500000, "-1 => 3": 0.500000, "-1 => -2": 1.000000, "-2 => 2": 0.500000, "-2 => 3": 0.500000, "-2 => -1": 1.000000, "2, 3 => -1": 1.000000, "2, 3 => -2": 1.000000, "2, -1 => 3": 1.000000, "2, -1 => -2": 1.000000, "2, -2 => 3": 1.000000, "2, -2 => -1": 1.000000, "3, -1 => 2": 1.000000, "3, -1 => -2": 1.000000, "3, -2 => 2": 1.000000, "3, -2 => -1": 1.000000, "-1, -2 => 2": 0.500000, "-1, -2 => 3": 0.500000, "2, 3, -1 => -2": 1.000000, "2, 3, -2 => -1": 1.000000, "2, -1, -2 => 3": 1.000000, "3, -1, -2 => 2": 1.000000}'::pg_dependencies
+      );
+ pg_set_extended_stats 
+-----------------------
+ 
+(1 row)
+
+SELECT
+    e.n_distinct, e.dependencies, e.most_common_vals, e.most_common_val_nulls,
+    e.most_common_freqs, e.most_common_base_freqs
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false
+\gx
+-[ RECORD 1 ]----------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+n_distinct             | {"2, 3": 4, "2, -1": 4, "2, -2": 4, "3, -1": 4, "3, -2": 4, "-1, -2": 3, "2, 3, -1": 4, "2, 3, -2": 4, "2, -1, -2": 4, "3, -1, -2": 4, "2, 3, -1, -2": 4}
+dependencies           | {"2 => 3": 1.000000, "2 => -1": 1.000000, "2 => -2": 1.000000, "3 => 2": 1.000000, "3 => -1": 1.000000, "3 => -2": 1.000000, "-1 => 2": 0.500000, "-1 => 3": 0.500000, "-1 => -2": 1.000000, "-2 => 2": 0.500000, "-2 => 3": 0.500000, "-2 => -1": 1.000000, "2, 3 => -1": 1.000000, "2, 3 => -2": 1.000000, "2, -1 => 3": 1.000000, "2, -1 => -2": 1.000000, "2, -2 => 3": 1.000000, "2, -2 => -1": 1.000000, "3, -1 => 2": 1.000000, "3, -1 => -2": 1.000000, "3, -2 => 2": 1.000000, "3, -2 => -1": 1.000000, "-1, -2 => 2": 0.500000, "-1, -2 => 3": 0.500000, "2, 3, -1 => -2": 1.000000, "2, 3, -2 => -1": 1.000000, "2, -1, -2 => 3": 1.000000, "3, -1, -2 => 2": 1.000000}
+most_common_vals       | 
+most_common_val_nulls  | 
+most_common_freqs      | 
+most_common_base_freqs | 
+
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        most_common_vals => '{{four,NULL,0,NULL},{one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")",1,2},{tre,"(3,3.3,TRE,03-03-2003,)",-1,3},{two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")",1,2}}'
+      );
+ERROR:  MCV parameters "most_common_vals", "most_common_val_nulls", "most_common_freqs", and "most_common_base_freqs" must be all specified if any are specified
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        most_common_val_nulls => '{{f,t,f,t},{f,f,f,f},{f,f,f,f},{f,f,f,f}}'
+      );
+ERROR:  MCV parameters "most_common_vals", "most_common_val_nulls", "most_common_freqs", and "most_common_base_freqs" must be all specified if any are specified
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        most_common_freqs => '{0.25,0.25,0.25,0.25}'
+      );
+ERROR:  MCV parameters "most_common_vals", "most_common_val_nulls", "most_common_freqs", and "most_common_base_freqs" must be all specified if any are specified
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        most_common_base_freqs => '{0.00390625,0.015625,0.00390625,0.015625}'
+      );
+ERROR:  MCV parameters "most_common_vals", "most_common_val_nulls", "most_common_freqs", and "most_common_base_freqs" must be all specified if any are specified
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        most_common_vals => '{{four,NULL,0,NULL},{one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")",1,2},{tre,"(3,3.3,TRE,03-03-2003,)",-1,3},{two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")",1,2}}',
+        most_common_val_nulls => '{{f,t,f,t},{f,f,f,f},{f,f,f,f},{f,f,f,f}}',
+        most_common_freqs => '{0.25,0.25,0.25,0.25}',
+        most_common_base_freqs => '{0.00390625,0.015625,0.00390625,0.015625}'
+      );
+ pg_set_extended_stats 
+-----------------------
+ 
+(1 row)
+
+SELECT
+    e.n_distinct, e.dependencies, e.most_common_vals, e.most_common_val_nulls,
+    e.most_common_freqs, e.most_common_base_freqs
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false
+\gx
+-[ RECORD 1 ]----------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+n_distinct             | {"2, 3": 4, "2, -1": 4, "2, -2": 4, "3, -1": 4, "3, -2": 4, "-1, -2": 3, "2, 3, -1": 4, "2, 3, -2": 4, "2, -1, -2": 4, "3, -1, -2": 4, "2, 3, -1, -2": 4}
+dependencies           | {"2 => 3": 1.000000, "2 => -1": 1.000000, "2 => -2": 1.000000, "3 => 2": 1.000000, "3 => -1": 1.000000, "3 => -2": 1.000000, "-1 => 2": 0.500000, "-1 => 3": 0.500000, "-1 => -2": 1.000000, "-2 => 2": 0.500000, "-2 => 3": 0.500000, "-2 => -1": 1.000000, "2, 3 => -1": 1.000000, "2, 3 => -2": 1.000000, "2, -1 => 3": 1.000000, "2, -1 => -2": 1.000000, "2, -2 => 3": 1.000000, "2, -2 => -1": 1.000000, "3, -1 => 2": 1.000000, "3, -1 => -2": 1.000000, "3, -2 => 2": 1.000000, "3, -2 => -1": 1.000000, "-1, -2 => 2": 0.500000, "-1, -2 => 3": 0.500000, "2, 3, -1 => -2": 1.000000, "2, 3, -2 => -1": 1.000000, "2, -1, -2 => 3": 1.000000, "3, -1, -2 => 2": 1.000000}
+most_common_vals       | {{four,NULL,0,NULL},{one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")",1,2},{tre,"(3,3.3,TRE,03-03-2003,)",-1,3},{two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")",1,2}}
+most_common_val_nulls  | {{f,t,f,t},{f,f,f,f},{f,f,f,f},{f,f,f,f}}
+most_common_freqs      | {0.25,0.25,0.25,0.25}
+most_common_base_freqs | {0.00390625,0.015625,0.00390625,0.015625}
+
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        exprs => '{{0,4,-0.75,"{1}","{0.5}","{-1,0}",-0.6,NULL,NULL,NULL},{0.25,4,-0.5,"{2}","{0.5}",NULL,1,NULL,NULL,NULL}}'
+      );
+ pg_set_extended_stats 
+-----------------------
+ 
+(1 row)
+
+SELECT
+    e.inherited, e.null_frac, e.avg_width, e.n_distinct, e.most_common_vals,
+    e.most_common_freqs, e.histogram_bounds, e.correlation,
+    e.most_common_elems, e.most_common_elem_freqs, e.elem_count_histogram
+FROM pg_stats_ext_exprs AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+and e.inherited = false
+\gx
+-[ RECORD 1 ]----------+-------
+inherited              | f
+null_frac              | 0
+avg_width              | 4
+n_distinct             | -0.75
+most_common_vals       | {1}
+most_common_freqs      | {0.5}
+histogram_bounds       | {-1,0}
+correlation            | -0.6
+most_common_elems      | 
+most_common_elem_freqs | 
+elem_count_histogram   | 
+-[ RECORD 2 ]----------+-------
+inherited              | f
+null_frac              | 0.25
+avg_width              | 4
+n_distinct             | -0.5
+most_common_vals       | {2}
+most_common_freqs      | {0.5}
+histogram_bounds       | 
+correlation            | 1
+most_common_elems      | 
+most_common_elem_freqs | 
+elem_count_histogram   | 
+
+SELECT
+    pg_catalog.pg_clear_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false);
+ pg_clear_extended_stats 
+-------------------------
+ 
+(1 row)
+
+SELECT COUNT(*)
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false;
+ count 
+-------
+     0
+(1 row)
+
+SELECT COUNT(*)
+FROM pg_stats_ext_exprs AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false;
+ count 
+-------
+     0
+(1 row)
+
+--
+-- Copy stats from test_stat to test_stat_clone
+--
+SELECT
+    e.statistics_name,
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', e.statistics_schemaname::regnamespace,
+        'statistics_name', 'test_stat_clone'::name,
+        'inherited', e.inherited,
+        'n_distinct', e.n_distinct,
+        'dependencies', e.dependencies,
+        'most_common_vals', e.most_common_vals,
+        'most_common_val_nulls', e.most_common_val_nulls,
+        'most_common_freqs', e.most_common_freqs,
+        'most_common_base_freqs', e.most_common_base_freqs,
+        'exprs', x.exprs
+    )
+FROM pg_stats_ext AS e
+CROSS JOIN LATERAL (
+    SELECT
+        array_agg(
+            ARRAY[ee.null_frac::text, ee.avg_width::text,
+                  ee.n_distinct::text, ee.most_common_vals::text,
+                  ee.most_common_freqs::text, ee.histogram_bounds::text,
+                  ee.correlation::text, ee.most_common_elems::text,
+                  ee.most_common_elem_freqs::text,
+                  ee.elem_count_histogram::text])
+    FROM pg_stats_ext_exprs AS ee
+    WHERE ee.statistics_schemaname = e.statistics_schemaname
+    AND ee.statistics_name = e.statistics_name
+    AND ee.inherited = e.inherited
+    ) AS x(exprs)
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat';
+ statistics_name | pg_restore_extended_stats 
+-----------------+---------------------------
+ test_stat       | t
+(1 row)
+
+SELECT o.inherited,
+       o.n_distinct, o.dependencies, o.most_common_vals,
+       o.most_common_val_nulls, o.most_common_freqs,
+       o.most_common_base_freqs
+FROM pg_stats_ext AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat'
+EXCEPT
+SELECT n.inherited,
+       n.n_distinct, n.dependencies, n.most_common_vals,
+       n.most_common_val_nulls, n.most_common_freqs,
+       n.most_common_base_freqs
+FROM pg_stats_ext AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone';
+ inherited | n_distinct | dependencies | most_common_vals | most_common_val_nulls | most_common_freqs | most_common_base_freqs 
+-----------+------------+--------------+------------------+-----------------------+-------------------+------------------------
+(0 rows)
+
+SELECT n.inherited,
+       n.n_distinct, n.dependencies, n.most_common_vals,
+       n.most_common_val_nulls, n.most_common_freqs,
+       n.most_common_base_freqs
+FROM pg_stats_ext AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone'
+EXCEPT
+SELECT o.inherited,
+       o.n_distinct, o.dependencies, o.most_common_vals,
+       o.most_common_val_nulls, o.most_common_freqs,
+       o.most_common_base_freqs
+FROM pg_stats_ext AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat';
+ inherited | n_distinct | dependencies | most_common_vals | most_common_val_nulls | most_common_freqs | most_common_base_freqs 
+-----------+------------+--------------+------------------+-----------------------+-------------------+------------------------
+(0 rows)
+
+SELECT o.inherited,
+       o.null_frac, o.avg_width, o.n_distinct,
+       o.most_common_vals::text AS most_common_vals,
+       o.most_common_freqs,
+       o.histogram_bounds::text AS histogram_bounds,
+       o.correlation,
+       o.most_common_elems::text AS most_common_elems,
+       o.most_common_elem_freqs, o.elem_count_histogram
+FROM pg_stats_ext_exprs AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat'
+EXCEPT
+SELECT n.inherited,
+       n.null_frac, n.avg_width, n.n_distinct,
+       n.most_common_vals::text AS most_common_vals,
+       n.most_common_freqs,
+       n.histogram_bounds::text AS histogram_bounds,
+       n.correlation,
+       n.most_common_elems::text AS most_common_elems,
+       n.most_common_elem_freqs, n.elem_count_histogram
+FROM pg_stats_ext_exprs AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone';
+ inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram 
+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------
+(0 rows)
+
+SELECT n.inherited,
+       n.null_frac, n.avg_width, n.n_distinct,
+       n.most_common_vals::text AS most_common_vals,
+       n.most_common_freqs,
+       n.histogram_bounds::text AS histogram_bounds,
+       n.correlation,
+       n.most_common_elems::text AS most_common_elems,
+       n.most_common_elem_freqs, n.elem_count_histogram
+FROM pg_stats_ext_exprs AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone'
+EXCEPT
+SELECT o.inherited,
+       o.null_frac, o.avg_width, o.n_distinct,
+       o.most_common_vals::text AS most_common_vals,
+       o.most_common_freqs,
+       o.histogram_bounds::text AS histogram_bounds,
+       o.correlation,
+       o.most_common_elems::text AS most_common_elems,
+       o.most_common_elem_freqs, o.elem_count_histogram
+FROM pg_stats_ext_exprs AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat';
+ inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram 
+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------
+(0 rows)
+
 DROP SCHEMA stats_import CASCADE;
 NOTICE:  drop cascades to 6 other objects
 DETAIL:  drop cascades to type stats_import.complex_type
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index d3058bf8f6..98aa934d12 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -1062,6 +1062,9 @@ SELECT 4, 'four', NULL, int4range(0,100), NULL;
 
 CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
 
+CREATE STATISTICS stats_import.test_stat ON name, comp, lower(arange), array_length(tags,1)
+FROM stats_import.test;
+
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 
@@ -1070,6 +1073,9 @@ CREATE TABLE stats_import.test_clone ( LIKE stats_import.test )
 
 CREATE INDEX is_odd_clone ON stats_import.test_clone(((comp).a % 2 = 1));
 
+CREATE STATISTICS stats_import.test_stat_clone ON name, comp, lower(arange), array_length(tags,1)
+FROM stats_import.test_clone;
+
 --
 -- Copy stats from test to test_clone, and is_odd to is_odd_clone
 --
@@ -1381,4 +1387,242 @@ FROM pg_statistic s
 JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
 WHERE s.starelid = 'stats_import.is_odd'::regclass;
 
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        n_distinct => '{"2, 3": 4, "2, -1": 4, "2, -2": 4, "3, -1": 4, "3, -2": 4, "-1, -2": 3, "2, 3, -1": 4, "2, 3, -2": 4, "2, -1, -2": 4, "3, -1, -2": 4, "2, 3, -1, -2": 4}'::pg_ndistinct
+      );
+
+SELECT
+    e.n_distinct, e.dependencies, e.most_common_vals, e.most_common_val_nulls,
+    e.most_common_freqs, e.most_common_base_freqs
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false
+\gx
+
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        dependencies => '{"2 => 3": 1.000000, "2 => -1": 1.000000, "2 => -2": 1.000000, "3 => 2": 1.000000, "3 => -1": 1.000000, "3 => -2": 1.000000, "-1 => 2": 0.500000, "-1 => 3": 0.500000, "-1 => -2": 1.000000, "-2 => 2": 0.500000, "-2 => 3": 0.500000, "-2 => -1": 1.000000, "2, 3 => -1": 1.000000, "2, 3 => -2": 1.000000, "2, -1 => 3": 1.000000, "2, -1 => -2": 1.000000, "2, -2 => 3": 1.000000, "2, -2 => -1": 1.000000, "3, -1 => 2": 1.000000, "3, -1 => -2": 1.000000, "3, -2 => 2": 1.000000, "3, -2 => -1": 1.000000, "-1, -2 => 2": 0.500000, "-1, -2 => 3": 0.500000, "2, 3, -1 => -2": 1.000000, "2, 3, -2 => -1": 1.000000, "2, -1, -2 => 3": 1.000000, "3, -1, -2 => 2": 1.000000}'::pg_dependencies
+      );
+
+SELECT
+    e.n_distinct, e.dependencies, e.most_common_vals, e.most_common_val_nulls,
+    e.most_common_freqs, e.most_common_base_freqs
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false
+\gx
+
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        most_common_vals => '{{four,NULL,0,NULL},{one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")",1,2},{tre,"(3,3.3,TRE,03-03-2003,)",-1,3},{two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")",1,2}}'
+      );
+
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        most_common_val_nulls => '{{f,t,f,t},{f,f,f,f},{f,f,f,f},{f,f,f,f}}'
+      );
+
+
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        most_common_freqs => '{0.25,0.25,0.25,0.25}'
+      );
+
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        most_common_base_freqs => '{0.00390625,0.015625,0.00390625,0.015625}'
+      );
+
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        most_common_vals => '{{four,NULL,0,NULL},{one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")",1,2},{tre,"(3,3.3,TRE,03-03-2003,)",-1,3},{two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")",1,2}}',
+        most_common_val_nulls => '{{f,t,f,t},{f,f,f,f},{f,f,f,f},{f,f,f,f}}',
+        most_common_freqs => '{0.25,0.25,0.25,0.25}',
+        most_common_base_freqs => '{0.00390625,0.015625,0.00390625,0.015625}'
+      );
+
+SELECT
+    e.n_distinct, e.dependencies, e.most_common_vals, e.most_common_val_nulls,
+    e.most_common_freqs, e.most_common_base_freqs
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false
+\gx
+
+SELECT
+    pg_catalog.pg_set_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false,
+        exprs => '{{0,4,-0.75,"{1}","{0.5}","{-1,0}",-0.6,NULL,NULL,NULL},{0.25,4,-0.5,"{2}","{0.5}",NULL,1,NULL,NULL,NULL}}'
+      );
+
+SELECT
+    e.inherited, e.null_frac, e.avg_width, e.n_distinct, e.most_common_vals,
+    e.most_common_freqs, e.histogram_bounds, e.correlation,
+    e.most_common_elems, e.most_common_elem_freqs, e.elem_count_histogram
+FROM pg_stats_ext_exprs AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+and e.inherited = false
+\gx
+
+SELECT
+    pg_catalog.pg_clear_extended_stats(
+        statistics_schemaname => 'stats_import'::regnamespace,
+        statistics_name => 'test_stat_clone'::name,
+        inherited => false);
+
+SELECT COUNT(*)
+FROM pg_stats_ext AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false;
+
+SELECT COUNT(*)
+FROM pg_stats_ext_exprs AS e
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat_clone'
+AND e.inherited = false;
+
+--
+-- Copy stats from test_stat to test_stat_clone
+--
+SELECT
+    e.statistics_name,
+    pg_catalog.pg_restore_extended_stats(
+        'statistics_schemaname', e.statistics_schemaname::regnamespace,
+        'statistics_name', 'test_stat_clone'::name,
+        'inherited', e.inherited,
+        'n_distinct', e.n_distinct,
+        'dependencies', e.dependencies,
+        'most_common_vals', e.most_common_vals,
+        'most_common_val_nulls', e.most_common_val_nulls,
+        'most_common_freqs', e.most_common_freqs,
+        'most_common_base_freqs', e.most_common_base_freqs,
+        'exprs', x.exprs
+    )
+FROM pg_stats_ext AS e
+CROSS JOIN LATERAL (
+    SELECT
+        array_agg(
+            ARRAY[ee.null_frac::text, ee.avg_width::text,
+                  ee.n_distinct::text, ee.most_common_vals::text,
+                  ee.most_common_freqs::text, ee.histogram_bounds::text,
+                  ee.correlation::text, ee.most_common_elems::text,
+                  ee.most_common_elem_freqs::text,
+                  ee.elem_count_histogram::text])
+    FROM pg_stats_ext_exprs AS ee
+    WHERE ee.statistics_schemaname = e.statistics_schemaname
+    AND ee.statistics_name = e.statistics_name
+    AND ee.inherited = e.inherited
+    ) AS x(exprs)
+WHERE e.statistics_schemaname = 'stats_import'
+AND e.statistics_name = 'test_stat';
+
+SELECT o.inherited,
+       o.n_distinct, o.dependencies, o.most_common_vals,
+       o.most_common_val_nulls, o.most_common_freqs,
+       o.most_common_base_freqs
+FROM pg_stats_ext AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat'
+EXCEPT
+SELECT n.inherited,
+       n.n_distinct, n.dependencies, n.most_common_vals,
+       n.most_common_val_nulls, n.most_common_freqs,
+       n.most_common_base_freqs
+FROM pg_stats_ext AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone';
+
+SELECT n.inherited,
+       n.n_distinct, n.dependencies, n.most_common_vals,
+       n.most_common_val_nulls, n.most_common_freqs,
+       n.most_common_base_freqs
+FROM pg_stats_ext AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone'
+EXCEPT
+SELECT o.inherited,
+       o.n_distinct, o.dependencies, o.most_common_vals,
+       o.most_common_val_nulls, o.most_common_freqs,
+       o.most_common_base_freqs
+FROM pg_stats_ext AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat';
+
+SELECT o.inherited,
+       o.null_frac, o.avg_width, o.n_distinct,
+       o.most_common_vals::text AS most_common_vals,
+       o.most_common_freqs,
+       o.histogram_bounds::text AS histogram_bounds,
+       o.correlation,
+       o.most_common_elems::text AS most_common_elems,
+       o.most_common_elem_freqs, o.elem_count_histogram
+FROM pg_stats_ext_exprs AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat'
+EXCEPT
+SELECT n.inherited,
+       n.null_frac, n.avg_width, n.n_distinct,
+       n.most_common_vals::text AS most_common_vals,
+       n.most_common_freqs,
+       n.histogram_bounds::text AS histogram_bounds,
+       n.correlation,
+       n.most_common_elems::text AS most_common_elems,
+       n.most_common_elem_freqs, n.elem_count_histogram
+FROM pg_stats_ext_exprs AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone';
+
+SELECT n.inherited,
+       n.null_frac, n.avg_width, n.n_distinct,
+       n.most_common_vals::text AS most_common_vals,
+       n.most_common_freqs,
+       n.histogram_bounds::text AS histogram_bounds,
+       n.correlation,
+       n.most_common_elems::text AS most_common_elems,
+       n.most_common_elem_freqs, n.elem_count_histogram
+FROM pg_stats_ext_exprs AS n
+WHERE n.statistics_schemaname = 'stats_import'
+AND n.statistics_name = 'test_stat_clone'
+EXCEPT
+SELECT o.inherited,
+       o.null_frac, o.avg_width, o.n_distinct,
+       o.most_common_vals::text AS most_common_vals,
+       o.most_common_freqs,
+       o.histogram_bounds::text AS histogram_bounds,
+       o.correlation,
+       o.most_common_elems::text AS most_common_elems,
+       o.most_common_elem_freqs, o.elem_count_histogram
+FROM pg_stats_ext_exprs AS o
+WHERE o.statistics_schemaname = 'stats_import'
+AND o.statistics_name = 'test_stat';
+
 DROP SCHEMA stats_import CASCADE;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 47370e581a..d74f23eda2 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30344,6 +30344,130 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
     'inherited',   false,
     'avg_width',   125::integer,
     'null_frac',   0.5::real);
+</programlisting>
+        </para>
+        <para>
+         Minor errors are reported as a <literal>WARNING</literal> and
+         ignored, and remaining statistics will still be restored. If all
+         specified statistics are successfully restored, return
+         <literal>true</literal>, otherwise <literal>false</literal>.
+        </para>
+       </entry>
+      </row>
+      <row>
+       <entry role="func_table_entry">
+        <para role="func_signature">
+         <indexterm>
+          <primary>pg_set_extended_stats</primary>
+         </indexterm>
+         <function>pg_set_extended_stats</function> (
+         <parameter>statistics_schemaname</parameter> <type>regnamespace</type>,
+         <parameter>statistics_name</parameter> <type>name</type>,
+         <parameter>inherited</parameter> <type>boolean</type>,
+         <optional>, <parameter>n_distinct</parameter> <type>pg_ndistinct</type></optional>,
+         <optional>, <parameter>dependencies</parameter> <type>pg_dependencies</type></optional>,
+         <optional>, <parameter>most_common_vals</parameter> <type>text[]</type></optional>,
+         <optional>, <parameter>most_common_val_nulls</parameter> <type>boolean[]</type></optional>,
+         <optional>, <parameter>most_common_freqs</parameter> <type>double precision[]</type> </optional>,
+         <optional>, <parameter>most_common_base_freqs</parameter> <type>double precision[]</type> </optional>,
+         <optional>, <parameter>exprs</parameter> <type>text[]</type> </optional> )
+         <returnvalue>void</returnvalue>
+        </para>
+        <para>
+         Creates or updates statistics for the given statistics object to the 
+         specified values. The parameters correspond to attributes of the same
+         name found in the view <link
+         linkend="view-pg-stats-ext"><structname>pg_stats_ext</structname></link>
+         and <link linkend="view-pg-stats-ext-exprs"><structname>pg_stats_ext_exprs</structname></link>,
+         except for <parameter>exprs</parameter> which corresponds to
+         <structfield>stxexpr</structfield> on
+         <link linkend="catalog-pg-statistic-ext-data"><structname>pg_statistic_ext_data</structname></link>
+         as a two-dimensional text array. The outer dimension represents a single
+         <structname>pg_statistic</structname> object, and the inner dimension elements are the
+         statistics fields from <link linkend="view-pg-stats"><structname>pg_stats</structname></link>
+         (currently <structfield>null_frac</structfield> through
+         <structfield>elem_count_histogram</structfield>).
+        </para>
+        <para>
+         Optional parameters default to <literal>NULL</literal>, which leave
+         the corresponding statistic unchanged.
+        </para>
+        <para>
+         Ordinarily, these statistics are collected automatically or updated
+         as a part of <xref linkend="sql-vacuum"/> or <xref
+         linkend="sql-analyze"/>, so it's not necessary to call this
+         function. However, it may be useful when testing the effects of
+         statistics on the planner to understand or anticipate plan changes.
+        </para>
+        <para>
+         The caller must have the <literal>MAINTAIN</literal> privilege on
+         the table or be the owner of the database.
+        </para>
+       </entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry">
+        <para role="func_signature">
+         <indexterm>
+          <primary>pg_clear_extended_stats</primary>
+         </indexterm>
+         <function>pg_clear_extended_stats</function> (
+         <parameter>statistics_schemaname</parameter> <type>regnamespace</type>,
+         <parameter>statistics_name</parameter> <type>name</type>,
+         <parameter>inherited</parameter> <type>boolean</type> )
+         <returnvalue>void</returnvalue>
+        </para>
+        <para>
+         Clears statistics for the given statistics object, as
+         though the object was newly created.
+        </para>
+        <para>
+         The caller must have the <literal>MAINTAIN</literal> privilege on
+         the table or be the owner of the database.
+        </para>
+       </entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_restore_extended_stats</primary>
+        </indexterm>
+        <function>pg_restore_extended_stats</function> (
+        <literal>VARIADIC</literal> <parameter>kwargs</parameter> <type>"any"</type> )
+        <returnvalue>boolean</returnvalue>
+        </para>
+        <para>
+         Similar to <function>pg_set_extended_stats()</function>, but
+         intended for bulk restore of extended statistics. The tracked
+         statistics may change from version to version, so the primary purpose
+         of this function is to maintain a consistent function signature to
+         avoid errors when restoring statistics from previous versions.
+        </para>
+        <para>
+         Arguments are passed as pairs of <replaceable>argname</replaceable>
+         and <replaceable>argvalue</replaceable>, where
+         <replaceable>argname</replaceable> corresponds to a named argument in
+         <function>pg_set_extended_stats()</function> and
+         <replaceable>argvalue</replaceable> is of the corresponding type.
+        </para>
+        <para>
+         Additionally, this function supports argument name
+         <literal>version</literal> of type <type>integer</type>, which
+         specifies the version from which the statistics originated, improving
+         interpretation of older statistics.
+        </para>
+        <para>
+         For example, to set the <structname>n_distinct</structname> and
+         <structname>exprs</structname> for the object
+         <structname>my_extended_stat</structname>:
+<programlisting>
+ SELECT pg_restore_extended_stats(
+    'statistics_schemaname', 'public'::regnamespace,
+    'statistics_name', 'my_extended_stat'::name,
+    'n_distinct', '{"2, 3": 4, "2, -1": 4, "2, -2": 4, "3, -1": 4, "3, -2": 4, "-1, -2": 3, "2, 3, -1": 4, "2, 3, -2": 4, "2, -1, -2": 4, "3, -1, -2": 4, "2, 3, -1, -2": 4}'::pg_ndistinct,
+    'exprs' => '{{0,4,-0.75,"{1}","{0.5}","{-1,0}",-0.6,NULL,NULL,NULL},{0.25,4,-0.5,"{2}","{0.5}",NULL,1,NULL,NULL,NULL}}');
 </programlisting>
         </para>
         <para>
-- 
2.47.1

v37-0001-Enable-dumping-of-table-index-stats-in-pg_dump.patchtext/x-patch; charset=US-ASCII; name=v37-0001-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From 06047840b9125c6e2b58c0c4ec4531c291ea59d8 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH v37 01/11] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will generate a statement that
calls pg_set_relation_stats(), and it will generate a series of
statements that call pg_set_attribute_stats(), one per attribute. These
statements will restore the statistics of the current system onto the
destination system.

Adds the command-line options -X / --statistics-only, which are mutually
exclusive to --schema-only and --data-only.

Statistics are not dumped when --schema-only is specified, except during
a binary upgrade.

As is the pattern with pg_dump options, staistics can be disabled using
--no-statistics.

Table statistics are dumped in the data section. This is true even if
dumping stats in a binary upgrade. Index and Materialized View
statistics are dumped in the post-data section.

Add --no-data option.

This option is useful for situations where someone wishes to test
query plans from a production database without copying production data.

This also makes the corresponding change to the simulated pg_upgrade in
the TAP tests for pg_dump.

This checks that dumping statistics is now the default, and that
--no-statistics will suppress statistics.

Add --no-schema option to pg_dump, etc.

Previously, users could use --data-only when they wanted to suppress
schema from a dump. However, that no longer makes sense now that the
data/schema binary has become the data/schema/statistics trinary.
---
 src/bin/pg_dump/pg_backup.h          |  10 +-
 src/bin/pg_dump/pg_backup_archiver.c |   8 +
 src/bin/pg_dump/pg_dump.c            | 385 ++++++++++++++++++++++++++-
 src/bin/pg_dump/pg_dump.h            |   9 +
 src/bin/pg_dump/pg_dump_sort.c       |  32 ++-
 src/bin/pg_dump/pg_dumpall.c         |   5 +
 src/bin/pg_dump/pg_restore.c         |  27 +-
 src/bin/pg_dump/t/001_basic.pl       |  18 ++
 src/bin/pg_dump/t/002_pg_dump.pl     |  28 +-
 src/bin/pg_upgrade/dump.c            |   6 +-
 src/bin/pg_upgrade/option.c          |  12 +
 src/bin/pg_upgrade/pg_upgrade.h      |   1 +
 doc/src/sgml/ref/pg_dump.sgml        |  69 +++--
 doc/src/sgml/ref/pg_dumpall.sgml     |  38 +++
 doc/src/sgml/ref/pg_restore.sgml     |  51 +++-
 doc/src/sgml/ref/pgupgrade.sgml      |  18 ++
 src/tools/pgindent/typedefs.list     |   1 +
 17 files changed, 688 insertions(+), 30 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index f0f19bb0b2..3fa1474fad 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -110,9 +110,12 @@ typedef struct _restoreOptions
 	int			column_inserts;
 	int			if_exists;
 	int			no_comments;	/* Skip comments */
+	int			no_data;			/* Skip data */
 	int			no_publications;	/* Skip publication entries */
+	int			no_schema;			/* Skip schema generation */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
@@ -160,6 +163,7 @@ typedef struct _restoreOptions
 	/* flags derived from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } RestoreOptions;
 
 typedef struct _dumpOptions
@@ -179,8 +183,11 @@ typedef struct _dumpOptions
 	int			column_inserts;
 	int			if_exists;
 	int			no_comments;
-	int			no_security_labels;
+	int			no_data;
 	int			no_publications;
+	int			no_schema;
+	int			no_security_labels;
+	int			no_statistics;
 	int			no_subscriptions;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
@@ -208,6 +215,7 @@ typedef struct _dumpOptions
 	/* flags derived from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } DumpOptions;
 
 /*
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 707a3fc844..db1d20d282 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -186,6 +186,9 @@ dumpOptionsFromRestoreOptions(RestoreOptions *ropt)
 	dopt->no_publications = ropt->no_publications;
 	dopt->no_security_labels = ropt->no_security_labels;
 	dopt->no_subscriptions = ropt->no_subscriptions;
+	dopt->no_data = ropt->no_data;
+	dopt->no_schema = ropt->no_schema;
+	dopt->no_statistics = ropt->no_statistics;
 	dopt->lockWaitTimeout = ropt->lockWaitTimeout;
 	dopt->include_everything = ropt->include_everything;
 	dopt->enable_row_security = ropt->enable_row_security;
@@ -2962,6 +2965,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's a stats dump, maybe ignore it */
+	if (ropt->no_statistics && strcmp(te->desc, "STATISTICS DATA") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2991,6 +2998,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS DATA") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 8f73a5df95..0f4976bc77 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -430,6 +430,7 @@ main(int argc, char **argv)
 	DataDirSyncMethod sync_method = DATA_DIR_SYNC_METHOD_FSYNC;
 	bool		data_only = false;
 	bool		schema_only = false;
+	bool		statistics_only = false;
 
 	static DumpOptions dopt;
 
@@ -466,6 +467,7 @@ main(int argc, char **argv)
 		{"encoding", required_argument, NULL, 'E'},
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
+		{"statistics-only", no_argument, NULL, 'X'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -492,8 +494,12 @@ main(int argc, char **argv)
 		{"strict-names", no_argument, &strict_names, 1},
 		{"use-set-session-authorization", no_argument, &dopt.use_setsessauth, 1},
 		{"no-comments", no_argument, &dopt.no_comments, 1},
+		{"no-data", no_argument, &dopt.no_data, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
+		{"no-schema", no_argument, &dopt.no_schema, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-schema", no_argument, &dopt.no_schema, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -539,7 +545,7 @@ main(int argc, char **argv)
 
 	InitDumpOptions(&dopt);
 
-	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:RsS:t:T:U:vwWxZ:",
+	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:PRsS:t:T:U:vwWxXZ:",
 							long_options, &optindex)) != -1)
 	{
 		switch (c)
@@ -613,6 +619,10 @@ main(int argc, char **argv)
 				dopt.cparams.pgport = pg_strdup(optarg);
 				break;
 
+			case 'X':			/* Dump statistics only */
+				statistics_only = true;
+				break;
+
 			case 'R':
 				/* no-op, still accepted for backwards compatibility */
 				break;
@@ -784,6 +794,17 @@ main(int argc, char **argv)
 
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (schema_only && statistics_only)
+		pg_fatal("options -s/--schema-only and -X/--statistics-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
+
+	if (data_only && dopt.no_data)
+		pg_fatal("options -a/--data-only and --no-data cannot be used together");
+	if (schema_only && dopt.no_schema)
+		pg_fatal("options -s/--schema-only and --no-schema cannot be used together");
+	if (statistics_only && dopt.no_statistics)
+		pg_fatal("options -X/--statistics-only and --no-statistics cannot be used together");
 
 	if (schema_only && foreign_servers_include_patterns.head != NULL)
 		pg_fatal("options -s/--schema-only and --include-foreign-data cannot be used together");
@@ -798,8 +819,9 @@ main(int argc, char **argv)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
 	/* set derivative flags */
-	dopt.dumpSchema = (!data_only);
-	dopt.dumpData = (!schema_only);
+	dopt.dumpData = data_only || (!schema_only && !statistics_only && !dopt.no_data);
+	dopt.dumpSchema = schema_only || (!data_only && !statistics_only && !dopt.no_schema);
+	dopt.dumpStatistics = statistics_only || (!data_only && !schema_only && !dopt.no_statistics);
 
 	/*
 	 * --inserts are already implied above if --column-inserts or
@@ -1099,6 +1121,7 @@ main(int argc, char **argv)
 	ropt->dropSchema = dopt.outputClean;
 	ropt->dumpData = dopt.dumpData;
 	ropt->dumpSchema = dopt.dumpSchema;
+	ropt->dumpStatistics = dopt.dumpStatistics;
 	ropt->if_exists = dopt.if_exists;
 	ropt->column_inserts = dopt.column_inserts;
 	ropt->dumpSections = dopt.dumpSections;
@@ -1177,7 +1200,7 @@ help(const char *progname)
 	printf(_("  -?, --help                   show this help, then exit\n"));
 
 	printf(_("\nOptions controlling the output content:\n"));
-	printf(_("  -a, --data-only              dump only the data, not the schema\n"));
+	printf(_("  -a, --data-only              dump only the data, not the schema or statistics\n"));
 	printf(_("  -b, --large-objects          include large objects in dump\n"));
 	printf(_("  --blobs                      (same as --large-objects, deprecated)\n"));
 	printf(_("  -B, --no-large-objects       exclude large objects in dump\n"));
@@ -1190,11 +1213,12 @@ help(const char *progname)
 	printf(_("  -N, --exclude-schema=PATTERN do NOT dump the specified schema(s)\n"));
 	printf(_("  -O, --no-owner               skip restoration of object ownership in\n"
 			 "                               plain-text format\n"));
-	printf(_("  -s, --schema-only            dump only the schema, no data\n"));
+	printf(_("  -s, --schema-only            dump only the schema, no data or statistics\n"));
 	printf(_("  -S, --superuser=NAME         superuser user name to use in plain-text format\n"));
 	printf(_("  -t, --table=PATTERN          dump only the specified table(s)\n"));
 	printf(_("  -T, --exclude-table=PATTERN  do NOT dump the specified table(s)\n"));
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
+	printf(_("  -X, --statistics-only        dump only the statistics, not schema or data\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
@@ -1219,8 +1243,11 @@ help(const char *progname)
 	printf(_("  --inserts                    dump data as INSERT commands, rather than COPY\n"));
 	printf(_("  --load-via-partition-root    load partitions via the root table\n"));
 	printf(_("  --no-comments                do not dump comment commands\n"));
+	printf(_("  --no-data                    do not dump data\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
+	printf(_("  --no-schema                  do not dump schema\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -6777,6 +6804,43 @@ getFuncs(Archive *fout)
 	destroyPQExpBuffer(query);
 }
 
+/*
+ * getRelationStatistics
+ *    register the statistics object as a dependent of the relation.
+ *
+ */
+static RelStatsInfo *
+getRelationStatistics(Archive *fout, DumpableObject *rel, char relkind)
+{
+	if ((relkind == RELKIND_RELATION) ||
+		(relkind == RELKIND_PARTITIONED_TABLE) ||
+		(relkind == RELKIND_INDEX) ||
+		(relkind == RELKIND_PARTITIONED_INDEX) ||
+		(relkind == RELKIND_MATVIEW))
+	{
+		RelStatsInfo	   *info = pg_malloc0(sizeof(RelStatsInfo));
+		DumpableObject	   *dobj = &info->dobj;
+
+		dobj->objType = DO_REL_STATS;
+		dobj->catId.tableoid = 0;
+		dobj->catId.oid = 0;
+		AssignDumpId(dobj);
+		dobj->dependencies = (DumpId *) pg_malloc(sizeof(DumpId));
+		dobj->dependencies[0] = rel->dumpId;
+		dobj->nDeps = 1;
+		dobj->allocDeps = 1;
+		dobj->components |= DUMP_COMPONENT_STATISTICS;
+		dobj->dump = rel->dump;
+		dobj->name = pg_strdup(rel->name);
+		dobj->namespace = rel->namespace;
+		info->relkind = relkind;
+		info->postponed_def = false;
+
+		return info;
+	}
+	return NULL;
+}
+
 /*
  * getTables
  *	  read all the tables (no indexes) in the system catalogs,
@@ -7154,6 +7218,7 @@ getTables(Archive *fout, int *numTables)
 
 		/* Tables have data */
 		tblinfo[i].dobj.components |= DUMP_COMPONENT_DATA;
+		tblinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
 
 		/* Mark whether table has an ACL */
 		if (!PQgetisnull(res, i, i_relacl))
@@ -7202,6 +7267,8 @@ getTables(Archive *fout, int *numTables)
 				}
 			}
 		}
+		if (tblinfo[i].interesting)
+			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relkind);
 	}
 
 	if (query->len != 0)
@@ -7648,11 +7715,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 		for (int c = 0; c < numinds; c++, j++)
 		{
 			char		contype;
+			char		indexkind;
+			RelStatsInfo   *relstats;
 
 			indxinfo[j].dobj.objType = DO_INDEX;
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -7675,7 +7745,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			{
 				NULL, NULL
 			};
+
+			if (indxinfo[j].parentidx == 0)
+				indexkind = RELKIND_INDEX;
+			else
+				indexkind = RELKIND_PARTITIONED_INDEX;
+
 			contype = *(PQgetvalue(res, j, i_contype));
+			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, indexkind);
 
 			if (contype == 'p' || contype == 'u' || contype == 'x')
 			{
@@ -7709,6 +7786,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				constrinfo->separate = true;
 
 				indxinfo[j].indexconstraint = constrinfo->dobj.dumpId;
+				if (relstats != NULL)
+					addObjectDependency(&relstats->dobj, constrinfo->dobj.dumpId);
 			}
 			else
 			{
@@ -10296,6 +10375,287 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * Tabular description of the parameters to pg_restore_relation_stats()
+ * param_name, param_type
+ */
+static const char *rel_stats_arginfo[][2] = {
+	{"relation", "regclass"},
+	{"version", "integer"},
+	{"relpages", "integer"},
+	{"reltuples", "real"},
+	{"relallvisible", "integer"},
+};
+
+/*
+ * Tabular description of the parameters to pg_restore_attribute_stats()
+ * param_name, param_type
+ */
+static const char *att_stats_arginfo[][2] = {
+	{"relation", "regclass"},
+	{"attname", "name"},
+	{"inherited", "boolean"},
+	{"version", "integer"},
+	{"null_frac", "float4"},
+	{"avg_width", "integer"},
+	{"n_distinct", "float4"},
+	{"most_common_vals", "text"},
+	{"most_common_freqs", "float4[]"},
+	{"histogram_bounds", "text"},
+	{"correlation", "float4"},
+	{"most_common_elems", "text"},
+	{"most_common_elem_freqs", "float4[]"},
+	{"elem_count_histogram", "float4[]"},
+	{"range_length_histogram", "text"},
+	{"range_empty_frac", "float4"},
+	{"range_bounds_histogram", "text"},
+};
+
+/*
+ * getRelStatsExportQuery --
+ *
+ * Generate a query that will fetch all relation (e.g. pg_class)
+ * stats for a given relation.
+ */
+static void
+getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "current_setting('server_version_num') AS version, "
+						 "c.relpages, c.reltuples, c.relallvisible "
+						 "FROM pg_class c "
+						 "JOIN pg_namespace n "
+						 "ON n.oid = c.relnamespace "
+						 "WHERE n.nspname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND c.relname = ");
+	appendStringLiteralAH(query, relname, fout);
+}
+
+/*
+ * getAttStatsExportQuery --
+ *
+ * Generate a query that will fetch all attribute (e.g. pg_statistic)
+ * stats for a given relation.
+ */
+static void
+getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "s.attname,"
+						 "s.inherited,"
+						 "current_setting('server_version_num') AS version, "
+						 "s.null_frac,"
+						 "s.avg_width,"
+						 "s.n_distinct,"
+						 "s.most_common_vals,"
+						 "s.most_common_freqs,"
+						 "s.histogram_bounds,"
+						 "s.correlation,"
+						 "s.most_common_elems,"
+						 "s.most_common_elem_freqs,"
+						 "s.elem_count_histogram,");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 "s.range_length_histogram,"
+							 "s.range_empty_frac,"
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(query,
+							 "NULL AS range_length_histogram,"
+							 "NULL AS range_empty_frac,"
+							 "NULL AS range_bounds_histogram ");
+
+	appendPQExpBufferStr(query,
+						 "FROM pg_stats s "
+						 "JOIN pg_namespace n "
+						 "ON n.nspname = s.schemaname "
+						 "JOIN pg_class c "
+						 "ON c.relname = s.tablename "
+						 "AND c.relnamespace = n.oid "
+						 "WHERE s.schemaname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND s.tablename = ");
+	appendStringLiteralAH(query, relname, fout);
+	appendPQExpBufferStr(query, " ORDER BY s.attname, s.inherited");
+}
+
+
+/*
+ * appendNamedArgument --
+ *
+ * Convenience routine for constructing parameters of the form:
+ * 'paraname', 'value'::type
+ */
+static void
+appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
+					const char *argval, const char *argtype)
+{
+	appendPQExpBufferStr(out, "\t");
+
+	appendStringLiteralAH(out, argname, fout);
+	appendPQExpBufferStr(out, ", ");
+
+	appendStringLiteralAH(out, argval, fout);
+	appendPQExpBuffer(out, "::%s", argtype);
+}
+
+/*
+ * appendRelStatsImport --
+ *
+ * Append a formatted pg_restore_relation_stats statement.
+ */
+static void
+appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	const char *sep = "";
+
+	if (PQntuples(res) == 0)
+		return;
+
+	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
+
+	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
+	{
+		const char *argname = rel_stats_arginfo[argno][0];
+		const char *argtype = rel_stats_arginfo[argno][1];
+		int			fieldno = PQfnumber(res, argname);
+
+		if (fieldno < 0)
+			pg_fatal("relation stats export query missing field '%s'",
+					 argname);
+
+		if (PQgetisnull(res, 0, fieldno))
+			continue;
+
+		appendPQExpBufferStr(out, sep);
+		appendNamedArgument(out, fout, argname, PQgetvalue(res, 0, fieldno), argtype);
+
+		sep = ",\n";
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+/*
+ * appendAttStatsImport --
+ *
+ * Append a series of formatted pg_restore_attribute_stats statements.
+ */
+static void
+appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	{
+		const char *sep = "";
+
+		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+		for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
+		{
+			const char *argname = att_stats_arginfo[argno][0];
+			const char *argtype = att_stats_arginfo[argno][1];
+			int			fieldno = PQfnumber(res, argname);
+
+			if (fieldno < 0)
+				pg_fatal("attribute stats export query missing field '%s'",
+						 argname);
+
+			if (PQgetisnull(res, rownum, fieldno))
+				continue;
+
+			appendPQExpBufferStr(out, sep);
+			appendNamedArgument(out, fout, argname, PQgetvalue(res, rownum, fieldno), argtype);
+			sep = ",\n";
+		}
+		appendPQExpBufferStr(out, "\n);\n");
+	}
+}
+
+/*
+ * Decide which section to use based on the relkind of the parent object.
+ *
+ * NB: materialized views may be postponed from SECTION_PRE_DATA to
+ * SECTION_POST_DATA to resolve some kinds of dependency problems. If so, the
+ * matview stats will also be postponed to SECTION_POST_DATA. See
+ * repairMatViewBoundaryMultiLoop().
+ */
+static teSection
+statisticsDumpSection(const RelStatsInfo *rsinfo)
+{
+	switch (rsinfo->relkind)
+	{
+		case RELKIND_RELATION:
+		case RELKIND_PARTITIONED_TABLE:
+		case RELKIND_MATVIEW:
+			return SECTION_DATA;
+		case RELKIND_INDEX:
+		case RELKIND_PARTITIONED_INDEX:
+			return SECTION_POST_DATA;
+		default:
+			pg_fatal("cannot dump statistics for relation kind '%c'",
+					 rsinfo->relkind);
+	}
+
+	return 0;				/* keep compiler quiet */
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	PGresult	   *res;
+	PQExpBuffer		query;
+	PQExpBuffer		out;
+	PQExpBuffer		tag;
+	DumpableObject *dobj = (DumpableObject *) &rsinfo->dobj;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", "STATISTICS DATA", fmtId(dobj->name));
+
+	query = createPQExpBuffer();
+	out = createPQExpBuffer();
+
+	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendRelStatsImport(out, fout, res);
+	PQclear(res);
+
+	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendAttStatsImport(out, fout, res);
+	PQclear(res);
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATISTICS DATA",
+							  .section = rsinfo->postponed_def ?
+								SECTION_POST_DATA :  statisticsDumpSection(rsinfo),
+							  .createStmt = out->data,
+							  .deps = dobj->dependencies,
+							  .nDeps = dobj->nDeps));
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(out);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -10744,6 +11104,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_SUBSCRIPTION_REL:
 			dumpSubscriptionTable(fout, (const SubRelInfo *) dobj);
 			break;
+		case DO_REL_STATS:
+			dumpRelationStats(fout, (const RelStatsInfo *) dobj);
+			break;
 		case DO_PRE_DATA_BOUNDARY:
 		case DO_POST_DATA_BOUNDARY:
 			/* never dumped, nothing to do */
@@ -17183,6 +17546,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 		free(indstatvalsarray);
 	}
 
+	/* Comments and stats share same .dep */
+
 	/* Dump Index Comments */
 	if (indxinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
 		dumpComment(fout, "INDEX", qindxname,
@@ -18970,6 +19335,16 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 				/* must come after the pre-data boundary */
 				addObjectDependency(dobj, preDataBound->dumpId);
 				break;
+			case DO_REL_STATS:
+				/* stats section varies by parent object type, DATA or POST */
+				if (statisticsDumpSection((RelStatsInfo *) dobj) == SECTION_DATA)
+				{
+					addObjectDependency(dobj, preDataBound->dumpId);
+					addObjectDependency(postDataBound, dobj->dumpId);
+				}
+				else
+					addObjectDependency(dobj, postDataBound->dumpId);
+				break;
 		}
 	}
 }
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index f62b564ed1..4edd88a54b 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -84,6 +84,7 @@ typedef enum
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION,
 	DO_SUBSCRIPTION_REL,		/* see note for SubRelInfo */
+	DO_REL_STATS,
 } DumpableObjectType;
 
 /*
@@ -109,6 +110,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
@@ -429,6 +431,13 @@ typedef struct _indexAttachInfo
 	IndxInfo   *partitionIdx;	/* link to index on partition */
 } IndexAttachInfo;
 
+typedef struct _relStatsInfo
+{
+	DumpableObject	dobj;
+	char			relkind;		/* 'r', 'v', 'c', etc */
+	bool			postponed_def;
+} RelStatsInfo;
+
 typedef struct _statsExtInfo
 {
 	DumpableObject dobj;
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index dc9a28924b..3a3602e3d2 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -801,11 +801,21 @@ repairMatViewBoundaryMultiLoop(DumpableObject *boundaryobj,
 {
 	/* remove boundary's dependency on object after it in loop */
 	removeObjectDependency(boundaryobj, nextobj->dumpId);
-	/* if that object is a matview, mark it as postponed into post-data */
+	/*
+	 * If that object is a matview or matview status, mark it as postponed into
+	 * post-data.
+	 */
 	if (nextobj->objType == DO_TABLE)
 	{
 		TableInfo  *nextinfo = (TableInfo *) nextobj;
 
+		if (nextinfo->relkind == RELKIND_MATVIEW)
+			nextinfo->postponed_def = true;
+	}
+	else if (nextobj->objType == DO_REL_STATS)
+	{
+		RelStatsInfo *nextinfo = (RelStatsInfo *) nextobj;
+
 		if (nextinfo->relkind == RELKIND_MATVIEW)
 			nextinfo->postponed_def = true;
 	}
@@ -1018,6 +1028,21 @@ repairDependencyLoop(DumpableObject **loop,
 					{
 						DumpableObject *nextobj;
 
+						nextobj = (j < nLoop - 1) ? loop[j + 1] : loop[0];
+						repairMatViewBoundaryMultiLoop(loop[j], nextobj);
+						return;
+					}
+				}
+			}
+			else if (loop[i]->objType == DO_REL_STATS &&
+					 ((RelStatsInfo *) loop[i])->relkind == RELKIND_MATVIEW)
+			{
+				for (j = 0; j < nLoop; j++)
+				{
+					if (loop[j]->objType == DO_POST_DATA_BOUNDARY)
+					{
+						DumpableObject *nextobj;
+
 						nextobj = (j < nLoop - 1) ? loop[j + 1] : loop[0];
 						repairMatViewBoundaryMultiLoop(loop[j], nextobj);
 						return;
@@ -1500,6 +1525,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "POST-DATA BOUNDARY  (ID %d)",
 					 obj->dumpId);
 			return;
+		case DO_REL_STATS:
+			snprintf(buf, bufsize,
+					 "RELATION STATISTICS FOR %s  (ID %d OID %u)",
+					 obj->name, obj->dumpId, obj->catId.oid);
+			return;
 	}
 	/* shouldn't get here */
 	snprintf(buf, bufsize,
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 396f79781c..7effb70490 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -103,6 +103,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -172,6 +173,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -451,6 +453,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -666,6 +670,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 88ae39d938..aa6db4fcc9 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -63,6 +63,7 @@ main(int argc, char **argv)
 	int			numWorkers = 1;
 	Archive    *AH;
 	char	   *inputFileSpec;
+	bool		statistics_only = false;
 	static int	disable_triggers = 0;
 	static int	enable_row_security = 0;
 	static int	if_exists = 0;
@@ -71,8 +72,11 @@ main(int argc, char **argv)
 	static int	outputNoTablespaces = 0;
 	static int	use_setsessauth = 0;
 	static int	no_comments = 0;
+	static int	no_data = 0;
 	static int	no_publications = 0;
+	static int	no_schema = 0;
 	static int	no_security_labels = 0;
+	static int  no_statistics = 0;
 	static int	no_subscriptions = 0;
 	static int	strict_names = 0;
 	bool		data_only = false;
@@ -108,6 +112,7 @@ main(int argc, char **argv)
 		{"username", 1, NULL, 'U'},
 		{"verbose", 0, NULL, 'v'},
 		{"single-transaction", 0, NULL, '1'},
+		{"statistics-only", no_argument, NULL, 'P'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -124,9 +129,12 @@ main(int argc, char **argv)
 		{"transaction-size", required_argument, NULL, 5},
 		{"use-set-session-authorization", no_argument, &use_setsessauth, 1},
 		{"no-comments", no_argument, &no_comments, 1},
+		{"no-data", no_argument, &no_data, 1},
 		{"no-publications", no_argument, &no_publications, 1},
+		{"no-schema", no_argument, &no_schema, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"filter", required_argument, NULL, 4},
 
 		{NULL, 0, NULL, 0}
@@ -271,6 +279,10 @@ main(int argc, char **argv)
 				opts->aclsSkip = 1;
 				break;
 
+			case 'X':			/* Restore statistics only */
+				statistics_only = true;
+				break;
+
 			case '1':			/* Restore data in a single transaction */
 				opts->single_txn = true;
 				opts->exit_on_error = true;
@@ -343,6 +355,10 @@ main(int argc, char **argv)
 
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
+	if (schema_only && statistics_only)
+		pg_fatal("options -s/--schema-only and -X/--statistics-only cannot be used together");
 
 	if (data_only && opts->dropSchema)
 		pg_fatal("options -c/--clean and -a/--data-only cannot be used together");
@@ -362,8 +378,9 @@ main(int argc, char **argv)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
 	/* set derivative flags */
-	opts->dumpSchema = (!data_only);
-	opts->dumpData = (!schema_only);
+	opts->dumpSchema = (!no_schema && !data_only && !statistics_only);
+	opts->dumpData = (!no_data && !schema_only && !statistics_only);
+	opts->dumpStatistics = (!no_statistics && !data_only && !schema_only);
 
 	opts->disable_triggers = disable_triggers;
 	opts->enable_row_security = enable_row_security;
@@ -375,6 +392,8 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
+	opts->no_data = no_data;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
@@ -484,6 +503,7 @@ usage(const char *progname)
 	printf(_("  -t, --table=NAME             restore named relation (table, view, etc.)\n"));
 	printf(_("  -T, --trigger=NAME           restore named trigger\n"));
 	printf(_("  -x, --no-privileges          skip restoration of access privileges (grant/revoke)\n"));
+	printf(_("  -X, --statistics-only        restore only the statistics, not schema or data\n"));
 	printf(_("  -1, --single-transaction     restore as a single transaction\n"));
 	printf(_("  --disable-triggers           disable triggers during data-only restore\n"));
 	printf(_("  --enable-row-security        enable row security\n"));
@@ -491,10 +511,13 @@ usage(const char *progname)
 			 "                               in FILENAME\n"));
 	printf(_("  --if-exists                  use IF EXISTS when dropping objects\n"));
 	printf(_("  --no-comments                do not restore comment commands\n"));
+	printf(_("  --no-data                    do not restore data\n"));
 	printf(_("  --no-data-for-failed-tables  do not restore data of tables that could not be\n"
 			 "                               created\n"));
 	printf(_("  --no-publications            do not restore publications\n"));
+	printf(_("  --no-schema                  do not restore schema\n"));
 	printf(_("  --no-security-labels         do not restore security labels\n"));
+	printf(_("  --no-statistics              do not restore statistics\n"));
 	printf(_("  --no-subscriptions           do not restore subscriptions\n"));
 	printf(_("  --no-table-access-method     do not restore table access methods\n"));
 	printf(_("  --no-tablespaces             do not restore tablespace assignments\n"));
diff --git a/src/bin/pg_dump/t/001_basic.pl b/src/bin/pg_dump/t/001_basic.pl
index 214240f1ae..f29da06ed2 100644
--- a/src/bin/pg_dump/t/001_basic.pl
+++ b/src/bin/pg_dump/t/001_basic.pl
@@ -50,12 +50,30 @@ command_fails_like(
 	'pg_dump: options -s/--schema-only and -a/--data-only cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '-s', '-X' ],
+	qr/\Qpg_dump: error: options -s\/--schema-only and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: error: options -s/--schema-only and -X/--statistics-only cannot be used together'
+);
+
+command_fails_like(
+	[ 'pg_dump', '-a', '-X' ],
+	qr/\Qpg_dump: error: options -a\/--data-only and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: error: options -a/--data-only and -X/--statistics-only cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-s', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: options -s\/--schema-only and --include-foreign-data cannot be used together\E/,
 	'pg_dump: options -s/--schema-only and --include-foreign-data cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '--statistics-only', '--no-statistics' ],
+	qr/\Qpg_dump: error: options -X\/--statistics-only and --no-statistics cannot be used together\E/,
+	'pg_dump: options -X\/--statistics-only and --no-statistics cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-j2', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: option --include-foreign-data is not supported with parallel backup\E/,
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index bf65d44b94..91f34e2dc0 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -66,7 +66,7 @@ my %pgdump_runs = (
 			'--format=custom',
 			"--file=$tempdir/binary_upgrade.dump",
 			'-w',
-			'--schema-only',
+			'--no-data',
 			'--binary-upgrade',
 			'-d', 'postgres',    # alternative way to specify database
 		],
@@ -645,6 +645,12 @@ my %pgdump_runs = (
 
 			'--schema=dump_test', '-b', '-B', '--no-sync', 'postgres',
 		],
+	},
+	no_statistics => {
+		dump_cmd => [
+			'pg_dump', "--file=$tempdir/no_statistics.sql",
+			'--no-sync', '--no-statistics', 'postgres',
+		],
 	},);
 
 ###############################################################
@@ -711,6 +717,7 @@ my %full_runs = (
 	no_large_objects => 1,
 	no_owner => 1,
 	no_privs => 1,
+	no_statistics => 1,
 	no_table_access_method => 1,
 	pg_dumpall_dbprivs => 1,
 	pg_dumpall_exclude => 1,
@@ -4586,6 +4593,25 @@ my %tests = (
 		},
 	},
 
+	#	'statistics_import' => {
+	#	create_sql => '
+	#		CREATE MATERIALIZED VIEW dump_test.has_stats
+	#		AS SELECT g.g AS x, g.g / 2 AS y FROM generate_series(1,100) AS g(g);
+	#		ANALYZE dump_test.has_stats;',
+	#	regexp => qr/pg_catalog.pg_restore_attribute_stats/,
+	#	like => {
+	#		%full_runs,
+	#		test_schema_plus_large_objects => 1,
+	#	},
+	#	unlike => {
+	#		exclude_dump_test_schema => 1,
+	#		no_statistics => 1,
+	#		only_dump_test_schema => 1,
+	#		schema_only => 1,
+	#		section_post_data => 1,
+	#	},
+	#},
+
 	# CREATE TABLE with partitioned table and various AMs.  One
 	# partition uses the same default as the parent, and a second
 	# uses its own AM.
diff --git a/src/bin/pg_upgrade/dump.c b/src/bin/pg_upgrade/dump.c
index 8ce0fa3020..a29cd2cca9 100644
--- a/src/bin/pg_upgrade/dump.c
+++ b/src/bin/pg_upgrade/dump.c
@@ -21,10 +21,11 @@ generate_old_dump(void)
 
 	/* run new pg_dumpall binary for globals */
 	exec_prog(UTILITY_LOG_FILE, NULL, true, true,
-			  "\"%s/pg_dumpall\" %s --globals-only --quote-all-identifiers "
+			  "\"%s/pg_dumpall\" %s %s --globals-only --quote-all-identifiers "
 			  "--binary-upgrade %s --no-sync -f \"%s/%s\"",
 			  new_cluster.bindir, cluster_conn_opts(&old_cluster),
 			  log_opts.verbose ? "--verbose" : "",
+			  user_opts.do_statistics ? "" : "--no-statistics",
 			  log_opts.dumpdir,
 			  GLOBALS_DUMP_FILE);
 	check_ok();
@@ -52,10 +53,11 @@ generate_old_dump(void)
 		snprintf(log_file_name, sizeof(log_file_name), DB_DUMP_LOG_FILE_MASK, old_db->db_oid);
 
 		parallel_exec_prog(log_file_name, NULL,
-						   "\"%s/pg_dump\" %s --schema-only --quote-all-identifiers "
+						   "\"%s/pg_dump\" %s --no-data %s --quote-all-identifiers "
 						   "--binary-upgrade --format=custom %s --no-sync --file=\"%s/%s\" %s",
 						   new_cluster.bindir, cluster_conn_opts(&old_cluster),
 						   log_opts.verbose ? "--verbose" : "",
+						   user_opts.do_statistics ? "" : "--no-statistics",
 						   log_opts.dumpdir,
 						   sql_file_name, escaped_connstr.data);
 
diff --git a/src/bin/pg_upgrade/option.c b/src/bin/pg_upgrade/option.c
index 108eb7a1ba..3b6c7ec994 100644
--- a/src/bin/pg_upgrade/option.c
+++ b/src/bin/pg_upgrade/option.c
@@ -60,6 +60,8 @@ parseCommandLine(int argc, char *argv[])
 		{"copy", no_argument, NULL, 2},
 		{"copy-file-range", no_argument, NULL, 3},
 		{"sync-method", required_argument, NULL, 4},
+		{"with-statistics", no_argument, NULL, 5},
+		{"no-statistics", no_argument, NULL, 6},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -70,6 +72,7 @@ parseCommandLine(int argc, char *argv[])
 
 	user_opts.do_sync = true;
 	user_opts.transfer_mode = TRANSFER_MODE_COPY;
+	user_opts.do_statistics = true;
 
 	os_info.progname = get_progname(argv[0]);
 
@@ -212,6 +215,13 @@ parseCommandLine(int argc, char *argv[])
 				user_opts.sync_method = pg_strdup(optarg);
 				break;
 
+			case 5:
+				user_opts.do_statistics = true;
+				break;
+			case 6:
+				user_opts.do_statistics = false;
+				break;
+
 			default:
 				fprintf(stderr, _("Try \"%s --help\" for more information.\n"),
 						os_info.progname);
@@ -306,7 +316,9 @@ usage(void)
 	printf(_("  --clone                       clone instead of copying files to new cluster\n"));
 	printf(_("  --copy                        copy files to new cluster (default)\n"));
 	printf(_("  --copy-file-range             copy files to new cluster with copy_file_range\n"));
+	printf(_("  --no-statistics               do not import statistics from old cluster\n"));
 	printf(_("  --sync-method=METHOD          set method for syncing files to disk\n"));
+	printf(_("  --with-statistics             import statistics from old cluster (default)\n"));
 	printf(_("  -?, --help                    show this help, then exit\n"));
 	printf(_("\n"
 			 "Before running pg_upgrade you must:\n"
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index 0cdd675e4f..3fe111fbde 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -327,6 +327,7 @@ typedef struct
 	int			jobs;			/* number of processes/threads to use */
 	char	   *socketdir;		/* directory to use for Unix sockets */
 	char	   *sync_method;
+	bool		do_statistics;	/* carry over statistics from old cluster */
 } UserOpts;
 
 typedef struct
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index d66e901f51..5e58f24d21 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -123,7 +123,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Dump only the data, not the schema (data definitions).
+        Dump only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are dumped.
        </para>
 
@@ -141,13 +141,11 @@ PostgreSQL documentation
       <listitem>
        <para>
         Include large objects in the dump.  This is the default behavior
-        except when <option>--schema</option>, <option>--table</option>, or
-        <option>--schema-only</option> is specified.  The <option>-b</option>
-        switch is therefore only useful to add large objects to dumps
-        where a specific schema or table has been requested.  Note that
-        large objects are considered data and therefore will be included when
-        <option>--data-only</option> is used, but not
-        when <option>--schema-only</option> is.
+        except when <option>--schema</option>, <option>--table</option>,
+        <option>--schema-only</option>, or <option>--statistics-only</option>, or
+        <option>--no-data</option> is specified.  The <option>-b</option>
+        switch is therefore only useful to add large objects to dumps where a
+        specific schema or table has been requested.
        </para>
       </listitem>
      </varlistentry>
@@ -516,10 +514,11 @@ PostgreSQL documentation
       <term><option>--schema-only</option></term>
       <listitem>
        <para>
-        Dump only the object definitions (schema), not data.
+        Dump only the object definitions (schema), not data or statistics.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive to <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
         <option>--section=pre-data --section=post-data</option>.
@@ -652,6 +651,18 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Dump only the statistics, not the schema (data definitions) or data.
+        Statistics for tables, materialized views, and indexes are dumped.
+       </para>
+
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-Z <replaceable class="parameter">level</replaceable></option></term>
       <term><option>-Z <replaceable class="parameter">method</replaceable></option>[:<replaceable>detail</replaceable>]</term>
@@ -824,16 +835,17 @@ PostgreSQL documentation
       <term><option>--exclude-table-data=<replaceable class="parameter">pattern</replaceable></option></term>
       <listitem>
        <para>
-        Do not dump data for any tables matching <replaceable
+        Do not dump data or statistics for any tables matching <replaceable
         class="parameter">pattern</replaceable>. The pattern is
         interpreted according to the same rules as for <option>-t</option>.
         <option>--exclude-table-data</option> can be given more than once to
-        exclude tables matching any of several patterns. This option is
-        useful when you need the definition of a particular table even
-        though you do not need the data in it.
+        exclude tables matching any of several patterns. This option is useful
+        when you need the definition of a particular table even though you do
+        not need the data in it.
        </para>
        <para>
-        To exclude data for all tables in the database, see <option>--schema-only</option>.
+        To exclude data for all tables in the database, see <option>--schema-only</option>
+        or <option>--statistics-only</option>.
        </para>
       </listitem>
      </varlistentry>
@@ -1080,6 +1092,15 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-data</option></term>
+      <listitem>
+       <para>
+        Do not dump data.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-publications</option></term>
       <listitem>
@@ -1098,6 +1119,24 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-schema</option></term>
+      <listitem>
+       <para>
+        Do not dump schema (data definitions).
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_dumpall.sgml b/doc/src/sgml/ref/pg_dumpall.sgml
index 014f279258..d423153a93 100644
--- a/doc/src/sgml/ref/pg_dumpall.sgml
+++ b/doc/src/sgml/ref/pg_dumpall.sgml
@@ -265,6 +265,17 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Dump only the statistics, not the schema (data definitions) or data.
+        Statistics for tables, materialized views, and indexes are dumped.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--binary-upgrade</option></term>
       <listitem>
@@ -422,6 +433,15 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-data</option></term>
+      <listitem>
+       <para>
+        Do not dump data.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-publications</option></term>
       <listitem>
@@ -447,6 +467,15 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-schema</option></term>
+      <listitem>
+       <para>
+        Do not dump schema (data definitions).
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-security-labels</option></term>
       <listitem>
@@ -456,6 +485,15 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index b8b27e1719..22c3c118ad 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -94,7 +94,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Restore only the data, not the schema (data definitions).
+        Restore only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are restored,
         if present in the archive.
        </para>
@@ -483,10 +483,11 @@ PostgreSQL documentation
         to the extent that schema entries are present in the archive.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive of <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
-        <option>--section=pre-data --section=post-data</option>.
+        <option>--section=pre-data --section=post-data --no-statistics</option>.
        </para>
        <para>
         (Do not confuse this with the <option>--schema</option> option, which
@@ -599,6 +600,20 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Restore only the statistics, not schema (data definitions) or data.
+       </para>
+       <para>
+        (Do not confuse this with the <option>--schema</option> option, which
+        uses the word <quote>schema</quote> in a different meaning.)
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-1</option></term>
       <term><option>--single-transaction</option></term>
@@ -681,6 +696,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-data</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore data, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-data-for-failed-tables</option></term>
       <listitem>
@@ -713,6 +738,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-schema</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore schema (data definitions), even if
+        the archive contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-security-labels</option></term>
       <listitem>
@@ -723,6 +758,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore statistics, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pgupgrade.sgml b/doc/src/sgml/ref/pgupgrade.sgml
index 4777381dac..64a1ebd613 100644
--- a/doc/src/sgml/ref/pgupgrade.sgml
+++ b/doc/src/sgml/ref/pgupgrade.sgml
@@ -145,6 +145,24 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--with-statistics</option></term>
+      <listitem>
+       <para>
+        Restore statistics from the old cluster into the new cluster.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not restore statistics from the old cluster into the new cluster.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-o</option> <replaceable class="parameter">options</replaceable></term>
       <term><option>--old-options</option> <replaceable class="parameter">options</replaceable></term>
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e1c4f913f8..a1863e0263 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2394,6 +2394,7 @@ RelMapFile
 RelMapping
 RelOptInfo
 RelOptKind
+RelStatsInfo
 RelToCheck
 RelToCluster
 RelabelType
-- 
2.47.1

v37-0007-split-out-print_processing_notice.patchtext/x-patch; charset=US-ASCII; name=v37-0007-split-out-print_processing_notice.patchDownload
From 2dbedb55639efed864d5041f004a932f9c8cf7e0 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 6 Nov 2024 15:46:51 -0500
Subject: [PATCH v37 07/11] split out print_processing_notice

---
 src/bin/scripts/vacuumdb.c | 41 +++++++++++++++++++++++---------------
 1 file changed, 25 insertions(+), 16 deletions(-)

diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index 9a92f65c4b..2aa55f191e 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -535,6 +535,30 @@ check_conn_options(PGconn *conn, vacuumingOptions *vacopts)
 }
 
 
+/*
+ * print the processing notice for a database.
+ */
+static void
+print_processing_notice(PGconn *conn, int stage, const char *progname, bool quiet)
+{
+	const char *stage_messages[] = {
+		gettext_noop("Generating minimal optimizer statistics (1 target)"),
+		gettext_noop("Generating medium optimizer statistics (10 targets)"),
+		gettext_noop("Generating default (full) optimizer statistics")
+	};
+
+	if (!quiet)
+	{
+		if (stage != ANALYZE_NO_STAGE)
+			printf(_("%s: processing database \"%s\": %s\n"),
+				   progname, PQdb(conn), _(stage_messages[stage]));
+		else
+			printf(_("%s: vacuuming database \"%s\"\n"),
+				   progname, PQdb(conn));
+		fflush(stdout);
+	}
+}
+
 /*
  * vacuum_one_database
  *
@@ -574,11 +598,6 @@ vacuum_one_database(ConnParams *cparams,
 		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
 		"RESET default_statistics_target;"
 	};
-	const char *stage_messages[] = {
-		gettext_noop("Generating minimal optimizer statistics (1 target)"),
-		gettext_noop("Generating medium optimizer statistics (10 targets)"),
-		gettext_noop("Generating default (full) optimizer statistics")
-	};
 
 	Assert(stage == ANALYZE_NO_STAGE ||
 		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
@@ -586,17 +605,7 @@ vacuum_one_database(ConnParams *cparams,
 	conn = connectDatabase(cparams, progname, echo, false, true);
 
 	check_conn_options(conn, vacopts);
-
-	if (!quiet)
-	{
-		if (stage != ANALYZE_NO_STAGE)
-			printf(_("%s: processing database \"%s\": %s\n"),
-				   progname, PQdb(conn), _(stage_messages[stage]));
-		else
-			printf(_("%s: vacuuming database \"%s\"\n"),
-				   progname, PQdb(conn));
-		fflush(stdout);
-	}
+	print_processing_notice(conn, stage, progname, quiet);
 
 	/*
 	 * Prepare the list of tables to process by querying the catalogs.
-- 
2.47.1

v37-0006-split-out-check_conn_options.patchtext/x-patch; charset=US-ASCII; name=v37-0006-split-out-check_conn_options.patchDownload
From d14231a7cd193f37f3e99b9d9b2a582fa14b99fc Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 6 Nov 2024 15:20:07 -0500
Subject: [PATCH v37 06/11] split out check_conn_options

---
 src/bin/scripts/vacuumdb.c | 103 ++++++++++++++++++++-----------------
 1 file changed, 57 insertions(+), 46 deletions(-)

diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index 170f34d927..9a92f65c4b 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -10,6 +10,7 @@
  *-------------------------------------------------------------------------
  */
 
+#include "libpq-fe.h"
 #include "postgres_fe.h"
 
 #include <limits.h>
@@ -459,55 +460,11 @@ escape_quotes(const char *src)
 }
 
 /*
- * vacuum_one_database
- *
- * Process tables in the given database.  If the 'tables' list is empty,
- * process all tables in the database.
- *
- * Note that this function is only concerned with running exactly one stage
- * when in analyze-in-stages mode; caller must iterate on us if necessary.
- *
- * If concurrentCons is > 1, multiple connections are used to vacuum tables
- * in parallel.  In this case and if the table list is empty, we first obtain
- * a list of tables from the database.
+ * Check connection options for compatibility with the connected database.
  */
 static void
-vacuum_one_database(ConnParams *cparams,
-					vacuumingOptions *vacopts,
-					int stage,
-					SimpleStringList *objects,
-					int concurrentCons,
-					const char *progname, bool echo, bool quiet)
+check_conn_options(PGconn *conn, vacuumingOptions *vacopts)
 {
-	PQExpBufferData sql;
-	PQExpBufferData buf;
-	PQExpBufferData catalog_query;
-	PGresult   *res;
-	PGconn	   *conn;
-	SimpleStringListCell *cell;
-	ParallelSlotArray *sa;
-	SimpleStringList dbtables = {NULL, NULL};
-	int			i;
-	int			ntups;
-	bool		failed = false;
-	bool		objects_listed = false;
-	const char *initcmd;
-	const char *stage_commands[] = {
-		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
-		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
-		"RESET default_statistics_target;"
-	};
-	const char *stage_messages[] = {
-		gettext_noop("Generating minimal optimizer statistics (1 target)"),
-		gettext_noop("Generating medium optimizer statistics (10 targets)"),
-		gettext_noop("Generating default (full) optimizer statistics")
-	};
-
-	Assert(stage == ANALYZE_NO_STAGE ||
-		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
-
-	conn = connectDatabase(cparams, progname, echo, false, true);
-
 	if (vacopts->disable_page_skipping && PQserverVersion(conn) < 90600)
 	{
 		PQfinish(conn);
@@ -575,6 +532,60 @@ vacuum_one_database(ConnParams *cparams,
 
 	/* skip_database_stats is used automatically if server supports it */
 	vacopts->skip_database_stats = (PQserverVersion(conn) >= 160000);
+}
+
+
+/*
+ * vacuum_one_database
+ *
+ * Process tables in the given database.  If the 'tables' list is empty,
+ * process all tables in the database.
+ *
+ * Note that this function is only concerned with running exactly one stage
+ * when in analyze-in-stages mode; caller must iterate on us if necessary.
+ *
+ * If concurrentCons is > 1, multiple connections are used to vacuum tables
+ * in parallel.  In this case and if the table list is empty, we first obtain
+ * a list of tables from the database.
+ */
+static void
+vacuum_one_database(ConnParams *cparams,
+					vacuumingOptions *vacopts,
+					int stage,
+					SimpleStringList *objects,
+					int concurrentCons,
+					const char *progname, bool echo, bool quiet)
+{
+	PQExpBufferData sql;
+	PQExpBufferData buf;
+	PQExpBufferData catalog_query;
+	PGresult   *res;
+	PGconn	   *conn;
+	SimpleStringListCell *cell;
+	ParallelSlotArray *sa;
+	SimpleStringList dbtables = {NULL, NULL};
+	int			i;
+	int			ntups;
+	bool		failed = false;
+	bool		objects_listed = false;
+	const char *initcmd;
+	const char *stage_commands[] = {
+		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
+		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
+		"RESET default_statistics_target;"
+	};
+	const char *stage_messages[] = {
+		gettext_noop("Generating minimal optimizer statistics (1 target)"),
+		gettext_noop("Generating medium optimizer statistics (10 targets)"),
+		gettext_noop("Generating default (full) optimizer statistics")
+	};
+
+	Assert(stage == ANALYZE_NO_STAGE ||
+		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
+
+	conn = connectDatabase(cparams, progname, echo, false, true);
+
+	check_conn_options(conn, vacopts);
 
 	if (!quiet)
 	{
-- 
2.47.1

v37-0009-preserve-catalog-lists-across-staged-runs.patchtext/x-patch; charset=US-ASCII; name=v37-0009-preserve-catalog-lists-across-staged-runs.patchDownload
From 7a8cfd54e7ed765e175ec5da130dfd8a20d97038 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 6 Nov 2024 21:30:49 -0500
Subject: [PATCH v37 09/11] preserve catalog lists across staged runs

---
 src/bin/scripts/vacuumdb.c | 113 ++++++++++++++++++++++++++-----------
 1 file changed, 80 insertions(+), 33 deletions(-)

diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index 5ae7241716..28506ca0c2 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -68,6 +68,9 @@ static void vacuum_one_database(ConnParams *cparams,
 								int stage,
 								SimpleStringList *objects,
 								int concurrentCons,
+								PGconn *conn,
+								SimpleStringList *dbtables,
+								int ntups,
 								const char *progname, bool echo, bool quiet);
 
 static void vacuum_all_databases(ConnParams *cparams,
@@ -83,6 +86,14 @@ static void prepare_vacuum_command(PQExpBuffer sql, int serverVersion,
 static void run_vacuum_command(PGconn *conn, const char *sql, bool echo,
 							   const char *table);
 
+static void check_conn_options(PGconn *conn, vacuumingOptions *vacopts);
+
+static void
+print_processing_notice(PGconn *conn, int stage, const char *progname, bool quiet);
+
+static SimpleStringList * generate_catalog_list(PGconn *conn, vacuumingOptions *vacopts,
+												SimpleStringList *objects, bool echo, int *ntups);
+
 static void help(const char *progname);
 
 void		check_objfilter(void);
@@ -386,6 +397,11 @@ main(int argc, char *argv[])
 	}
 	else
 	{
+		PGconn	   *conn;
+		int			ntup;
+		SimpleStringList   *found_objects;
+		int			stage;
+
 		if (dbname == NULL)
 		{
 			if (getenv("PGDATABASE"))
@@ -397,25 +413,37 @@ main(int argc, char *argv[])
 		}
 
 		cparams.dbname = dbname;
+		stage = (analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
+
+		conn = connectDatabase(&cparams, progname, echo, false, true);
+		check_conn_options(conn, &vacopts);
+		print_processing_notice(conn, stage, progname, quiet);
+		found_objects = generate_catalog_list(conn, &vacopts, &objects, echo, &ntup);
 
 		if (analyze_in_stages)
 		{
-			int			stage;
-
 			for (stage = 0; stage < ANALYZE_NUM_STAGES; stage++)
 			{
-				vacuum_one_database(&cparams, &vacopts,
-									stage,
+				/* the last pass disconnected the conn */
+				if (stage > 0)
+					conn = connectDatabase(&cparams, progname, echo, false, true);
+
+				vacuum_one_database(&cparams, &vacopts, stage,
 									&objects,
 									concurrentCons,
+									conn,
+									found_objects,
+									ntup,
 									progname, echo, quiet);
 			}
 		}
 		else
-			vacuum_one_database(&cparams, &vacopts,
-								ANALYZE_NO_STAGE,
+			vacuum_one_database(&cparams, &vacopts, stage,
 								&objects,
 								concurrentCons,
+								conn,
+								found_objects,
+								ntup,
 								progname, echo, quiet);
 	}
 
@@ -791,16 +819,17 @@ vacuum_one_database(ConnParams *cparams,
 					int stage,
 					SimpleStringList *objects,
 					int concurrentCons,
+					PGconn *conn,
+					SimpleStringList *dbtables,
+					int ntups,
 					const char *progname, bool echo, bool quiet)
 {
 	PQExpBufferData sql;
-	PGconn	   *conn;
 	SimpleStringListCell *cell;
 	ParallelSlotArray *sa;
-	int			ntups;
 	bool		failed = false;
 	const char *initcmd;
-	SimpleStringList *dbtables;
+
 	const char *stage_commands[] = {
 		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
 		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
@@ -810,13 +839,6 @@ vacuum_one_database(ConnParams *cparams,
 	Assert(stage == ANALYZE_NO_STAGE ||
 		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
 
-	conn = connectDatabase(cparams, progname, echo, false, true);
-
-	check_conn_options(conn, vacopts);
-	print_processing_notice(conn, stage, progname, quiet);
-
-	dbtables = generate_catalog_list(conn, vacopts, objects, echo, &ntups);
-
 	/*
 	 * If no rows are returned, there are no matching tables, so we are done.
 	 */
@@ -928,7 +950,7 @@ finish:
 }
 
 /*
- * Vacuum/analyze all connectable databases.
+ * Vacuum/analyze all ccparams->override_dbname = PQgetvalue(result, i, 0);onnectable databases.
  *
  * In analyze-in-stages mode, we process all databases in one stage before
  * moving on to the next stage.  That ensure minimal stats are available
@@ -944,8 +966,13 @@ vacuum_all_databases(ConnParams *cparams,
 {
 	PGconn	   *conn;
 	PGresult   *result;
-	int			stage;
 	int			i;
+	int			stage;
+
+	SimpleStringList  **found_objects;
+	int				   *num_tuples;
+
+	stage = (analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
 
 	conn = connectMaintenanceDatabase(cparams, progname, echo);
 	result = executeQuery(conn,
@@ -953,7 +980,33 @@ vacuum_all_databases(ConnParams *cparams,
 						  echo);
 	PQfinish(conn);
 
-	if (analyze_in_stages)
+	/*
+	 * connect to each database, check validity of options,
+	 * build the list of found objects per database,
+	 * and run the first/only vacuum stage
+	 */
+	found_objects = palloc(PQntuples(result) * sizeof(SimpleStringList *));
+	num_tuples = palloc(PQntuples(result) * sizeof (int));
+
+	for (i = 0; i < PQntuples(result); i++)
+	{
+		cparams->override_dbname = PQgetvalue(result, i, 0);
+		conn = connectDatabase(cparams, progname, echo, false, true);
+		check_conn_options(conn, vacopts);
+		print_processing_notice(conn, stage, progname, quiet);
+		found_objects[i] = generate_catalog_list(conn, vacopts, objects, echo, &num_tuples[i]);
+
+		vacuum_one_database(cparams, vacopts,
+							stage,
+							objects,
+							concurrentCons,
+							conn,
+							found_objects[i],
+							num_tuples[i],
+							progname, echo, quiet);
+	}
+
+	if (stage != ANALYZE_NO_STAGE)
 	{
 		/*
 		 * When analyzing all databases in stages, we analyze them all in the
@@ -963,35 +1016,29 @@ vacuum_all_databases(ConnParams *cparams,
 		 * This means we establish several times as many connections, but
 		 * that's a secondary consideration.
 		 */
-		for (stage = 0; stage < ANALYZE_NUM_STAGES; stage++)
+		for (stage = 1; stage < ANALYZE_NUM_STAGES; stage++)
 		{
 			for (i = 0; i < PQntuples(result); i++)
 			{
 				cparams->override_dbname = PQgetvalue(result, i, 0);
+				conn = connectDatabase(cparams, progname, echo, false, true);
+				print_processing_notice(conn, stage, progname, quiet);
 
 				vacuum_one_database(cparams, vacopts,
 									stage,
 									objects,
 									concurrentCons,
+									conn,
+									found_objects[i],
+									num_tuples[i],
 									progname, echo, quiet);
 			}
 		}
 	}
-	else
-	{
-		for (i = 0; i < PQntuples(result); i++)
-		{
-			cparams->override_dbname = PQgetvalue(result, i, 0);
-
-			vacuum_one_database(cparams, vacopts,
-								ANALYZE_NO_STAGE,
-								objects,
-								concurrentCons,
-								progname, echo, quiet);
-		}
-	}
 
 	PQclear(result);
+	pfree(found_objects);
+	pfree(num_tuples);
 }
 
 /*
-- 
2.47.1

v37-0010-Add-issues_sql_unlike-opposite-of-issues_sql_lik.patchtext/x-patch; charset=US-ASCII; name=v37-0010-Add-issues_sql_unlike-opposite-of-issues_sql_lik.patchDownload
From 3b189ae9bcaf542d37d7ca3c4e356f449d2bb5e6 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 7 Nov 2024 18:06:52 -0500
Subject: [PATCH v37 10/11] Add issues_sql_unlike, opposite of issues_sql_like

This is the same as issues_sql_like(), but the specified text is
prohibited from being in the output rather than required.

This became necessary to test that a command-line filter was in fact
filtering out certain output that a prior test required.
---
 src/test/perl/PostgreSQL/Test/Cluster.pm | 25 ++++++++++++++++++++++++
 1 file changed, 25 insertions(+)

diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index 08b89a4cdf..7f6e5508fe 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -2800,6 +2800,31 @@ sub issues_sql_like
 }
 
 =pod
+=item $node->issues_sql_unlike(cmd, prohibited_sql, test_name)
+
+Run a command on the node, then verify that $prohibited_sql does not appear
+in the server log file.
+
+=cut
+
+sub issues_sql_unlike
+{
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+	my ($self, $cmd, $prohibited_sql, $test_name) = @_;
+
+	local %ENV = $self->_get_env();
+
+	my $log_location = -s $self->logfile;
+
+	my $result = PostgreSQL::Test::Utils::run_log($cmd);
+	ok($result, "@$cmd exit code 0");
+	my $log =
+	  PostgreSQL::Test::Utils::slurp_file($self->logfile, $log_location);
+	unlike($log, $prohibited_sql, "$test_name: SQL found in server log");
+	return;
+}
+
 
 =item $node->log_content()
 
-- 
2.47.1

v37-0011-Add-force-analyze-to-vacuumdb.patchtext/x-patch; charset=US-ASCII; name=v37-0011-Add-force-analyze-to-vacuumdb.patchDownload
From 091e1f07f758c110536c6a53d2f70f892212ed18 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Fri, 8 Nov 2024 12:27:50 -0500
Subject: [PATCH v37 11/11] Add --force-analyze to vacuumdb.

The vacuumdb options of --analyze-in-stages and --analyze-only are often
used after a restore from a dump or a pg_upgrade to quickly rebuild
stats on a databse.

However, now that stats are imported in most (but not all) cases,
running either of these commands will be at least partially redundant,
and will overwrite the stats that were just imported, which is a big
POLA violation.

We could add a new option such as --analyze-missing-in-stages, but that
wouldn't help the userbase that grown accustomed to running
--analyze-in-stages after an upgrade.

The least-bad option to handle the situation is to change the behavior
of --analyze-only and --analyze-in-stages to only analyze tables which
were missing stats before the vacuumdb started, but offer the
--force-analyze flag to restore the old behavior for those who truly
wanted it.
---
 src/bin/scripts/t/100_vacuumdb.pl |  6 +-
 src/bin/scripts/vacuumdb.c        | 91 ++++++++++++++++++++++++-------
 doc/src/sgml/ref/vacuumdb.sgml    | 48 ++++++++++++++++
 3 files changed, 125 insertions(+), 20 deletions(-)

diff --git a/src/bin/scripts/t/100_vacuumdb.pl b/src/bin/scripts/t/100_vacuumdb.pl
index ccb7711af4..a3fcfda5ee 100644
--- a/src/bin/scripts/t/100_vacuumdb.pl
+++ b/src/bin/scripts/t/100_vacuumdb.pl
@@ -127,9 +127,13 @@ $node->issues_sql_like(
 	qr/statement: VACUUM \(SKIP_DATABASE_STATS, ANALYZE\) public.vactable\(a, b\);/,
 	'vacuumdb --analyze with complete column list');
 $node->issues_sql_like(
+	[ 'vacuumdb', '--analyze-only', '--force-analyze', '--table', 'vactable(b)', 'postgres' ],
+	qr/statement: ANALYZE public.vactable\(b\);/,
+	'vacuumdb --analyze-only --force-analyze with partial column list');
+$node->issues_sql_unlike(
 	[ 'vacuumdb', '--analyze-only', '--table', 'vactable(b)', 'postgres' ],
 	qr/statement: ANALYZE public.vactable\(b\);/,
-	'vacuumdb --analyze-only with partial column list');
+	'vacuumdb --analyze-only --force-analyze with partial column list skipping vacuumed tables');
 $node->command_checks_all(
 	[ 'vacuumdb', '--analyze', '--table', 'vacview', 'postgres' ],
 	0,
diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index 28506ca0c2..30804af75d 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -25,6 +25,7 @@
 #include "fe_utils/query_utils.h"
 #include "fe_utils/simple_list.h"
 #include "fe_utils/string_utils.h"
+#include "pqexpbuffer.h"
 
 
 /* vacuum options controlled by user flags */
@@ -47,6 +48,8 @@ typedef struct vacuumingOptions
 	bool		process_main;
 	bool		process_toast;
 	bool		skip_database_stats;
+	bool		analyze_in_stages;
+	bool		force_analyze;
 	char	   *buffer_usage_limit;
 } vacuumingOptions;
 
@@ -75,7 +78,6 @@ static void vacuum_one_database(ConnParams *cparams,
 
 static void vacuum_all_databases(ConnParams *cparams,
 								 vacuumingOptions *vacopts,
-								 bool analyze_in_stages,
 								 SimpleStringList *objects,
 								 int concurrentCons,
 								 const char *progname, bool echo, bool quiet);
@@ -140,6 +142,7 @@ main(int argc, char *argv[])
 		{"no-process-toast", no_argument, NULL, 11},
 		{"no-process-main", no_argument, NULL, 12},
 		{"buffer-usage-limit", required_argument, NULL, 13},
+		{"force-analyze", no_argument, NULL, 14},
 		{NULL, 0, NULL, 0}
 	};
 
@@ -156,7 +159,6 @@ main(int argc, char *argv[])
 	bool		echo = false;
 	bool		quiet = false;
 	vacuumingOptions vacopts;
-	bool		analyze_in_stages = false;
 	SimpleStringList objects = {NULL, NULL};
 	int			concurrentCons = 1;
 	int			tbl_count = 0;
@@ -170,6 +172,8 @@ main(int argc, char *argv[])
 	vacopts.do_truncate = true;
 	vacopts.process_main = true;
 	vacopts.process_toast = true;
+	vacopts.force_analyze = false;
+	vacopts.analyze_in_stages = false;
 
 	pg_logging_init(argv[0]);
 	progname = get_progname(argv[0]);
@@ -251,7 +255,7 @@ main(int argc, char *argv[])
 				maintenance_db = pg_strdup(optarg);
 				break;
 			case 3:
-				analyze_in_stages = vacopts.analyze_only = true;
+				vacopts.analyze_in_stages = vacopts.analyze_only = true;
 				break;
 			case 4:
 				vacopts.disable_page_skipping = true;
@@ -287,6 +291,9 @@ main(int argc, char *argv[])
 			case 13:
 				vacopts.buffer_usage_limit = escape_quotes(optarg);
 				break;
+			case 14:
+				vacopts.force_analyze = true;
+				break;
 			default:
 				/* getopt_long already emitted a complaint */
 				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -372,6 +379,14 @@ main(int argc, char *argv[])
 		pg_fatal("cannot use the \"%s\" option with the \"%s\" option",
 				 "buffer-usage-limit", "full");
 
+	/*
+	 * --force-analyze is only valid when used with --analyze-only, -analyze,
+	 * or --analyze-in-stages
+	 */
+	if (vacopts.force_analyze && !vacopts.analyze_only && !vacopts.analyze_in_stages)
+		pg_fatal("can only use the \"%s\" option with \"%s\" or \"%s\"",
+				 "--force-analyze", "-Z/--analyze-only", "--analyze-in-stages");
+
 	/* fill cparams except for dbname, which is set below */
 	cparams.pghost = host;
 	cparams.pgport = port;
@@ -390,7 +405,6 @@ main(int argc, char *argv[])
 		cparams.dbname = maintenance_db;
 
 		vacuum_all_databases(&cparams, &vacopts,
-							 analyze_in_stages,
 							 &objects,
 							 concurrentCons,
 							 progname, echo, quiet);
@@ -413,20 +427,27 @@ main(int argc, char *argv[])
 		}
 
 		cparams.dbname = dbname;
-		stage = (analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
+		stage = (vacopts.analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
 
 		conn = connectDatabase(&cparams, progname, echo, false, true);
 		check_conn_options(conn, &vacopts);
 		print_processing_notice(conn, stage, progname, quiet);
 		found_objects = generate_catalog_list(conn, &vacopts, &objects, echo, &ntup);
+		vacuum_one_database(&cparams, &vacopts, stage,
+							&objects,
+							concurrentCons,
+							conn,
+							found_objects,
+							ntup,
+							progname, echo, quiet);
 
-		if (analyze_in_stages)
+		if (stage != ANALYZE_NO_STAGE)
 		{
-			for (stage = 0; stage < ANALYZE_NUM_STAGES; stage++)
+			for (stage = 1; stage < ANALYZE_NUM_STAGES; stage++)
 			{
 				/* the last pass disconnected the conn */
-				if (stage > 0)
-					conn = connectDatabase(&cparams, progname, echo, false, true);
+				conn = connectDatabase(&cparams, progname, echo, false, true);
+				print_processing_notice(conn, stage, progname, quiet);
 
 				vacuum_one_database(&cparams, &vacopts, stage,
 									&objects,
@@ -437,14 +458,6 @@ main(int argc, char *argv[])
 									progname, echo, quiet);
 			}
 		}
-		else
-			vacuum_one_database(&cparams, &vacopts, stage,
-								&objects,
-								concurrentCons,
-								conn,
-								found_objects,
-								ntup,
-								progname, echo, quiet);
 	}
 
 	exit(0);
@@ -763,6 +776,47 @@ generate_catalog_list(PGconn *conn,
 						  vacopts->min_mxid_age);
 	}
 
+	/*
+	 * If this query is for an analyze-only or analyze-in-stages, two
+	 * upgrade-centric operations, and force-analyze is NOT set, then
+	 * exclude any relations that already have their full compliment
+	 * of attribute stats and extended stats.
+	 *
+	 *
+	 */
+	if ((vacopts->analyze_only || vacopts->analyze_in_stages) &&
+		!vacopts->force_analyze)
+	{
+		/*
+		 * The pg_class in question has no pg_statistic rows representing
+		 * user-visible columns that lack a corresponding pg_statitic row.
+		 * Currently no differentiation is made for whether the
+		 * pg_statistic.stainherit is true or false.
+		 */
+		appendPQExpBufferStr(&catalog_query,
+							 " AND NOT EXISTS (\n"
+							 " SELECT NULL FROM pg_catalog.pg_attribute AS a\n"
+							 " WHERE a.attrelid OPERATOR(pg_catalog.=) c.oid\n"
+							 " AND a.attnum OPERATOR(pg_catalog.>) 0 AND NOT a.attisdropped\n"
+							 " AND NOT EXISTS (\n"
+							 " SELECT NULL FROM pg_catalog.pg_statistic AS s\n"
+							 " WHERE s.starelid OPERATOR(pg_catalog.=) c.oid\n"
+							 " AND s.staattnum OPERATOR(pg_catalog.=) a.attnum))\n");
+
+		/*
+		 * The pg_class entry has no pg_statistic_ext rows that lack a corresponding
+		 * pg_statistic_ext_data row. Currently no differentiation is made for whether
+		 * pg_statistic_exta_data.stxdinherit is true or false.
+		 */
+		appendPQExpBufferStr(&catalog_query,
+							 " AND NOT EXISTS (\n"
+							 " SELECT NULL FROM pg_catalog.pg_statistic_ext AS e\n"
+							 " WHERE e.stxrelid OPERATOR(pg_catalog.=) c.oid\n"
+							 " AND NOT EXISTS (\n"
+							 " SELECT NULL FROM pg_catalog.pg_statistic_ext_data AS d\n"
+							 " WHERE d.stxoid OPERATOR(pg_catalog.=) e.oid))\n");
+	}
+
 	/*
 	 * Execute the catalog query.  We use the default search_path for this
 	 * query for consistency with table lookups done elsewhere by the user.
@@ -959,7 +1013,6 @@ finish:
 static void
 vacuum_all_databases(ConnParams *cparams,
 					 vacuumingOptions *vacopts,
-					 bool analyze_in_stages,
 					 SimpleStringList *objects,
 					 int concurrentCons,
 					 const char *progname, bool echo, bool quiet)
@@ -972,7 +1025,7 @@ vacuum_all_databases(ConnParams *cparams,
 	SimpleStringList  **found_objects;
 	int				   *num_tuples;
 
-	stage = (analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
+	stage = (vacopts->analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
 
 	conn = connectMaintenanceDatabase(cparams, progname, echo);
 	result = executeQuery(conn,
diff --git a/doc/src/sgml/ref/vacuumdb.sgml b/doc/src/sgml/ref/vacuumdb.sgml
index 66fccb30a2..00ec927606 100644
--- a/doc/src/sgml/ref/vacuumdb.sgml
+++ b/doc/src/sgml/ref/vacuumdb.sgml
@@ -425,6 +425,12 @@ PostgreSQL documentation
        <para>
         Only calculate statistics for use by the optimizer (no vacuum).
        </para>
+       <para>
+        By default, this operation excludes relations that already have
+        statistics generated. If the option <option>--force-analyze</option>
+        is also specified, then relations with existing stastistics are not
+        excluded.
+       </para>
       </listitem>
      </varlistentry>
 
@@ -439,6 +445,47 @@ PostgreSQL documentation
         to produce usable statistics faster, and subsequent stages build the
         full statistics.
        </para>
+       <para>
+        This option is intended to be run after a <command>pg_upgrade</command>
+        to generate statistics for relations that have no stistatics or incomplete
+        statistics (such as those with extended statistics objects, which are not
+        imported on upgrade).
+       </para>
+       <para>
+        If the option <option>--force-analyze</option> is also specified, then
+        relations with existing stastistics are not excluded.
+        This option is only useful to analyze a database that currently has
+        no statistics or has wholly incorrect ones, such as if it is newly
+        populated from a restored dump.
+        Be aware that running with this option combinationin a database with
+        existing statistics may cause the query optimizer choices to become
+        transiently worse due to the low statistics targets of the early
+        stages.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--force-analyze</option></term>
+      <listitem>
+       <para>
+        This option can only be used if either <option>--analyze-only</option>
+        or <option>--analyze-in-stages</option> is specified. It modifies those
+        options to not filter out relations that already have statistics.
+       </para>
+       <para>
+
+        Only calculate statistics for use by the optimizer (no vacuum),
+        like <option>--analyze-only</option>.  Run three
+        stages of analyze; the first stage uses the lowest possible statistics
+        target (see <xref linkend="guc-default-statistics-target"/>)
+        to produce usable statistics faster, and subsequent stages build the
+        full statistics.
+       </para>
+
+       <para>
+        This option was created
+       </para>
 
        <para>
         This option is only useful to analyze a database that currently has
@@ -452,6 +499,7 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+
      <varlistentry>
        <term><option>-?</option></term>
        <term><option>--help</option></term>
-- 
2.47.1

v37-0008-split-out-generate_catalog_list.patchtext/x-patch; charset=US-ASCII; name=v37-0008-split-out-generate_catalog_list.patchDownload
From ceacfd5372fad6ad56780e0ac7ba129df5f72c36 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 6 Nov 2024 16:15:16 -0500
Subject: [PATCH v37 08/11] split out generate_catalog_list

---
 src/bin/scripts/vacuumdb.c | 176 +++++++++++++++++++++----------------
 1 file changed, 99 insertions(+), 77 deletions(-)

diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index 2aa55f191e..5ae7241716 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -560,67 +560,38 @@ print_processing_notice(PGconn *conn, int stage, const char *progname, bool quie
 }
 
 /*
- * vacuum_one_database
- *
- * Process tables in the given database.  If the 'tables' list is empty,
- * process all tables in the database.
- *
- * Note that this function is only concerned with running exactly one stage
- * when in analyze-in-stages mode; caller must iterate on us if necessary.
- *
- * If concurrentCons is > 1, multiple connections are used to vacuum tables
- * in parallel.  In this case and if the table list is empty, we first obtain
- * a list of tables from the database.
- */
-static void
-vacuum_one_database(ConnParams *cparams,
-					vacuumingOptions *vacopts,
-					int stage,
-					SimpleStringList *objects,
-					int concurrentCons,
-					const char *progname, bool echo, bool quiet)
+	* Prepare the list of tables to process by querying the catalogs.
+	*
+	* Since we execute the constructed query with the default search_path
+	* (which could be unsafe), everything in this query MUST be fully
+	* qualified.
+	*
+	* First, build a WITH clause for the catalog query if any tables were
+	* specified, with a set of values made of relation names and their
+	* optional set of columns.  This is used to match any provided column
+	* lists with the generated qualified identifiers and to filter for the
+	* tables provided via --table.  If a listed table does not exist, the
+	* catalog query will fail.
+	*/
+static SimpleStringList *
+generate_catalog_list(PGconn *conn,
+					  vacuumingOptions *vacopts,
+					  SimpleStringList *objects,
+					  bool echo,
+					  int *ntups)
 {
-	PQExpBufferData sql;
-	PQExpBufferData buf;
 	PQExpBufferData catalog_query;
-	PGresult   *res;
-	PGconn	   *conn;
+	PQExpBufferData buf;
+	SimpleStringList *dbtables;
 	SimpleStringListCell *cell;
-	ParallelSlotArray *sa;
-	SimpleStringList dbtables = {NULL, NULL};
-	int			i;
-	int			ntups;
-	bool		failed = false;
 	bool		objects_listed = false;
-	const char *initcmd;
-	const char *stage_commands[] = {
-		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
-		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
-		"RESET default_statistics_target;"
-	};
+	PGresult   *res;
+	int			i;
 
-	Assert(stage == ANALYZE_NO_STAGE ||
-		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
+	dbtables = palloc(sizeof(SimpleStringList));
+	dbtables->head = NULL;
+	dbtables->tail = NULL;
 
-	conn = connectDatabase(cparams, progname, echo, false, true);
-
-	check_conn_options(conn, vacopts);
-	print_processing_notice(conn, stage, progname, quiet);
-
-	/*
-	 * Prepare the list of tables to process by querying the catalogs.
-	 *
-	 * Since we execute the constructed query with the default search_path
-	 * (which could be unsafe), everything in this query MUST be fully
-	 * qualified.
-	 *
-	 * First, build a WITH clause for the catalog query if any tables were
-	 * specified, with a set of values made of relation names and their
-	 * optional set of columns.  This is used to match any provided column
-	 * lists with the generated qualified identifiers and to filter for the
-	 * tables provided via --table.  If a listed table does not exist, the
-	 * catalog query will fail.
-	 */
 	initPQExpBuffer(&catalog_query);
 	for (cell = objects ? objects->head : NULL; cell; cell = cell->next)
 	{
@@ -771,40 +742,91 @@ vacuum_one_database(ConnParams *cparams,
 	appendPQExpBufferStr(&catalog_query, " ORDER BY c.relpages DESC;");
 	executeCommand(conn, "RESET search_path;", echo);
 	res = executeQuery(conn, catalog_query.data, echo);
+	*ntups = PQntuples(res);
 	termPQExpBuffer(&catalog_query);
 	PQclear(executeQuery(conn, ALWAYS_SECURE_SEARCH_PATH_SQL, echo));
 
-	/*
-	 * If no rows are returned, there are no matching tables, so we are done.
-	 */
-	ntups = PQntuples(res);
-	if (ntups == 0)
-	{
-		PQclear(res);
-		PQfinish(conn);
-		return;
-	}
-
 	/*
 	 * Build qualified identifiers for each table, including the column list
 	 * if given.
 	 */
-	initPQExpBuffer(&buf);
-	for (i = 0; i < ntups; i++)
+	if (*ntups > 0)
 	{
-		appendPQExpBufferStr(&buf,
-							 fmtQualifiedId(PQgetvalue(res, i, 1),
-											PQgetvalue(res, i, 0)));
+		initPQExpBuffer(&buf);
+		for (i = 0; i < *ntups; i++)
+		{
+			appendPQExpBufferStr(&buf,
+								fmtQualifiedId(PQgetvalue(res, i, 1),
+												PQgetvalue(res, i, 0)));
 
-		if (objects_listed && !PQgetisnull(res, i, 2))
-			appendPQExpBufferStr(&buf, PQgetvalue(res, i, 2));
+			if (objects_listed && !PQgetisnull(res, i, 2))
+				appendPQExpBufferStr(&buf, PQgetvalue(res, i, 2));
 
-		simple_string_list_append(&dbtables, buf.data);
-		resetPQExpBuffer(&buf);
+			simple_string_list_append(dbtables, buf.data);
+			resetPQExpBuffer(&buf);
+		}
+		termPQExpBuffer(&buf);
 	}
-	termPQExpBuffer(&buf);
 	PQclear(res);
 
+	return dbtables;
+}
+
+/*
+ * vacuum_one_database
+ *
+ * Process tables in the given database.  If the 'tables' list is empty,
+ * process all tables in the database.
+ *
+ * Note that this function is only concerned with running exactly one stage
+ * when in analyze-in-stages mode; caller must iterate on us if necessary.
+ *
+ * If concurrentCons is > 1, multiple connections are used to vacuum tables
+ * in parallel.  In this case and if the table list is empty, we first obtain
+ * a list of tables from the database.
+ */
+static void
+vacuum_one_database(ConnParams *cparams,
+					vacuumingOptions *vacopts,
+					int stage,
+					SimpleStringList *objects,
+					int concurrentCons,
+					const char *progname, bool echo, bool quiet)
+{
+	PQExpBufferData sql;
+	PGconn	   *conn;
+	SimpleStringListCell *cell;
+	ParallelSlotArray *sa;
+	int			ntups;
+	bool		failed = false;
+	const char *initcmd;
+	SimpleStringList *dbtables;
+	const char *stage_commands[] = {
+		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
+		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
+		"RESET default_statistics_target;"
+	};
+
+	Assert(stage == ANALYZE_NO_STAGE ||
+		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
+
+	conn = connectDatabase(cparams, progname, echo, false, true);
+
+	check_conn_options(conn, vacopts);
+	print_processing_notice(conn, stage, progname, quiet);
+
+	dbtables = generate_catalog_list(conn, vacopts, objects, echo, &ntups);
+
+	/*
+	 * If no rows are returned, there are no matching tables, so we are done.
+	 */
+	if (ntups == 0)
+	{
+		PQfinish(conn);
+		return;
+	}
+
+
 	/*
 	 * Ensure concurrentCons is sane.  If there are more connections than
 	 * vacuumable relations, we don't need to use them all.
@@ -837,7 +859,7 @@ vacuum_one_database(ConnParams *cparams,
 
 	initPQExpBuffer(&sql);
 
-	cell = dbtables.head;
+	cell = dbtables->head;
 	do
 	{
 		const char *tabname = cell->val;
-- 
2.47.1

#271Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#270)
Re: Statistics Import and Export

On Tue, 2025-01-07 at 01:18 -0500, Corey Huinker wrote:

Attached is the latest (and probably last) unified patchset before
parts get spun off into their own threads.

In this thread I'm only looking at 0001. Please start a new thread for
vacuumdb and extended stats changes.

0001 - This is the unified changes to pg_dump, pg_restore,
pg_dumpall, and pg_upgrade.

It incorporates most of what Jeff changed when he unified v36j, with
typo fixes spotted by Bruce. There was interest in splitting
STATISTICS DATA into RELATION STATISTICS DATA and ATTRIBUTE
STATISTICS DATA.

I think we should just stick with "STATISTICS DATA".

There was also interest in changing the prefix for STATISTICS DATA.
However, the only special case for prefixes currently relies on an
isData flag. Since there is no isStatistics flag, we would either
have to create one, or do strcmps on te->description looking for
"STATISTICS DATA". It's do-able, but I'm not sure it's worth it.

I do like the idea of a "Statistics for ..." prefix, and I think it's
doable.

The caller needs some knowledge about that anyway, to correctly output
the statistics dump when the schema is not requested. Tests should
cover those cases, too.

Regards,
Jeff Davis

#272Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#271)
1 attachment(s)
Re: Statistics Import and Export

I do like the idea of a "Statistics for ..." prefix, and I think it's
doable.

And that's now implemented.

The caller needs some knowledge about that anyway, to correctly output

the statistics dump when the schema is not requested. Tests should
cover those cases, too.

Tests for pg_dump --no-statistics and pg_dump --schema-only were added.
Rebased to master as of today.

I'm not completely happy with this patch, as I had to comment out one check
in pg_backup_archiver that seemed necessary, but perhaps another set of
eyes will set me straight.

Attached is just the pg_dump stuff, and only for relation/attribute stats.
The extended stats and vacuumdb work will be in their own threads going
forward.

Attachments:

v38-0001-Enable-dumping-of-table-index-stats-in-pg_dump.patchtext/x-patch; charset=US-ASCII; name=v38-0001-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From fdb53409458bc9aeed3496f355173ac97062afd1 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH v38] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will generate a statement that
calls pg_set_relation_stats(), and it will generate a series of
statements that call pg_set_attribute_stats(), one per attribute. These
statements will restore the statistics of the current system onto the
destination system.

Adds the command-line options -X / --statistics-only, which are mutually
exclusive to --schema-only and --data-only.

Statistics are not dumped when --schema-only is specified, except during
a binary upgrade.

As is the pattern with pg_dump options, staistics can be disabled using
--no-statistics.

Table statistics are dumped in the data section. This is true even if
dumping stats in a binary upgrade. Index and Materialized View
statistics are dumped in the post-data section.

Add --no-data option.

This option is useful for situations where someone wishes to test
query plans from a production database without copying production data.

This also makes the corresponding change to the simulated pg_upgrade in
the TAP tests for pg_dump.

This checks that dumping statistics is now the default, and that
--no-statistics will suppress statistics.

Add --no-schema option to pg_dump, etc.

Previously, users could use --data-only when they wanted to suppress
schema from a dump. However, that no longer makes sense now that the
data/schema binary has become the data/schema/statistics trinary.
---
 src/bin/pg_dump/pg_backup.h          |  10 +-
 src/bin/pg_dump/pg_backup_archiver.c |  70 ++++-
 src/bin/pg_dump/pg_backup_archiver.h |   3 +-
 src/bin/pg_dump/pg_dump.c            | 384 ++++++++++++++++++++++++++-
 src/bin/pg_dump/pg_dump.h            |   9 +
 src/bin/pg_dump/pg_dump_sort.c       |  32 ++-
 src/bin/pg_dump/pg_dumpall.c         |   5 +
 src/bin/pg_dump/pg_restore.c         |  32 ++-
 src/bin/pg_dump/t/001_basic.pl       |  18 ++
 src/bin/pg_dump/t/002_pg_dump.pl     |  61 ++++-
 src/bin/pg_upgrade/dump.c            |   6 +-
 src/bin/pg_upgrade/option.c          |  12 +
 src/bin/pg_upgrade/pg_upgrade.h      |   1 +
 doc/src/sgml/ref/pg_dump.sgml        |  69 +++--
 doc/src/sgml/ref/pg_dumpall.sgml     |  38 +++
 doc/src/sgml/ref/pg_restore.sgml     |  51 +++-
 doc/src/sgml/ref/pgupgrade.sgml      |  18 ++
 src/tools/pgindent/typedefs.list     |   1 +
 18 files changed, 771 insertions(+), 49 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index f0f19bb0b2..3fa1474fad 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -110,9 +110,12 @@ typedef struct _restoreOptions
 	int			column_inserts;
 	int			if_exists;
 	int			no_comments;	/* Skip comments */
+	int			no_data;			/* Skip data */
 	int			no_publications;	/* Skip publication entries */
+	int			no_schema;			/* Skip schema generation */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
@@ -160,6 +163,7 @@ typedef struct _restoreOptions
 	/* flags derived from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } RestoreOptions;
 
 typedef struct _dumpOptions
@@ -179,8 +183,11 @@ typedef struct _dumpOptions
 	int			column_inserts;
 	int			if_exists;
 	int			no_comments;
-	int			no_security_labels;
+	int			no_data;
 	int			no_publications;
+	int			no_schema;
+	int			no_security_labels;
+	int			no_statistics;
 	int			no_subscriptions;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
@@ -208,6 +215,7 @@ typedef struct _dumpOptions
 	/* flags derived from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } DumpOptions;
 
 /*
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 707a3fc844..d651b9b764 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -46,6 +46,11 @@
 #define TEXT_DUMP_HEADER "--\n-- PostgreSQL database dump\n--\n\n"
 #define TEXT_DUMPALL_HEADER "--\n-- PostgreSQL database cluster dump\n--\n\n"
 
+typedef enum entryType {
+	default_entry,
+	data_entry,
+	statistics_entry
+} entryType;
 
 static ArchiveHandle *_allocAH(const char *FileSpec, const ArchiveFormat fmt,
 							   const pg_compress_specification compression_spec,
@@ -53,7 +58,7 @@ static ArchiveHandle *_allocAH(const char *FileSpec, const ArchiveFormat fmt,
 							   SetupWorkerPtrType setupWorkerPtr,
 							   DataDirSyncMethod sync_method);
 static void _getObjectDescription(PQExpBuffer buf, const TocEntry *te);
-static void _printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData);
+static void _printTocEntry(ArchiveHandle *AH, TocEntry *te, entryType entry_type);
 static char *sanitize_line(const char *str, bool want_hyphen);
 static void _doSetFixedOutputState(ArchiveHandle *AH);
 static void _doSetSessionAuth(ArchiveHandle *AH, const char *user);
@@ -149,6 +154,7 @@ InitDumpOptions(DumpOptions *opts)
 	opts->dumpSections = DUMP_UNSECTIONED;
 	opts->dumpSchema = true;
 	opts->dumpData = true;
+	opts->dumpStatistics = true;
 }
 
 /*
@@ -169,9 +175,10 @@ dumpOptionsFromRestoreOptions(RestoreOptions *ropt)
 	dopt->outputClean = ropt->dropSchema;
 	dopt->dumpData = ropt->dumpData;
 	dopt->dumpSchema = ropt->dumpSchema;
+	dopt->dumpSections = ropt->dumpSections;
+	dopt->dumpStatistics = ropt->dumpStatistics;
 	dopt->if_exists = ropt->if_exists;
 	dopt->column_inserts = ropt->column_inserts;
-	dopt->dumpSections = ropt->dumpSections;
 	dopt->aclsSkip = ropt->aclsSkip;
 	dopt->outputSuperuser = ropt->superuser;
 	dopt->outputCreateDB = ropt->createDB;
@@ -186,6 +193,9 @@ dumpOptionsFromRestoreOptions(RestoreOptions *ropt)
 	dopt->no_publications = ropt->no_publications;
 	dopt->no_security_labels = ropt->no_security_labels;
 	dopt->no_subscriptions = ropt->no_subscriptions;
+	dopt->no_data = ropt->no_data;
+	dopt->no_schema = ropt->no_schema;
+	dopt->no_statistics = ropt->no_statistics;
 	dopt->lockWaitTimeout = ropt->lockWaitTimeout;
 	dopt->include_everything = ropt->include_everything;
 	dopt->enable_row_security = ropt->enable_row_security;
@@ -739,7 +749,7 @@ RestoreArchive(Archive *AHX)
 
 		for (te = AH->toc->next; te != AH->toc; te = te->next)
 		{
-			if ((te->reqs & (REQ_SCHEMA | REQ_DATA)) == 0)
+			if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) == 0)
 				continue;		/* ignore if not to be dumped at all */
 
 			switch (_tocEntryRestorePass(te))
@@ -760,7 +770,7 @@ RestoreArchive(Archive *AHX)
 		{
 			for (te = AH->toc->next; te != AH->toc; te = te->next)
 			{
-				if ((te->reqs & (REQ_SCHEMA | REQ_DATA)) != 0 &&
+				if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) != 0 &&
 					_tocEntryRestorePass(te) == RESTORE_PASS_ACL)
 					(void) restore_toc_entry(AH, te, false);
 			}
@@ -770,7 +780,7 @@ RestoreArchive(Archive *AHX)
 		{
 			for (te = AH->toc->next; te != AH->toc; te = te->next)
 			{
-				if ((te->reqs & (REQ_SCHEMA | REQ_DATA)) != 0 &&
+				if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) != 0 &&
 					_tocEntryRestorePass(te) == RESTORE_PASS_POST_ACL)
 					(void) restore_toc_entry(AH, te, false);
 			}
@@ -869,7 +879,7 @@ restore_toc_entry(ArchiveHandle *AH, TocEntry *te, bool is_parallel)
 			pg_log_info("creating %s \"%s\"",
 						te->desc, te->tag);
 
-		_printTocEntry(AH, te, false);
+		_printTocEntry(AH, te, default_entry);
 		defnDumped = true;
 
 		if (strcmp(te->desc, "TABLE") == 0)
@@ -938,7 +948,7 @@ restore_toc_entry(ArchiveHandle *AH, TocEntry *te, bool is_parallel)
 			 */
 			if (AH->PrintTocDataPtr != NULL)
 			{
-				_printTocEntry(AH, te, true);
+				_printTocEntry(AH, te, data_entry);
 
 				if (strcmp(te->desc, "BLOBS") == 0 ||
 					strcmp(te->desc, "BLOB COMMENTS") == 0)
@@ -1036,15 +1046,24 @@ restore_toc_entry(ArchiveHandle *AH, TocEntry *te, bool is_parallel)
 		{
 			/* If we haven't already dumped the defn part, do so now */
 			pg_log_info("executing %s %s", te->desc, te->tag);
-			_printTocEntry(AH, te, false);
+			_printTocEntry(AH, te, default_entry);
 		}
 	}
 
+	/*
+	 * If it has a statistics component that we want, then process that
+	 */
+	if ((reqs & REQ_STATS) != 0)
+	{
+		_printTocEntry(AH, te, statistics_entry);
+		defnDumped = true;
+	}
+
 	/*
 	 * If we emitted anything for this TOC entry, that counts as one action
 	 * against the transaction-size limit.  Commit if it's time to.
 	 */
-	if ((reqs & (REQ_SCHEMA | REQ_DATA)) != 0 && ropt->txn_size > 0)
+	if ((reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) != 0 && ropt->txn_size > 0)
 	{
 		if (++AH->txnCount >= ropt->txn_size)
 		{
@@ -1084,6 +1103,7 @@ NewRestoreOptions(void)
 	opts->compression_spec.level = 0;
 	opts->dumpSchema = true;
 	opts->dumpData = true;
+	opts->dumpStatistics = true;
 
 	return opts;
 }
@@ -1093,6 +1113,7 @@ _disableTriggersIfNecessary(ArchiveHandle *AH, TocEntry *te)
 {
 	RestoreOptions *ropt = AH->public.ropt;
 
+	ahprintf(AH, "-- ALTER TABLE DISABLE TRIGGER ALL %d %d;   \n\n", ropt->dumpSchema, ropt->disable_triggers);
 	/* This hack is only needed in a data-only restore */
 	if (ropt->dumpSchema || !ropt->disable_triggers)
 		return;
@@ -2962,6 +2983,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's statistics and we don't want statistics, maybe ignore it */
+	if (!ropt->dumpStatistics && strcmp(te->desc, "STATISTICS DATA") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2991,6 +3016,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS DATA") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
@@ -3107,6 +3133,15 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 		}
 	}
 
+	/*
+	 * Statistics Data entries have no other components.
+	 */
+	/* 
+	 * TODO: removed for now
+	if (strcmp(te->desc, "STATISTICS DATA") == 0)
+		return REQ_STATS;
+	*/
+
 	/*
 	 * Determine whether the TOC entry contains schema and/or data components,
 	 * and mask off inapplicable REQ bits.  If it had a dataDumper, assume
@@ -3729,7 +3764,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te)
  * will remain at default, until the matching ACL TOC entry is restored.
  */
 static void
-_printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData)
+_printTocEntry(ArchiveHandle *AH, TocEntry *te, entryType entry_type)
 {
 	RestoreOptions *ropt = AH->public.ropt;
 
@@ -3753,10 +3788,17 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData)
 		char	   *sanitized_schema;
 		char	   *sanitized_owner;
 
-		if (isData)
-			pfx = "Data for ";
-		else
-			pfx = "";
+		switch (entry_type)
+		{
+			case data_entry:
+				pfx = "Data for ";
+				break;
+			case statistics_entry:
+				pfx = "Statistics for ";
+				break;
+			default:
+				pfx = "";
+		}
 
 		ahprintf(AH, "--\n");
 		if (AH->public.verbose)
diff --git a/src/bin/pg_dump/pg_backup_archiver.h b/src/bin/pg_dump/pg_backup_archiver.h
index ce5ed1dd39..a2064f471e 100644
--- a/src/bin/pg_dump/pg_backup_archiver.h
+++ b/src/bin/pg_dump/pg_backup_archiver.h
@@ -209,7 +209,8 @@ typedef enum
 
 #define REQ_SCHEMA	0x01		/* want schema */
 #define REQ_DATA	0x02		/* want data */
-#define REQ_SPECIAL	0x04		/* for special TOC entries */
+#define REQ_STATS	0x04
+#define REQ_SPECIAL	0x08		/* for special TOC entries */
 
 struct _archiveHandle
 {
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 8f73a5df95..9e7cb2c48f 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -430,6 +430,7 @@ main(int argc, char **argv)
 	DataDirSyncMethod sync_method = DATA_DIR_SYNC_METHOD_FSYNC;
 	bool		data_only = false;
 	bool		schema_only = false;
+	bool		statistics_only = false;
 
 	static DumpOptions dopt;
 
@@ -466,6 +467,7 @@ main(int argc, char **argv)
 		{"encoding", required_argument, NULL, 'E'},
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
+		{"statistics-only", no_argument, NULL, 'X'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -492,8 +494,11 @@ main(int argc, char **argv)
 		{"strict-names", no_argument, &strict_names, 1},
 		{"use-set-session-authorization", no_argument, &dopt.use_setsessauth, 1},
 		{"no-comments", no_argument, &dopt.no_comments, 1},
+		{"no-data", no_argument, &dopt.no_data, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
+		{"no-schema", no_argument, &dopt.no_schema, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -539,7 +544,7 @@ main(int argc, char **argv)
 
 	InitDumpOptions(&dopt);
 
-	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:RsS:t:T:U:vwWxZ:",
+	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:PRsS:t:T:U:vwWxXZ:",
 							long_options, &optindex)) != -1)
 	{
 		switch (c)
@@ -613,6 +618,10 @@ main(int argc, char **argv)
 				dopt.cparams.pgport = pg_strdup(optarg);
 				break;
 
+			case 'X':			/* Dump statistics only */
+				statistics_only = true;
+				break;
+
 			case 'R':
 				/* no-op, still accepted for backwards compatibility */
 				break;
@@ -784,6 +793,17 @@ main(int argc, char **argv)
 
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (schema_only && statistics_only)
+		pg_fatal("options -s/--schema-only and -X/--statistics-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
+
+	if (data_only && dopt.no_data)
+		pg_fatal("options -a/--data-only and --no-data cannot be used together");
+	if (schema_only && dopt.no_schema)
+		pg_fatal("options -s/--schema-only and --no-schema cannot be used together");
+	if (statistics_only && dopt.no_statistics)
+		pg_fatal("options -X/--statistics-only and --no-statistics cannot be used together");
 
 	if (schema_only && foreign_servers_include_patterns.head != NULL)
 		pg_fatal("options -s/--schema-only and --include-foreign-data cannot be used together");
@@ -798,8 +818,9 @@ main(int argc, char **argv)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
 	/* set derivative flags */
-	dopt.dumpSchema = (!data_only);
-	dopt.dumpData = (!schema_only);
+	dopt.dumpData = data_only || (!schema_only && !statistics_only && !dopt.no_data);
+	dopt.dumpSchema = schema_only || (!data_only && !statistics_only && !dopt.no_schema);
+	dopt.dumpStatistics = statistics_only || (!data_only && !schema_only && !dopt.no_statistics);
 
 	/*
 	 * --inserts are already implied above if --column-inserts or
@@ -1099,6 +1120,7 @@ main(int argc, char **argv)
 	ropt->dropSchema = dopt.outputClean;
 	ropt->dumpData = dopt.dumpData;
 	ropt->dumpSchema = dopt.dumpSchema;
+	ropt->dumpStatistics = dopt.dumpStatistics;
 	ropt->if_exists = dopt.if_exists;
 	ropt->column_inserts = dopt.column_inserts;
 	ropt->dumpSections = dopt.dumpSections;
@@ -1177,7 +1199,7 @@ help(const char *progname)
 	printf(_("  -?, --help                   show this help, then exit\n"));
 
 	printf(_("\nOptions controlling the output content:\n"));
-	printf(_("  -a, --data-only              dump only the data, not the schema\n"));
+	printf(_("  -a, --data-only              dump only the data, not the schema or statistics\n"));
 	printf(_("  -b, --large-objects          include large objects in dump\n"));
 	printf(_("  --blobs                      (same as --large-objects, deprecated)\n"));
 	printf(_("  -B, --no-large-objects       exclude large objects in dump\n"));
@@ -1190,11 +1212,12 @@ help(const char *progname)
 	printf(_("  -N, --exclude-schema=PATTERN do NOT dump the specified schema(s)\n"));
 	printf(_("  -O, --no-owner               skip restoration of object ownership in\n"
 			 "                               plain-text format\n"));
-	printf(_("  -s, --schema-only            dump only the schema, no data\n"));
+	printf(_("  -s, --schema-only            dump only the schema, no data or statistics\n"));
 	printf(_("  -S, --superuser=NAME         superuser user name to use in plain-text format\n"));
 	printf(_("  -t, --table=PATTERN          dump only the specified table(s)\n"));
 	printf(_("  -T, --exclude-table=PATTERN  do NOT dump the specified table(s)\n"));
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
+	printf(_("  -X, --statistics-only        dump only the statistics, not schema or data\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
@@ -1219,8 +1242,11 @@ help(const char *progname)
 	printf(_("  --inserts                    dump data as INSERT commands, rather than COPY\n"));
 	printf(_("  --load-via-partition-root    load partitions via the root table\n"));
 	printf(_("  --no-comments                do not dump comment commands\n"));
+	printf(_("  --no-data                    do not dump data\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
+	printf(_("  --no-schema                  do not dump schema\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -6777,6 +6803,43 @@ getFuncs(Archive *fout)
 	destroyPQExpBuffer(query);
 }
 
+/*
+ * getRelationStatistics
+ *    register the statistics object as a dependent of the relation.
+ *
+ */
+static RelStatsInfo *
+getRelationStatistics(Archive *fout, DumpableObject *rel, char relkind)
+{
+	if ((relkind == RELKIND_RELATION) ||
+		(relkind == RELKIND_PARTITIONED_TABLE) ||
+		(relkind == RELKIND_INDEX) ||
+		(relkind == RELKIND_PARTITIONED_INDEX) ||
+		(relkind == RELKIND_MATVIEW))
+	{
+		RelStatsInfo	   *info = pg_malloc0(sizeof(RelStatsInfo));
+		DumpableObject	   *dobj = &info->dobj;
+
+		dobj->objType = DO_REL_STATS;
+		dobj->catId.tableoid = 0;
+		dobj->catId.oid = 0;
+		AssignDumpId(dobj);
+		dobj->dependencies = (DumpId *) pg_malloc(sizeof(DumpId));
+		dobj->dependencies[0] = rel->dumpId;
+		dobj->nDeps = 1;
+		dobj->allocDeps = 1;
+		dobj->components |= DUMP_COMPONENT_STATISTICS;
+		dobj->dump = rel->dump;
+		dobj->name = pg_strdup(rel->name);
+		dobj->namespace = rel->namespace;
+		info->relkind = relkind;
+		info->postponed_def = false;
+
+		return info;
+	}
+	return NULL;
+}
+
 /*
  * getTables
  *	  read all the tables (no indexes) in the system catalogs,
@@ -7154,6 +7217,7 @@ getTables(Archive *fout, int *numTables)
 
 		/* Tables have data */
 		tblinfo[i].dobj.components |= DUMP_COMPONENT_DATA;
+		tblinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
 
 		/* Mark whether table has an ACL */
 		if (!PQgetisnull(res, i, i_relacl))
@@ -7202,6 +7266,8 @@ getTables(Archive *fout, int *numTables)
 				}
 			}
 		}
+		if (tblinfo[i].interesting)
+			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relkind);
 	}
 
 	if (query->len != 0)
@@ -7648,11 +7714,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 		for (int c = 0; c < numinds; c++, j++)
 		{
 			char		contype;
+			char		indexkind;
+			RelStatsInfo   *relstats;
 
 			indxinfo[j].dobj.objType = DO_INDEX;
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -7675,7 +7744,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			{
 				NULL, NULL
 			};
+
+			if (indxinfo[j].parentidx == 0)
+				indexkind = RELKIND_INDEX;
+			else
+				indexkind = RELKIND_PARTITIONED_INDEX;
+
 			contype = *(PQgetvalue(res, j, i_contype));
+			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, indexkind);
 
 			if (contype == 'p' || contype == 'u' || contype == 'x')
 			{
@@ -7709,6 +7785,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				constrinfo->separate = true;
 
 				indxinfo[j].indexconstraint = constrinfo->dobj.dumpId;
+				if (relstats != NULL)
+					addObjectDependency(&relstats->dobj, constrinfo->dobj.dumpId);
 			}
 			else
 			{
@@ -10296,6 +10374,287 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * Tabular description of the parameters to pg_restore_relation_stats()
+ * param_name, param_type
+ */
+static const char *rel_stats_arginfo[][2] = {
+	{"relation", "regclass"},
+	{"version", "integer"},
+	{"relpages", "integer"},
+	{"reltuples", "real"},
+	{"relallvisible", "integer"},
+};
+
+/*
+ * Tabular description of the parameters to pg_restore_attribute_stats()
+ * param_name, param_type
+ */
+static const char *att_stats_arginfo[][2] = {
+	{"relation", "regclass"},
+	{"attname", "name"},
+	{"inherited", "boolean"},
+	{"version", "integer"},
+	{"null_frac", "float4"},
+	{"avg_width", "integer"},
+	{"n_distinct", "float4"},
+	{"most_common_vals", "text"},
+	{"most_common_freqs", "float4[]"},
+	{"histogram_bounds", "text"},
+	{"correlation", "float4"},
+	{"most_common_elems", "text"},
+	{"most_common_elem_freqs", "float4[]"},
+	{"elem_count_histogram", "float4[]"},
+	{"range_length_histogram", "text"},
+	{"range_empty_frac", "float4"},
+	{"range_bounds_histogram", "text"},
+};
+
+/*
+ * getRelStatsExportQuery --
+ *
+ * Generate a query that will fetch all relation (e.g. pg_class)
+ * stats for a given relation.
+ */
+static void
+getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "current_setting('server_version_num') AS version, "
+						 "c.relpages, c.reltuples, c.relallvisible "
+						 "FROM pg_class c "
+						 "JOIN pg_namespace n "
+						 "ON n.oid = c.relnamespace "
+						 "WHERE n.nspname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND c.relname = ");
+	appendStringLiteralAH(query, relname, fout);
+}
+
+/*
+ * getAttStatsExportQuery --
+ *
+ * Generate a query that will fetch all attribute (e.g. pg_statistic)
+ * stats for a given relation.
+ */
+static void
+getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "s.attname,"
+						 "s.inherited,"
+						 "current_setting('server_version_num') AS version, "
+						 "s.null_frac,"
+						 "s.avg_width,"
+						 "s.n_distinct,"
+						 "s.most_common_vals,"
+						 "s.most_common_freqs,"
+						 "s.histogram_bounds,"
+						 "s.correlation,"
+						 "s.most_common_elems,"
+						 "s.most_common_elem_freqs,"
+						 "s.elem_count_histogram,");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 "s.range_length_histogram,"
+							 "s.range_empty_frac,"
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(query,
+							 "NULL AS range_length_histogram,"
+							 "NULL AS range_empty_frac,"
+							 "NULL AS range_bounds_histogram ");
+
+	appendPQExpBufferStr(query,
+						 "FROM pg_stats s "
+						 "JOIN pg_namespace n "
+						 "ON n.nspname = s.schemaname "
+						 "JOIN pg_class c "
+						 "ON c.relname = s.tablename "
+						 "AND c.relnamespace = n.oid "
+						 "WHERE s.schemaname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND s.tablename = ");
+	appendStringLiteralAH(query, relname, fout);
+	appendPQExpBufferStr(query, " ORDER BY s.attname, s.inherited");
+}
+
+
+/*
+ * appendNamedArgument --
+ *
+ * Convenience routine for constructing parameters of the form:
+ * 'paraname', 'value'::type
+ */
+static void
+appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
+					const char *argval, const char *argtype)
+{
+	appendPQExpBufferStr(out, "\t");
+
+	appendStringLiteralAH(out, argname, fout);
+	appendPQExpBufferStr(out, ", ");
+
+	appendStringLiteralAH(out, argval, fout);
+	appendPQExpBuffer(out, "::%s", argtype);
+}
+
+/*
+ * appendRelStatsImport --
+ *
+ * Append a formatted pg_restore_relation_stats statement.
+ */
+static void
+appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	const char *sep = "";
+
+	if (PQntuples(res) == 0)
+		return;
+
+	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
+
+	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
+	{
+		const char *argname = rel_stats_arginfo[argno][0];
+		const char *argtype = rel_stats_arginfo[argno][1];
+		int			fieldno = PQfnumber(res, argname);
+
+		if (fieldno < 0)
+			pg_fatal("relation stats export query missing field '%s'",
+					 argname);
+
+		if (PQgetisnull(res, 0, fieldno))
+			continue;
+
+		appendPQExpBufferStr(out, sep);
+		appendNamedArgument(out, fout, argname, PQgetvalue(res, 0, fieldno), argtype);
+
+		sep = ",\n";
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+/*
+ * appendAttStatsImport --
+ *
+ * Append a series of formatted pg_restore_attribute_stats statements.
+ */
+static void
+appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	{
+		const char *sep = "";
+
+		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+		for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
+		{
+			const char *argname = att_stats_arginfo[argno][0];
+			const char *argtype = att_stats_arginfo[argno][1];
+			int			fieldno = PQfnumber(res, argname);
+
+			if (fieldno < 0)
+				pg_fatal("attribute stats export query missing field '%s'",
+						 argname);
+
+			if (PQgetisnull(res, rownum, fieldno))
+				continue;
+
+			appendPQExpBufferStr(out, sep);
+			appendNamedArgument(out, fout, argname, PQgetvalue(res, rownum, fieldno), argtype);
+			sep = ",\n";
+		}
+		appendPQExpBufferStr(out, "\n);\n");
+	}
+}
+
+/*
+ * Decide which section to use based on the relkind of the parent object.
+ *
+ * NB: materialized views may be postponed from SECTION_PRE_DATA to
+ * SECTION_POST_DATA to resolve some kinds of dependency problems. If so, the
+ * matview stats will also be postponed to SECTION_POST_DATA. See
+ * repairMatViewBoundaryMultiLoop().
+ */
+static teSection
+statisticsDumpSection(const RelStatsInfo *rsinfo)
+{
+	switch (rsinfo->relkind)
+	{
+		case RELKIND_RELATION:
+		case RELKIND_PARTITIONED_TABLE:
+		case RELKIND_MATVIEW:
+			return SECTION_DATA;
+		case RELKIND_INDEX:
+		case RELKIND_PARTITIONED_INDEX:
+			return SECTION_POST_DATA;
+		default:
+			pg_fatal("cannot dump statistics for relation kind '%c'",
+					 rsinfo->relkind);
+	}
+
+	return 0;				/* keep compiler quiet */
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	PGresult	   *res;
+	PQExpBuffer		query;
+	PQExpBuffer		out;
+	PQExpBuffer		tag;
+	DumpableObject *dobj = (DumpableObject *) &rsinfo->dobj;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", "STATISTICS DATA", fmtId(dobj->name));
+
+	query = createPQExpBuffer();
+	out = createPQExpBuffer();
+
+	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendRelStatsImport(out, fout, res);
+	PQclear(res);
+
+	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendAttStatsImport(out, fout, res);
+	PQclear(res);
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATISTICS DATA",
+							  .section = rsinfo->postponed_def ?
+								SECTION_POST_DATA :  statisticsDumpSection(rsinfo),
+							  .createStmt = out->data,
+							  .deps = dobj->dependencies,
+							  .nDeps = dobj->nDeps));
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(out);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -10744,6 +11103,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_SUBSCRIPTION_REL:
 			dumpSubscriptionTable(fout, (const SubRelInfo *) dobj);
 			break;
+		case DO_REL_STATS:
+			dumpRelationStats(fout, (const RelStatsInfo *) dobj);
+			break;
 		case DO_PRE_DATA_BOUNDARY:
 		case DO_POST_DATA_BOUNDARY:
 			/* never dumped, nothing to do */
@@ -17183,6 +17545,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 		free(indstatvalsarray);
 	}
 
+	/* Comments and stats share same .dep */
+
 	/* Dump Index Comments */
 	if (indxinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
 		dumpComment(fout, "INDEX", qindxname,
@@ -18970,6 +19334,16 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 				/* must come after the pre-data boundary */
 				addObjectDependency(dobj, preDataBound->dumpId);
 				break;
+			case DO_REL_STATS:
+				/* stats section varies by parent object type, DATA or POST */
+				if (statisticsDumpSection((RelStatsInfo *) dobj) == SECTION_DATA)
+				{
+					addObjectDependency(dobj, preDataBound->dumpId);
+					addObjectDependency(postDataBound, dobj->dumpId);
+				}
+				else
+					addObjectDependency(dobj, postDataBound->dumpId);
+				break;
 		}
 	}
 }
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index f62b564ed1..4edd88a54b 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -84,6 +84,7 @@ typedef enum
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION,
 	DO_SUBSCRIPTION_REL,		/* see note for SubRelInfo */
+	DO_REL_STATS,
 } DumpableObjectType;
 
 /*
@@ -109,6 +110,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
@@ -429,6 +431,13 @@ typedef struct _indexAttachInfo
 	IndxInfo   *partitionIdx;	/* link to index on partition */
 } IndexAttachInfo;
 
+typedef struct _relStatsInfo
+{
+	DumpableObject	dobj;
+	char			relkind;		/* 'r', 'v', 'c', etc */
+	bool			postponed_def;
+} RelStatsInfo;
+
 typedef struct _statsExtInfo
 {
 	DumpableObject dobj;
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index dc9a28924b..3a3602e3d2 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -801,11 +801,21 @@ repairMatViewBoundaryMultiLoop(DumpableObject *boundaryobj,
 {
 	/* remove boundary's dependency on object after it in loop */
 	removeObjectDependency(boundaryobj, nextobj->dumpId);
-	/* if that object is a matview, mark it as postponed into post-data */
+	/*
+	 * If that object is a matview or matview status, mark it as postponed into
+	 * post-data.
+	 */
 	if (nextobj->objType == DO_TABLE)
 	{
 		TableInfo  *nextinfo = (TableInfo *) nextobj;
 
+		if (nextinfo->relkind == RELKIND_MATVIEW)
+			nextinfo->postponed_def = true;
+	}
+	else if (nextobj->objType == DO_REL_STATS)
+	{
+		RelStatsInfo *nextinfo = (RelStatsInfo *) nextobj;
+
 		if (nextinfo->relkind == RELKIND_MATVIEW)
 			nextinfo->postponed_def = true;
 	}
@@ -1018,6 +1028,21 @@ repairDependencyLoop(DumpableObject **loop,
 					{
 						DumpableObject *nextobj;
 
+						nextobj = (j < nLoop - 1) ? loop[j + 1] : loop[0];
+						repairMatViewBoundaryMultiLoop(loop[j], nextobj);
+						return;
+					}
+				}
+			}
+			else if (loop[i]->objType == DO_REL_STATS &&
+					 ((RelStatsInfo *) loop[i])->relkind == RELKIND_MATVIEW)
+			{
+				for (j = 0; j < nLoop; j++)
+				{
+					if (loop[j]->objType == DO_POST_DATA_BOUNDARY)
+					{
+						DumpableObject *nextobj;
+
 						nextobj = (j < nLoop - 1) ? loop[j + 1] : loop[0];
 						repairMatViewBoundaryMultiLoop(loop[j], nextobj);
 						return;
@@ -1500,6 +1525,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "POST-DATA BOUNDARY  (ID %d)",
 					 obj->dumpId);
 			return;
+		case DO_REL_STATS:
+			snprintf(buf, bufsize,
+					 "RELATION STATISTICS FOR %s  (ID %d OID %u)",
+					 obj->name, obj->dumpId, obj->catId.oid);
+			return;
 	}
 	/* shouldn't get here */
 	snprintf(buf, bufsize,
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 396f79781c..7effb70490 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -103,6 +103,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -172,6 +173,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -451,6 +453,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -666,6 +670,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 88ae39d938..9586bd032c 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -63,6 +63,9 @@ main(int argc, char **argv)
 	int			numWorkers = 1;
 	Archive    *AH;
 	char	   *inputFileSpec;
+	bool		data_only = false;
+	bool		schema_only = false;
+	bool		statistics_only = false;
 	static int	disable_triggers = 0;
 	static int	enable_row_security = 0;
 	static int	if_exists = 0;
@@ -71,12 +74,13 @@ main(int argc, char **argv)
 	static int	outputNoTablespaces = 0;
 	static int	use_setsessauth = 0;
 	static int	no_comments = 0;
+	static int	no_data = 0;
 	static int	no_publications = 0;
+	static int	no_schema = 0;
 	static int	no_security_labels = 0;
+	static int  no_statistics = 0;
 	static int	no_subscriptions = 0;
 	static int	strict_names = 0;
-	bool		data_only = false;
-	bool		schema_only = false;
 
 	struct option cmdopts[] = {
 		{"clean", 0, NULL, 'c'},
@@ -108,6 +112,7 @@ main(int argc, char **argv)
 		{"username", 1, NULL, 'U'},
 		{"verbose", 0, NULL, 'v'},
 		{"single-transaction", 0, NULL, '1'},
+		{"statistics-only", no_argument, NULL, 'P'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -124,9 +129,12 @@ main(int argc, char **argv)
 		{"transaction-size", required_argument, NULL, 5},
 		{"use-set-session-authorization", no_argument, &use_setsessauth, 1},
 		{"no-comments", no_argument, &no_comments, 1},
+		{"no-data", no_argument, &no_data, 1},
 		{"no-publications", no_argument, &no_publications, 1},
+		{"no-schema", no_argument, &no_schema, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"filter", required_argument, NULL, 4},
 
 		{NULL, 0, NULL, 0}
@@ -271,6 +279,10 @@ main(int argc, char **argv)
 				opts->aclsSkip = 1;
 				break;
 
+			case 'X':			/* Restore statistics only */
+				statistics_only = true;
+				break;
+
 			case '1':			/* Restore data in a single transaction */
 				opts->single_txn = true;
 				opts->exit_on_error = true;
@@ -343,6 +355,10 @@ main(int argc, char **argv)
 
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
+	if (schema_only && statistics_only)
+		pg_fatal("options -s/--schema-only and -X/--statistics-only cannot be used together");
 
 	if (data_only && opts->dropSchema)
 		pg_fatal("options -c/--clean and -a/--data-only cannot be used together");
@@ -362,8 +378,9 @@ main(int argc, char **argv)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
 	/* set derivative flags */
-	opts->dumpSchema = (!data_only);
-	opts->dumpData = (!schema_only);
+	opts->dumpData = data_only || (!no_data && !schema_only && !statistics_only);
+	opts->dumpSchema = schema_only || (!no_schema && !data_only && !statistics_only);
+	opts->dumpStatistics = statistics_only || (!no_statistics && !data_only && !schema_only);
 
 	opts->disable_triggers = disable_triggers;
 	opts->enable_row_security = enable_row_security;
@@ -375,6 +392,8 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
+	opts->no_data = no_data;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
@@ -484,6 +503,7 @@ usage(const char *progname)
 	printf(_("  -t, --table=NAME             restore named relation (table, view, etc.)\n"));
 	printf(_("  -T, --trigger=NAME           restore named trigger\n"));
 	printf(_("  -x, --no-privileges          skip restoration of access privileges (grant/revoke)\n"));
+	printf(_("  -X, --statistics-only        restore only the statistics, not schema or data\n"));
 	printf(_("  -1, --single-transaction     restore as a single transaction\n"));
 	printf(_("  --disable-triggers           disable triggers during data-only restore\n"));
 	printf(_("  --enable-row-security        enable row security\n"));
@@ -491,10 +511,14 @@ usage(const char *progname)
 			 "                               in FILENAME\n"));
 	printf(_("  --if-exists                  use IF EXISTS when dropping objects\n"));
 	printf(_("  --no-comments                do not restore comment commands\n"));
+	printf(_("  --no-data                    do not restore data\n"));
 	printf(_("  --no-data-for-failed-tables  do not restore data of tables that could not be\n"
 			 "                               created\n"));
 	printf(_("  --no-publications            do not restore publications\n"));
+	printf(_("  --no-schema                  do not restore schema\n"));
 	printf(_("  --no-security-labels         do not restore security labels\n"));
+	printf(_("  --no-statistics              do not restore statistics\n"));
+	/* This hack is only needed in a data-only restore */
 	printf(_("  --no-subscriptions           do not restore subscriptions\n"));
 	printf(_("  --no-table-access-method     do not restore table access methods\n"));
 	printf(_("  --no-tablespaces             do not restore tablespace assignments\n"));
diff --git a/src/bin/pg_dump/t/001_basic.pl b/src/bin/pg_dump/t/001_basic.pl
index 214240f1ae..f29da06ed2 100644
--- a/src/bin/pg_dump/t/001_basic.pl
+++ b/src/bin/pg_dump/t/001_basic.pl
@@ -50,12 +50,30 @@ command_fails_like(
 	'pg_dump: options -s/--schema-only and -a/--data-only cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '-s', '-X' ],
+	qr/\Qpg_dump: error: options -s\/--schema-only and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: error: options -s/--schema-only and -X/--statistics-only cannot be used together'
+);
+
+command_fails_like(
+	[ 'pg_dump', '-a', '-X' ],
+	qr/\Qpg_dump: error: options -a\/--data-only and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: error: options -a/--data-only and -X/--statistics-only cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-s', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: options -s\/--schema-only and --include-foreign-data cannot be used together\E/,
 	'pg_dump: options -s/--schema-only and --include-foreign-data cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '--statistics-only', '--no-statistics' ],
+	qr/\Qpg_dump: error: options -X\/--statistics-only and --no-statistics cannot be used together\E/,
+	'pg_dump: options -X\/--statistics-only and --no-statistics cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-j2', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: option --include-foreign-data is not supported with parallel backup\E/,
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index bf65d44b94..737b184ea9 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -66,7 +66,7 @@ my %pgdump_runs = (
 			'--format=custom',
 			"--file=$tempdir/binary_upgrade.dump",
 			'-w',
-			'--schema-only',
+			'--no-data',
 			'--binary-upgrade',
 			'-d', 'postgres',    # alternative way to specify database
 		],
@@ -645,7 +645,19 @@ my %pgdump_runs = (
 
 			'--schema=dump_test', '-b', '-B', '--no-sync', 'postgres',
 		],
-	},);
+	},
+	no_statistics => {
+		dump_cmd => [
+			'pg_dump', "--file=$tempdir/no_statistics.sql",
+			'--no-sync', '--no-statistics', 'postgres',
+		],
+	},
+	no_schema => {
+		dump_cmd => [
+			'pg_dump', "--file=$tempdir/no_schema.sql",
+			'--no-sync', '--no-schema', 'postgres',
+		],
+	});
 
 ###############################################################
 # Definition of the tests to run.
@@ -711,6 +723,7 @@ my %full_runs = (
 	no_large_objects => 1,
 	no_owner => 1,
 	no_privs => 1,
+	no_statistics => 1,
 	no_table_access_method => 1,
 	pg_dumpall_dbprivs => 1,
 	pg_dumpall_exclude => 1,
@@ -912,6 +925,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1325,6 +1339,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1346,6 +1361,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1367,6 +1383,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1533,6 +1550,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1, 
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1686,6 +1704,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			only_dump_test_table => 1,
 			section_data => 1,
 		},
@@ -1713,6 +1732,7 @@ my %tests = (
 			data_only => 1,
 			exclude_test_table => 1,
 			exclude_test_table_data => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1733,7 +1753,10 @@ my %tests = (
 			\QCOPY dump_test.fk_reference_test_table (col1) FROM stdin;\E
 			\n(?:\d\n){5}\\\.\n
 			/xms,
-		like => { data_only => 1, },
+		like => {
+			data_only => 1,
+			no_schema => 1,
+		},
 	},
 
 	'COPY test_second_table' => {
@@ -1749,6 +1772,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1771,6 +1795,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1794,6 +1819,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1816,6 +1842,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1838,6 +1865,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -3234,6 +3262,7 @@ my %tests = (
 		like => {
 			%full_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 			only_dump_test_schema => 1,
 			test_schema_plus_large_objects => 1,
@@ -3404,6 +3433,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			only_dump_measurement => 1,
 			section_data => 1,
 			only_dump_test_schema => 1,
@@ -4286,6 +4316,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 			binary_upgrade => 1,
@@ -4586,6 +4617,30 @@ my %tests = (
 		},
 	},
 
+	#
+	# Table statistics should go in section=data.
+	# Materialized view statistics should go in section=post-data.
+	#
+	'statistics_import' => {
+	create_sql => '
+		CREATE TABLE dump_test.has_stats
+		AS SELECT g.g AS x, g.g / 2 AS y FROM generate_series(1,100) AS g(g);
+		CREATE TABLE dump_test.has_stats_mv AS SELECT * FROM dump_test.has_stats;
+		ANALYZE dump_test.has_stats, dump_test.has_stats_mv;',
+	regexp => qr/pg_catalog.pg_restore_attribute_stats/,
+	like => {
+		%full_runs,
+		%dump_test_schema_runs,
+		section_data => 1,
+		},
+	unlike => {
+		exclude_dump_test_schema => 1,
+		no_statistics => 1,
+		only_dump_measurement => 1,
+		schema_only => 1,
+		},
+	},
+
 	# CREATE TABLE with partitioned table and various AMs.  One
 	# partition uses the same default as the parent, and a second
 	# uses its own AM.
diff --git a/src/bin/pg_upgrade/dump.c b/src/bin/pg_upgrade/dump.c
index 8ce0fa3020..a29cd2cca9 100644
--- a/src/bin/pg_upgrade/dump.c
+++ b/src/bin/pg_upgrade/dump.c
@@ -21,10 +21,11 @@ generate_old_dump(void)
 
 	/* run new pg_dumpall binary for globals */
 	exec_prog(UTILITY_LOG_FILE, NULL, true, true,
-			  "\"%s/pg_dumpall\" %s --globals-only --quote-all-identifiers "
+			  "\"%s/pg_dumpall\" %s %s --globals-only --quote-all-identifiers "
 			  "--binary-upgrade %s --no-sync -f \"%s/%s\"",
 			  new_cluster.bindir, cluster_conn_opts(&old_cluster),
 			  log_opts.verbose ? "--verbose" : "",
+			  user_opts.do_statistics ? "" : "--no-statistics",
 			  log_opts.dumpdir,
 			  GLOBALS_DUMP_FILE);
 	check_ok();
@@ -52,10 +53,11 @@ generate_old_dump(void)
 		snprintf(log_file_name, sizeof(log_file_name), DB_DUMP_LOG_FILE_MASK, old_db->db_oid);
 
 		parallel_exec_prog(log_file_name, NULL,
-						   "\"%s/pg_dump\" %s --schema-only --quote-all-identifiers "
+						   "\"%s/pg_dump\" %s --no-data %s --quote-all-identifiers "
 						   "--binary-upgrade --format=custom %s --no-sync --file=\"%s/%s\" %s",
 						   new_cluster.bindir, cluster_conn_opts(&old_cluster),
 						   log_opts.verbose ? "--verbose" : "",
+						   user_opts.do_statistics ? "" : "--no-statistics",
 						   log_opts.dumpdir,
 						   sql_file_name, escaped_connstr.data);
 
diff --git a/src/bin/pg_upgrade/option.c b/src/bin/pg_upgrade/option.c
index 108eb7a1ba..3b6c7ec994 100644
--- a/src/bin/pg_upgrade/option.c
+++ b/src/bin/pg_upgrade/option.c
@@ -60,6 +60,8 @@ parseCommandLine(int argc, char *argv[])
 		{"copy", no_argument, NULL, 2},
 		{"copy-file-range", no_argument, NULL, 3},
 		{"sync-method", required_argument, NULL, 4},
+		{"with-statistics", no_argument, NULL, 5},
+		{"no-statistics", no_argument, NULL, 6},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -70,6 +72,7 @@ parseCommandLine(int argc, char *argv[])
 
 	user_opts.do_sync = true;
 	user_opts.transfer_mode = TRANSFER_MODE_COPY;
+	user_opts.do_statistics = true;
 
 	os_info.progname = get_progname(argv[0]);
 
@@ -212,6 +215,13 @@ parseCommandLine(int argc, char *argv[])
 				user_opts.sync_method = pg_strdup(optarg);
 				break;
 
+			case 5:
+				user_opts.do_statistics = true;
+				break;
+			case 6:
+				user_opts.do_statistics = false;
+				break;
+
 			default:
 				fprintf(stderr, _("Try \"%s --help\" for more information.\n"),
 						os_info.progname);
@@ -306,7 +316,9 @@ usage(void)
 	printf(_("  --clone                       clone instead of copying files to new cluster\n"));
 	printf(_("  --copy                        copy files to new cluster (default)\n"));
 	printf(_("  --copy-file-range             copy files to new cluster with copy_file_range\n"));
+	printf(_("  --no-statistics               do not import statistics from old cluster\n"));
 	printf(_("  --sync-method=METHOD          set method for syncing files to disk\n"));
+	printf(_("  --with-statistics             import statistics from old cluster (default)\n"));
 	printf(_("  -?, --help                    show this help, then exit\n"));
 	printf(_("\n"
 			 "Before running pg_upgrade you must:\n"
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index 0cdd675e4f..3fe111fbde 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -327,6 +327,7 @@ typedef struct
 	int			jobs;			/* number of processes/threads to use */
 	char	   *socketdir;		/* directory to use for Unix sockets */
 	char	   *sync_method;
+	bool		do_statistics;	/* carry over statistics from old cluster */
 } UserOpts;
 
 typedef struct
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index d66e901f51..5e58f24d21 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -123,7 +123,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Dump only the data, not the schema (data definitions).
+        Dump only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are dumped.
        </para>
 
@@ -141,13 +141,11 @@ PostgreSQL documentation
       <listitem>
        <para>
         Include large objects in the dump.  This is the default behavior
-        except when <option>--schema</option>, <option>--table</option>, or
-        <option>--schema-only</option> is specified.  The <option>-b</option>
-        switch is therefore only useful to add large objects to dumps
-        where a specific schema or table has been requested.  Note that
-        large objects are considered data and therefore will be included when
-        <option>--data-only</option> is used, but not
-        when <option>--schema-only</option> is.
+        except when <option>--schema</option>, <option>--table</option>,
+        <option>--schema-only</option>, or <option>--statistics-only</option>, or
+        <option>--no-data</option> is specified.  The <option>-b</option>
+        switch is therefore only useful to add large objects to dumps where a
+        specific schema or table has been requested.
        </para>
       </listitem>
      </varlistentry>
@@ -516,10 +514,11 @@ PostgreSQL documentation
       <term><option>--schema-only</option></term>
       <listitem>
        <para>
-        Dump only the object definitions (schema), not data.
+        Dump only the object definitions (schema), not data or statistics.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive to <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
         <option>--section=pre-data --section=post-data</option>.
@@ -652,6 +651,18 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Dump only the statistics, not the schema (data definitions) or data.
+        Statistics for tables, materialized views, and indexes are dumped.
+       </para>
+
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-Z <replaceable class="parameter">level</replaceable></option></term>
       <term><option>-Z <replaceable class="parameter">method</replaceable></option>[:<replaceable>detail</replaceable>]</term>
@@ -824,16 +835,17 @@ PostgreSQL documentation
       <term><option>--exclude-table-data=<replaceable class="parameter">pattern</replaceable></option></term>
       <listitem>
        <para>
-        Do not dump data for any tables matching <replaceable
+        Do not dump data or statistics for any tables matching <replaceable
         class="parameter">pattern</replaceable>. The pattern is
         interpreted according to the same rules as for <option>-t</option>.
         <option>--exclude-table-data</option> can be given more than once to
-        exclude tables matching any of several patterns. This option is
-        useful when you need the definition of a particular table even
-        though you do not need the data in it.
+        exclude tables matching any of several patterns. This option is useful
+        when you need the definition of a particular table even though you do
+        not need the data in it.
        </para>
        <para>
-        To exclude data for all tables in the database, see <option>--schema-only</option>.
+        To exclude data for all tables in the database, see <option>--schema-only</option>
+        or <option>--statistics-only</option>.
        </para>
       </listitem>
      </varlistentry>
@@ -1080,6 +1092,15 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-data</option></term>
+      <listitem>
+       <para>
+        Do not dump data.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-publications</option></term>
       <listitem>
@@ -1098,6 +1119,24 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-schema</option></term>
+      <listitem>
+       <para>
+        Do not dump schema (data definitions).
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_dumpall.sgml b/doc/src/sgml/ref/pg_dumpall.sgml
index 014f279258..d423153a93 100644
--- a/doc/src/sgml/ref/pg_dumpall.sgml
+++ b/doc/src/sgml/ref/pg_dumpall.sgml
@@ -265,6 +265,17 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Dump only the statistics, not the schema (data definitions) or data.
+        Statistics for tables, materialized views, and indexes are dumped.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--binary-upgrade</option></term>
       <listitem>
@@ -422,6 +433,15 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-data</option></term>
+      <listitem>
+       <para>
+        Do not dump data.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-publications</option></term>
       <listitem>
@@ -447,6 +467,15 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-schema</option></term>
+      <listitem>
+       <para>
+        Do not dump schema (data definitions).
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-security-labels</option></term>
       <listitem>
@@ -456,6 +485,15 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index b8b27e1719..22c3c118ad 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -94,7 +94,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Restore only the data, not the schema (data definitions).
+        Restore only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are restored,
         if present in the archive.
        </para>
@@ -483,10 +483,11 @@ PostgreSQL documentation
         to the extent that schema entries are present in the archive.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive of <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
-        <option>--section=pre-data --section=post-data</option>.
+        <option>--section=pre-data --section=post-data --no-statistics</option>.
        </para>
        <para>
         (Do not confuse this with the <option>--schema</option> option, which
@@ -599,6 +600,20 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Restore only the statistics, not schema (data definitions) or data.
+       </para>
+       <para>
+        (Do not confuse this with the <option>--schema</option> option, which
+        uses the word <quote>schema</quote> in a different meaning.)
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-1</option></term>
       <term><option>--single-transaction</option></term>
@@ -681,6 +696,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-data</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore data, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-data-for-failed-tables</option></term>
       <listitem>
@@ -713,6 +738,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-schema</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore schema (data definitions), even if
+        the archive contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-security-labels</option></term>
       <listitem>
@@ -723,6 +758,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore statistics, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pgupgrade.sgml b/doc/src/sgml/ref/pgupgrade.sgml
index 4777381dac..64a1ebd613 100644
--- a/doc/src/sgml/ref/pgupgrade.sgml
+++ b/doc/src/sgml/ref/pgupgrade.sgml
@@ -145,6 +145,24 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--with-statistics</option></term>
+      <listitem>
+       <para>
+        Restore statistics from the old cluster into the new cluster.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not restore statistics from the old cluster into the new cluster.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-o</option> <replaceable class="parameter">options</replaceable></term>
       <term><option>--old-options</option> <replaceable class="parameter">options</replaceable></term>
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 94dc956ae8..eab0d0f84c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2397,6 +2397,7 @@ RelMapFile
 RelMapping
 RelOptInfo
 RelOptKind
+RelStatsInfo
 RelToCheck
 RelToCluster
 RelabelType
-- 
2.48.0

#273jian he
jian.universality@gmail.com
In reply to: Corey Huinker (#272)
Re: Statistics Import and Export

On Thu, Jan 16, 2025 at 4:56 AM Corey Huinker <corey.huinker@gmail.com> wrote:

Attached is just the pg_dump stuff, and only for relation/attribute stats. The extended stats and vacuumdb work will be in their own threads going forward.

I didn't follow this thread actively,
so if the following issue is already addressed, please forgive me.

pg_dump --dbname=src2 --table=tenk1 --statistics-only --verbose > x.sql
there no pg_restore_attribute_stats, pg_restore_relation_stats there
for table tenk1.
There aren't any statistics, should there be?

also should
pg_dump --dbname=src2 --no-data --no-schema --table=tenk1
dump the statistics? currently seems no.

in doc/src/sgml/ref/pg_dump.sgml
there are six options to control the main output now.
--schema-only, --statistics-only, --data-only,
--no-schema, --no-data, --no-statistics
maybe we need spare one paragraph to explain the default behavior,
and also have an example on it?

pg_dump --dbname=src2 --table=tenk1 --verbose > 1.sql
seems the statistics dump (pg_restore_attribute_stats) is ordered by
attribute name.
should it make more sense to order by attnum?

getRelationStatistics
typedef struct _relStatsInfo
{
DumpableObject dobj;
char relkind; /* 'r', 'v', 'c', etc */
bool postponed_def;
} RelStatsInfo;
comment /* 'r', 'v', 'c', etc */
Is it wrong? there is no relkind='c'.
field postponed_def really deserves a comment.

we also need change
enum dbObjectTypePriorities
static const int dbObjectTypePriority[]
?

in dumpRelationStats, we can add Assert on it.
if (!fout->dopt->dumpStatistics)
return;
Assert(dobj->dump & DUMP_COMPONENT_STATISTICS);

I found out the owner's info is missing in the dumped content.
for example, the line "Name: STATISTICS DATA tenk1_pkey;" missing owner info.
not sure this is intended?

--
-- Name: tenk1 tenk1_pkey; Type: CONSTRAINT; Schema: public; Owner: jian
--
ALTER TABLE ONLY public.tenk1
ADD CONSTRAINT tenk1_pkey PRIMARY KEY (unique1);
--
-- Name: STATISTICS DATA tenk1_pkey; Type: STATISTICS DATA; Schema:
public; Owner: -
--
SELECT * FROM pg_catalog.pg_restore_relation_stats(
'relation', 'public.tenk1_pkey'::regclass,
'version', '180000'::integer,
'relpages', '30'::integer,
'reltuples', '10000'::real,
'relallvisible', '0'::integer
);

#274Corey Huinker
corey.huinker@gmail.com
In reply to: jian he (#273)
Re: Statistics Import and Export

pg_dump --dbname=src2 --table=tenk1 --statistics-only --verbose > x.sql
there no pg_restore_attribute_stats, pg_restore_relation_stats there
for table tenk1.
There aren't any statistics, should there be?

pg_restore_relation_stats: yes. looking into that.
pg_restore_attribute_stats: yes IF the table had been analyzed, no
otherwise.

also should
pg_dump --dbname=src2 --no-data --no-schema --table=tenk1
dump the statistics? currently seems no.

in doc/src/sgml/ref/pg_dump.sgml
there are six options to control the main output now.
--schema-only, --statistics-only, --data-only,
--no-schema, --no-data, --no-statistics
maybe we need spare one paragraph to explain the default behavior,
and also have an example on it?

+1

pg_dump --dbname=src2 --table=tenk1 --verbose > 1.sql
seems the statistics dump (pg_restore_attribute_stats) is ordered by
attribute name.
should it make more sense to order by attnum?

For security reasons, we pull attribute statistics from pg_stats (
https://www.postgresql.org/docs/current/view-pg-stats.html) which does not
have attnum. The only reason we order the list is to ensure that dump
comparison tests match.

getRelationStatistics
typedef struct _relStatsInfo
{
DumpableObject dobj;
char relkind; /* 'r', 'v', 'c', etc */
bool postponed_def;
} RelStatsInfo;
comment /* 'r', 'v', 'c', etc */
Is it wrong? there is no relkind='c'.

Yeah, I think 'c' should be an 'm' there.

field postponed_def really deserves a comment.

Can do.

we also need change
enum dbObjectTypePriorities
static const int dbObjectTypePriority[]
?

We'd need two entries, because that enum includes PRIO_PRE_DATA_BOUNDARY
and PRIO_POST_DATA_BOUNDARY, and statistics can either be in DATA or
POST_DATA.

in dumpRelationStats, we can add Assert on it.
if (!fout->dopt->dumpStatistics)
return;
Assert(dobj->dump & DUMP_COMPONENT_STATISTICS);

I found out the owner's info is missing in the dumped content.
for example, the line "Name: STATISTICS DATA tenk1_pkey;" missing owner
info.
not sure this is intended?

Good question. I'm not sure if we need it or not. If stats had an owner,
it'd be the owner of the relation.

#275jian he
jian.universality@gmail.com
In reply to: Corey Huinker (#274)
1 attachment(s)
Re: Statistics Import and Export

hi.

SELECT * FROM pg_catalog.pg_restore_relation_stats(
'relation', 'public.tenk1_hundred'::regclass,
'version', '180000'::integer,
'relpages', '11'::integer,
'reltuples', '10000'::real,
'relallvisible', '0'::integer
);
dump and execute the above query generated a warning
WARNING: missing lock for relation "tenk1_hundred" (OID 18431,
relkind i) @ TID (15,34)

in dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
{
getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
dobj->name);
res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
appendAttStatsImport(out, fout, res);
PQclear(res)
}
i think, getAttStatsExportQuery no need to join catalog pg_namespace.

for example, after getAttStatsExportQuery, query->data looks like:
SELECT c.oid::regclass AS relation,
s.attname,s.inherited,
current_setting('server_version_num') AS version,
s.null_frac,
s.avg_width,
s.n_distinct,
s.most_common_vals,
s.most_common_freqs,
s.histogram_bounds,
s.correlation,
s.most_common_elems,
s.most_common_elem_freqs,
s.elem_count_histogram,
s.range_length_histogram,
s.range_empty_frac,
s.range_bounds_histogram
FROM pg_stats s JOIN pg_namespace n ON n.nspname = s.schemaname JOIN pg_class c
ON c.relname = s.tablename AND c.relnamespace = n.oid
WHERE s.schemaname = 'public'
AND s.tablename = 'varchar_tbl'
ORDER BY s.attname, s.inherited
-----------------------
The SELECT column list doesn't mention/use any of the pg_namespace columns.

```WHERE s.schemaname = 'public' AND s.tablename = 'varchar_tbl'``
" s.schemaname" combined with "s.tablename" will make sure that the
output is unique, at most one row.

i did a minor refactor about validation dumpData, dumpSchema,
dumpStatistics option.
I think it's more intuitive. first we process ``*only``option then
process no* option

Attachments:

refactor_pg_dump_onlyoption.no-cfbotapplication/octet-stream; name=refactor_pg_dump_onlyoption.no-cfbotDownload
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 9e7cb2c48f..4a2e3e123c 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -818,9 +818,16 @@ main(int argc, char **argv)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
 	/* set derivative flags */
-	dopt.dumpData = data_only || (!schema_only && !statistics_only && !dopt.no_data);
-	dopt.dumpSchema = schema_only || (!data_only && !statistics_only && !dopt.no_schema);
-	dopt.dumpStatistics = statistics_only || (!data_only && !schema_only && !dopt.no_statistics);
+	dopt.dumpData = data_only || (!schema_only && !statistics_only);
+	dopt.dumpSchema = schema_only || (!data_only && !statistics_only);
+	dopt.dumpStatistics = statistics_only || (!data_only && !schema_only);
+
+	if (dopt.no_data)
+		dopt.dumpData = false;
+	if (dopt.no_schema)
+		dopt.dumpSchema = false;
+	if (dopt.no_statistics)
+		dopt.dumpStatistics = false;
 
 	/*
 	 * --inserts are already implied above if --column-inserts or
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 9586bd032c..4eb40df1cb 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -378,9 +378,16 @@ main(int argc, char **argv)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
 	/* set derivative flags */
-	opts->dumpData = data_only || (!no_data && !schema_only && !statistics_only);
-	opts->dumpSchema = schema_only || (!no_schema && !data_only && !statistics_only);
-	opts->dumpStatistics = statistics_only || (!no_statistics && !data_only && !schema_only);
+	opts->dumpData = data_only || (!schema_only && !statistics_only);
+	opts->dumpSchema = schema_only || (!data_only && !statistics_only);
+	opts->dumpStatistics = statistics_only || (!data_only && !schema_only);
+
+	if (opts->no_data)
+		opts->dumpData = false;
+	if (opts->no_schema)
+		opts->dumpSchema = false;
+	if (opts->no_statistics)
+		opts->dumpStatistics = false;
 
 	opts->disable_triggers = disable_triggers;
 	opts->enable_row_security = enable_row_security;
#276jian he
jian.universality@gmail.com
In reply to: jian he (#275)
1 attachment(s)
Re: Statistics Import and Export

On Fri, Jan 17, 2025 at 10:20 PM jian he <jian.universality@gmail.com> wrote:

hi.

SELECT * FROM pg_catalog.pg_restore_relation_stats(
'relation', 'public.tenk1_hundred'::regclass,
'version', '180000'::integer,
'relpages', '11'::integer,
'reltuples', '10000'::real,
'relallvisible', '0'::integer
);
dump and execute the above query generated a warning
WARNING: missing lock for relation "tenk1_hundred" (OID 18431,
relkind i) @ TID (15,34)

This seems to be an existing issue.
For pg_restore_relation_stats, we don't have regress tests for index relation.
I am not sure the WARNING is ok.

I found out that the previous mail attached no-cfbot
(refactor_pg_dump_onlyoption.no-cfbot)
refactoring of statistics, data, schema is not fully correct.
This email attached no-cfbot,
I think it is tuitive and correct refactor of handling these three options.

typedef struct _dumpOptions, typedef struct _restoreOptions
we already have three bools (dumpSchema, dumpData, dumpStatistics).
Why do we need three int (no_data, no_schema, no_statistics) fields
for these two structs?
since they represent the same information. (for example, no_data == 1,
means/imply dumpData is false)
(disclaimer, this part I didn't dig deeper).

doc/src/sgml/ref/pg_restore.sgml
<varlistentry>
<term><option>-X</option></term>
<term><option>--statistics-only</option></term>
<listitem>
<para>
Restore only the statistics, not schema (data definitions) or data.
</para>
<para>
(Do not confuse this with the <option>--schema</option> option, which
uses the word <quote>schema</quote> in a different meaning.)
</para>
</listitem>
</varlistentry>
here, we don't need to mention
"(Do not confuse this with the <option>--schema</option> option, which"... part?

--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -108,6 +112,7 @@ main(int argc, char **argv)
         {"username", 1, NULL, 'U'},
         {"verbose", 0, NULL, 'v'},
         {"single-transaction", 0, NULL, '1'},
+        {"statistics-only", no_argument, NULL, 'P'},

Here it should be
+ {"statistics-only", no_argument, NULL, 'X'},
?

If we introduced REQ_STATS, then better checking all the REQ_DATA occurrences,
does REQ_STATS apply to there also?
for example in pg_backup_tar.c and pg_backup_directory.c,
functions: WriteToc, function WriteDataChunks, RestoreArchive.

-----------------------------------------------
I tested locally, dump, restore, directory, custom format is not
working as intended, with v38.
I use the following to test it.
CONN2 is my local connect string.
BIN2 is a local bin directory.
varchar_tbl.dir is directory format dump full output, including data,
schema, statistics.
-----------------------------------------------
${CONN2} -c 'drop table varchar_tbl;'
$BIN2/pg_restore --dbname=src2 --list varchar_tbl.dir
#only schema
$BIN2/pg_restore --dbname=src2 --format=directory --no-statistics
--no-data varchar_tbl.dir
${CONN2} -c 'select attname=$$f1$$ as expect_zero_row from pg_stats
where tablename = $$varchar_tbl$$;'
${CONN2} -c 'select (reltuples < 0 and relpages = 0) as expect_true
from pg_class where relname = $$varchar_tbl$$;'
#only data
$BIN2/pg_restore --dbname=src2 --format=directory --no-statistics
--no-schema varchar_tbl.dir
${CONN2} -c 'select attname=$$f1$$ as expect_zero_row from pg_stats
where tablename = $$varchar_tbl$$;'
${CONN2} -c 'select (reltuples < 0 and relpages = 0) as expect_true
from pg_class where relname = $$varchar_tbl$$;'
#only statistics
$BIN2/pg_restore --dbname=src2 --format=directory --statistics-only
varchar_tbl.dir
${CONN2} -c 'select attname=$$f1$$ as expect_zero_row from pg_stats
where tablename = $$varchar_tbl$$;'
${CONN2} -c 'select reltuples > 0 and relpages > 0 as expect_true from
pg_class where relname = $$varchar_tbl$$;'

Attachments:

v1-0001-misc-minor-refactoring.no-cfbotapplication/octet-stream; name=v1-0001-misc-minor-refactoring.no-cfbotDownload
From 95a12a8ae143f12569d92a483904ed36a116d80d Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Sat, 18 Jan 2025 23:27:42 +0800
Subject: [PATCH v1 1/1] misc minor refactoring.

---
 src/bin/pg_dump/pg_dump.c    | 13 ++++++++++---
 src/bin/pg_dump/pg_restore.c | 14 ++++++++++----
 2 files changed, 20 insertions(+), 7 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 9e7cb2c48f..4a2e3e123c 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -818,9 +818,16 @@ main(int argc, char **argv)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
 	/* set derivative flags */
-	dopt.dumpData = data_only || (!schema_only && !statistics_only && !dopt.no_data);
-	dopt.dumpSchema = schema_only || (!data_only && !statistics_only && !dopt.no_schema);
-	dopt.dumpStatistics = statistics_only || (!data_only && !schema_only && !dopt.no_statistics);
+	dopt.dumpData = data_only || (!schema_only && !statistics_only);
+	dopt.dumpSchema = schema_only || (!data_only && !statistics_only);
+	dopt.dumpStatistics = statistics_only || (!data_only && !schema_only);
+
+	if (dopt.no_data)
+		dopt.dumpData = false;
+	if (dopt.no_schema)
+		dopt.dumpSchema = false;
+	if (dopt.no_statistics)
+		dopt.dumpStatistics = false;
 
 	/*
 	 * --inserts are already implied above if --column-inserts or
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 9586bd032c..0e0e6edc0a 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -112,7 +112,7 @@ main(int argc, char **argv)
 		{"username", 1, NULL, 'U'},
 		{"verbose", 0, NULL, 'v'},
 		{"single-transaction", 0, NULL, '1'},
-		{"statistics-only", no_argument, NULL, 'P'},
+		{"statistics-only", no_argument, NULL, 'X'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -378,9 +378,15 @@ main(int argc, char **argv)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
 	/* set derivative flags */
-	opts->dumpData = data_only || (!no_data && !schema_only && !statistics_only);
-	opts->dumpSchema = schema_only || (!no_schema && !data_only && !statistics_only);
-	opts->dumpStatistics = statistics_only || (!no_statistics && !data_only && !schema_only);
+	opts->dumpData = data_only || (!schema_only && !statistics_only);
+	opts->dumpSchema = schema_only || (!data_only && !statistics_only);
+	opts->dumpStatistics = statistics_only || (!data_only && !schema_only);
+	if (no_data)
+		opts->dumpData = false;
+	if (no_schema)
+		opts->dumpSchema = false;
+	if (no_statistics)
+		opts->dumpStatistics = false;
 
 	opts->disable_triggers = disable_triggers;
 	opts->enable_row_security = enable_row_security;
-- 
2.34.1

#277Michael Paquier
michael@paquier.xyz
In reply to: jian he (#276)
Re: Statistics Import and Export

On Sun, Jan 19, 2025 at 01:00:04AM +0800, jian he wrote:

On Fri, Jan 17, 2025 at 10:20 PM jian he <jian.universality@gmail.com> wrote:

dump and execute the above query generated a warning
WARNING: missing lock for relation "tenk1_hundred" (OID 18431,
relkind i) @ TID (15,34)

This seems to be an existing issue.
For pg_restore_relation_stats, we don't have regress tests for index relation.
I am not sure the WARNING is ok.

This is not OK based on the rules introduced by Noah at aac2c9b4fde8.
check_lock_if_inplace_updateable_rel() expects a lock to be taken.
Note that elog() are used when reports should never be user-facing,
for states that should not be reachable.
--
Michael

#278jian he
jian.universality@gmail.com
In reply to: Corey Huinker (#272)
1 attachment(s)
Re: Statistics Import and Export

On Thu, Jan 16, 2025 at 4:56 AM Corey Huinker <corey.huinker@gmail.com> wrote:

I do like the idea of a "Statistics for ..." prefix, and I think it's
doable.

And that's now implemented.

The caller needs some knowledge about that anyway, to correctly output
the statistics dump when the schema is not requested. Tests should
cover those cases, too.

Tests for pg_dump --no-statistics and pg_dump --schema-only were added. Rebased to master as of today.

I'm not completely happy with this patch, as I had to comment out one check in pg_backup_archiver that seemed necessary, but perhaps another set of eyes will set me straight.

Attached is just the pg_dump stuff, and only for relation/attribute stats. The extended stats and vacuumdb work will be in their own threads going forward.

hi
The current v38 implementation allows statistics to be placed in
either SECTION_DATA or SECTION_POST_DATA.
IMHO, moving all statistics to the SECTION_POST_DATA section would
simplify things.
Attached is a patch that implements this change, (based on your patch,
obviously)

Reasoning making statistics in SECTION_POST_DATA are:
* statistics in multi sections will make the --section
handle statistics more harder for pg_dump and pg_restore.

* current doc in --schema-only says
"It is similar to, but for historical reasons not identical to, specifying
--section=pre-data --section=post-data.".
section span two sections making this sentence not less accurate.
Also, if we want to use --section option to dump all the statistics, we
need to use --data and --post-data together to get all the statistics.

* generally, --post-data section takes less time then --data section,
so putting it in SECTION_POST_DATA won't "cost" us that much.

* repairDependencyLoop, repairMatViewBoundaryMultiLoop is quite hard
to comprehend.
make statistics in SECTION_POST_DATA can make use not to think about
these changes.
also seems there is no materialized view statistics dump and restore
tests in src/bin/pg_dump/t/002_pg_dump.pl

* There are many REQ_DATA or REQ_SCHEMA occurrences, for each
occurrence, we may need to consider REQ_STATS.
make the statistics in SECTION_POST_DATA, then we don't need REQ_STATS.

what do you think?

This issue [1]/messages/by-id/Z22kX5x2IhNb8kHE@momjian.us even if pg_upgrade has options --with-statistics and --with-statistics. we still need a sentence to mention which option is default? seems not yet resolved.
[1]: /messages/by-id/Z22kX5x2IhNb8kHE@momjian.us even if pg_upgrade has options --with-statistics and --with-statistics. we still need a sentence to mention which option is default?
even if pg_upgrade has options --with-statistics and --with-statistics.
we still need a sentence to mention which option is default?

Attachments:

v38-0001-make-statistics-dumped-at-SECTION_POST_DATA.no-cfbotapplication/octet-stream; name=v38-0001-make-statistics-dumped-at-SECTION_POST_DATA.no-cfbotDownload
From aa78d861024b409bf63121dedf9dd955ad9dc734 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Mon, 20 Jan 2025 16:12:40 +0800
Subject: [PATCH v38 1/1] make statistics dumped at SECTION_POST_DATA

based on v38-0001-Enable-dumping-of-table-index-stats-in-pg_dump.patch
refactoring make statistics dump in the SECTION_POST_DATA.
---
 doc/src/sgml/ref/pg_dump.sgml        |  58 +++-
 doc/src/sgml/ref/pg_dumpall.sgml     |  38 +++
 doc/src/sgml/ref/pg_restore.sgml     |  51 +++-
 doc/src/sgml/ref/pgupgrade.sgml      |  18 ++
 src/bin/pg_dump/common.c             |   3 +
 src/bin/pg_dump/pg_backup.h          |   2 +
 src/bin/pg_dump/pg_backup_archiver.c |  22 +-
 src/bin/pg_dump/pg_dump.c            | 383 ++++++++++++++++++++++++++-
 src/bin/pg_dump/pg_dump.h            |   8 +
 src/bin/pg_dump/pg_dump_sort.c       |  11 +-
 src/bin/pg_dump/pg_dumpall.c         |   5 +
 src/bin/pg_dump/pg_restore.c         |  38 ++-
 src/bin/pg_dump/t/001_basic.pl       |  18 ++
 src/bin/pg_dump/t/002_pg_dump.pl     |  62 ++++-
 src/bin/pg_upgrade/dump.c            |   6 +-
 src/bin/pg_upgrade/option.c          |  12 +
 src/bin/pg_upgrade/pg_upgrade.h      |   1 +
 src/tools/pgindent/typedefs.list     |   1 +
 18 files changed, 705 insertions(+), 32 deletions(-)

diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index d66e901f51b..3f663915422 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -123,7 +123,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Dump only the data, not the schema (data definitions).
+        Dump only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are dumped.
        </para>
 
@@ -141,13 +141,11 @@ PostgreSQL documentation
       <listitem>
        <para>
         Include large objects in the dump.  This is the default behavior
-        except when <option>--schema</option>, <option>--table</option>, or
-        <option>--schema-only</option> is specified.  The <option>-b</option>
-        switch is therefore only useful to add large objects to dumps
-        where a specific schema or table has been requested.  Note that
-        large objects are considered data and therefore will be included when
-        <option>--data-only</option> is used, but not
-        when <option>--schema-only</option> is.
+        except when <option>--schema</option>, <option>--table</option>,
+        <option>--schema-only</option>, or <option>--statistics-only</option>, or
+        <option>--no-data</option> is specified.  The <option>-b</option>
+        switch is therefore only useful to add large objects to dumps where a
+        specific schema or table has been requested.
        </para>
       </listitem>
      </varlistentry>
@@ -516,10 +514,11 @@ PostgreSQL documentation
       <term><option>--schema-only</option></term>
       <listitem>
        <para>
-        Dump only the object definitions (schema), not data.
+        Dump only the object definitions (schema), not data or statistics.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive to <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
         <option>--section=pre-data --section=post-data</option>.
@@ -652,6 +651,18 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Dump only the statistics, not the schema (data definitions) or data.
+        Statistics for tables, materialized views, and indexes are dumped.
+       </para>
+
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-Z <replaceable class="parameter">level</replaceable></option></term>
       <term><option>-Z <replaceable class="parameter">method</replaceable></option>[:<replaceable>detail</replaceable>]</term>
@@ -1080,6 +1091,15 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-data</option></term>
+      <listitem>
+       <para>
+        Do not dump data.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-publications</option></term>
       <listitem>
@@ -1098,6 +1118,24 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-schema</option></term>
+      <listitem>
+       <para>
+        Do not dump schema (data definitions).
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_dumpall.sgml b/doc/src/sgml/ref/pg_dumpall.sgml
index 014f2792589..d423153a93a 100644
--- a/doc/src/sgml/ref/pg_dumpall.sgml
+++ b/doc/src/sgml/ref/pg_dumpall.sgml
@@ -265,6 +265,17 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Dump only the statistics, not the schema (data definitions) or data.
+        Statistics for tables, materialized views, and indexes are dumped.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--binary-upgrade</option></term>
       <listitem>
@@ -422,6 +433,15 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-data</option></term>
+      <listitem>
+       <para>
+        Do not dump data.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-publications</option></term>
       <listitem>
@@ -447,6 +467,15 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-schema</option></term>
+      <listitem>
+       <para>
+        Do not dump schema (data definitions).
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-security-labels</option></term>
       <listitem>
@@ -456,6 +485,15 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index b8b27e1719e..22c3c118add 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -94,7 +94,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Restore only the data, not the schema (data definitions).
+        Restore only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are restored,
         if present in the archive.
        </para>
@@ -483,10 +483,11 @@ PostgreSQL documentation
         to the extent that schema entries are present in the archive.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive of <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
-        <option>--section=pre-data --section=post-data</option>.
+        <option>--section=pre-data --section=post-data --no-statistics</option>.
        </para>
        <para>
         (Do not confuse this with the <option>--schema</option> option, which
@@ -599,6 +600,20 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Restore only the statistics, not schema (data definitions) or data.
+       </para>
+       <para>
+        (Do not confuse this with the <option>--schema</option> option, which
+        uses the word <quote>schema</quote> in a different meaning.)
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-1</option></term>
       <term><option>--single-transaction</option></term>
@@ -681,6 +696,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-data</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore data, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-data-for-failed-tables</option></term>
       <listitem>
@@ -713,6 +738,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-schema</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore schema (data definitions), even if
+        the archive contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-security-labels</option></term>
       <listitem>
@@ -723,6 +758,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore statistics, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pgupgrade.sgml b/doc/src/sgml/ref/pgupgrade.sgml
index 4777381dac2..64a1ebd613b 100644
--- a/doc/src/sgml/ref/pgupgrade.sgml
+++ b/doc/src/sgml/ref/pgupgrade.sgml
@@ -145,6 +145,24 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--with-statistics</option></term>
+      <listitem>
+       <para>
+        Restore statistics from the old cluster into the new cluster.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not restore statistics from the old cluster into the new cluster.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-o</option> <replaceable class="parameter">options</replaceable></term>
       <term><option>--old-options</option> <replaceable class="parameter">options</replaceable></term>
diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c
index 56b6c368acf..bc856492cc8 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -216,6 +216,9 @@ getSchemaData(Archive *fout, int *numTablesPtr)
 	pg_log_info("flagging indexes in partitioned tables");
 	flagInhIndexes(fout, tblinfo, numTables);
 
+	pg_log_info("reading table statistics");
+	getRelationStatistics(fout, tblinfo, numTables);
+
 	pg_log_info("reading extended statistics");
 	getExtendedStatistics(fout);
 
diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index f0f19bb0b29..350cf659c41 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -160,6 +160,7 @@ typedef struct _restoreOptions
 	/* flags derived from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } RestoreOptions;
 
 typedef struct _dumpOptions
@@ -208,6 +209,7 @@ typedef struct _dumpOptions
 	/* flags derived from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } DumpOptions;
 
 /*
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 707a3fc844c..b190d9731f6 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -46,7 +46,6 @@
 #define TEXT_DUMP_HEADER "--\n-- PostgreSQL database dump\n--\n\n"
 #define TEXT_DUMPALL_HEADER "--\n-- PostgreSQL database cluster dump\n--\n\n"
 
-
 static ArchiveHandle *_allocAH(const char *FileSpec, const ArchiveFormat fmt,
 							   const pg_compress_specification compression_spec,
 							   bool dosync, ArchiveMode mode,
@@ -149,6 +148,7 @@ InitDumpOptions(DumpOptions *opts)
 	opts->dumpSections = DUMP_UNSECTIONED;
 	opts->dumpSchema = true;
 	opts->dumpData = true;
+	opts->dumpStatistics = true;
 }
 
 /*
@@ -169,9 +169,10 @@ dumpOptionsFromRestoreOptions(RestoreOptions *ropt)
 	dopt->outputClean = ropt->dropSchema;
 	dopt->dumpData = ropt->dumpData;
 	dopt->dumpSchema = ropt->dumpSchema;
+	dopt->dumpSections = ropt->dumpSections;
+	dopt->dumpStatistics = ropt->dumpStatistics;
 	dopt->if_exists = ropt->if_exists;
 	dopt->column_inserts = ropt->column_inserts;
-	dopt->dumpSections = ropt->dumpSections;
 	dopt->aclsSkip = ropt->aclsSkip;
 	dopt->outputSuperuser = ropt->superuser;
 	dopt->outputCreateDB = ropt->createDB;
@@ -1084,6 +1085,7 @@ NewRestoreOptions(void)
 	opts->compression_spec.level = 0;
 	opts->dumpSchema = true;
 	opts->dumpData = true;
+	opts->dumpStatistics = true;
 
 	return opts;
 }
@@ -1093,6 +1095,7 @@ _disableTriggersIfNecessary(ArchiveHandle *AH, TocEntry *te)
 {
 	RestoreOptions *ropt = AH->public.ropt;
 
+	ahprintf(AH, "-- ALTER TABLE DISABLE TRIGGER ALL %d %d;   \n\n", ropt->dumpSchema, ropt->disable_triggers);
 	/* This hack is only needed in a data-only restore */
 	if (ropt->dumpSchema || !ropt->disable_triggers)
 		return;
@@ -2962,6 +2965,18 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/*
+	 * statistics is in the REQ_DATA section. we can choose to ignore it, but
+	 * not when we request dump statistics
+	 */
+	if (strcmp(te->desc, "STATISTICS DATA") == 0)
+	{
+		if (!ropt->dumpStatistics)
+			return 0;
+		else
+			return REQ_DATA;
+	}
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -3196,7 +3211,8 @@ _tocEntryRestorePass(TocEntry *te)
 		strcmp(te->desc, "DEFAULT ACL") == 0)
 		return RESTORE_PASS_ACL;
 	if (strcmp(te->desc, "EVENT TRIGGER") == 0 ||
-		strcmp(te->desc, "MATERIALIZED VIEW DATA") == 0)
+		strcmp(te->desc, "MATERIALIZED VIEW DATA") == 0 ||
+		strcmp(te->desc, "STATISTICS DATA") == 0 )
 		return RESTORE_PASS_POST_ACL;
 
 	/*
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 8f73a5df956..19a08f8d08d 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -430,7 +430,11 @@ main(int argc, char **argv)
 	DataDirSyncMethod sync_method = DATA_DIR_SYNC_METHOD_FSYNC;
 	bool		data_only = false;
 	bool		schema_only = false;
+	bool		statistics_only = false;
 
+	static int	no_data = 0;
+	static int	no_schema = 0;
+	static int  no_statistics = 0;
 	static DumpOptions dopt;
 
 	static struct option long_options[] = {
@@ -466,6 +470,7 @@ main(int argc, char **argv)
 		{"encoding", required_argument, NULL, 'E'},
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
+		{"statistics-only", no_argument, NULL, 'X'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -492,8 +497,11 @@ main(int argc, char **argv)
 		{"strict-names", no_argument, &strict_names, 1},
 		{"use-set-session-authorization", no_argument, &dopt.use_setsessauth, 1},
 		{"no-comments", no_argument, &dopt.no_comments, 1},
+		{"no-data", no_argument, &no_data, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
+		{"no-schema", no_argument, &no_schema, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -539,7 +547,7 @@ main(int argc, char **argv)
 
 	InitDumpOptions(&dopt);
 
-	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:RsS:t:T:U:vwWxZ:",
+	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:PRsS:t:T:U:vwWxXZ:",
 							long_options, &optindex)) != -1)
 	{
 		switch (c)
@@ -613,6 +621,10 @@ main(int argc, char **argv)
 				dopt.cparams.pgport = pg_strdup(optarg);
 				break;
 
+			case 'X':			/* Dump statistics only */
+				statistics_only = true;
+				break;
+
 			case 'R':
 				/* no-op, still accepted for backwards compatibility */
 				break;
@@ -784,6 +796,17 @@ main(int argc, char **argv)
 
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (schema_only && statistics_only)
+		pg_fatal("options -s/--schema-only and -X/--statistics-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
+
+	if (data_only && no_data)
+		pg_fatal("options -a/--data-only and --no-data cannot be used together");
+	if (schema_only && no_schema)
+		pg_fatal("options -s/--schema-only and --no-schema cannot be used together");
+	if (statistics_only && no_statistics)
+		pg_fatal("options -X/--statistics-only and --no-statistics cannot be used together");
 
 	if (schema_only && foreign_servers_include_patterns.head != NULL)
 		pg_fatal("options -s/--schema-only and --include-foreign-data cannot be used together");
@@ -798,8 +821,17 @@ main(int argc, char **argv)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
 	/* set derivative flags */
-	dopt.dumpSchema = (!data_only);
-	dopt.dumpData = (!schema_only);
+	dopt.dumpData = data_only || (!schema_only && !statistics_only);
+	dopt.dumpSchema = schema_only || (!data_only && !statistics_only);
+	dopt.dumpStatistics = statistics_only || (!data_only && !schema_only);
+
+	if (no_data)
+		dopt.dumpData = false;
+	if (no_schema)
+		dopt.dumpSchema = false;
+	if (no_statistics ||
+		(dopt.dumpSections && (!(dopt.dumpSections & DUMP_POST_DATA))))
+		dopt.dumpStatistics = false;
 
 	/*
 	 * --inserts are already implied above if --column-inserts or
@@ -1099,6 +1131,7 @@ main(int argc, char **argv)
 	ropt->dropSchema = dopt.outputClean;
 	ropt->dumpData = dopt.dumpData;
 	ropt->dumpSchema = dopt.dumpSchema;
+	ropt->dumpStatistics = dopt.dumpStatistics;
 	ropt->if_exists = dopt.if_exists;
 	ropt->column_inserts = dopt.column_inserts;
 	ropt->dumpSections = dopt.dumpSections;
@@ -1177,7 +1210,7 @@ help(const char *progname)
 	printf(_("  -?, --help                   show this help, then exit\n"));
 
 	printf(_("\nOptions controlling the output content:\n"));
-	printf(_("  -a, --data-only              dump only the data, not the schema\n"));
+	printf(_("  -a, --data-only              dump only the data, not the schema or statistics\n"));
 	printf(_("  -b, --large-objects          include large objects in dump\n"));
 	printf(_("  --blobs                      (same as --large-objects, deprecated)\n"));
 	printf(_("  -B, --no-large-objects       exclude large objects in dump\n"));
@@ -1190,11 +1223,12 @@ help(const char *progname)
 	printf(_("  -N, --exclude-schema=PATTERN do NOT dump the specified schema(s)\n"));
 	printf(_("  -O, --no-owner               skip restoration of object ownership in\n"
 			 "                               plain-text format\n"));
-	printf(_("  -s, --schema-only            dump only the schema, no data\n"));
+	printf(_("  -s, --schema-only            dump only the schema, no data or statistics\n"));
 	printf(_("  -S, --superuser=NAME         superuser user name to use in plain-text format\n"));
 	printf(_("  -t, --table=PATTERN          dump only the specified table(s)\n"));
 	printf(_("  -T, --exclude-table=PATTERN  do NOT dump the specified table(s)\n"));
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
+	printf(_("  -X, --statistics-only        dump only the statistics, not schema or data\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
@@ -1219,8 +1253,11 @@ help(const char *progname)
 	printf(_("  --inserts                    dump data as INSERT commands, rather than COPY\n"));
 	printf(_("  --load-via-partition-root    load partitions via the root table\n"));
 	printf(_("  --no-comments                do not dump comment commands\n"));
+	printf(_("  --no-data                    do not dump data\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
+	printf(_("  --no-schema                  do not dump schema\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -6777,6 +6814,85 @@ getFuncs(Archive *fout)
 	destroyPQExpBuffer(query);
 }
 
+/*
+ * getRelationStatistics
+ *	  get information about all statistics on dumpable relations.
+ */
+void
+getRelationStatistics(Archive *fout, TableInfo tblinfo[], int numTables)
+{
+	int		i = 0;
+	int		j = 0;
+	int		tbl_cnt = 0;
+	int		index_cnt = 0;
+	RelStatsInfo *rel_stats_info = NULL;
+	TableInfo	*tbinfo = NULL;
+	IndxInfo	*indxinfo = NULL;
+	Oid		   *tabldids = NULL;
+
+	if (!fout->dopt->dumpStatistics)
+		return;
+
+	tabldids = pg_malloc0(numTables * sizeof(Oid));
+	for (i = 0; i < numTables; i++)
+	{
+		tbinfo = &tblinfo[i];
+		/*
+		 * we only dump statistics related to regular relations, partitioned
+		 * tables, materialized views
+		*/
+		if (!(tbinfo->relkind == RELKIND_RELATION ||
+			  tbinfo->relkind == RELKIND_MATVIEW ||
+			  tbinfo->relkind == RELKIND_PARTITIONED_TABLE))
+			continue;
+
+		if (!tbinfo->interesting)
+			continue;
+
+		tabldids[tbl_cnt] = tbinfo->dobj.catId.oid;
+		index_cnt = index_cnt + tbinfo->numIndexes;
+		tbl_cnt++;
+	}
+
+	if(tbl_cnt == 0)
+	{
+		free(tabldids);
+		return;
+	}
+
+	/*
+	 * since we dump indexes statistics, we need iterate over TableInfo->indexes
+	 */
+	rel_stats_info = pg_malloc((tbl_cnt + index_cnt) * sizeof(RelStatsInfo));
+	for (i = 0; i < tbl_cnt; i++)
+	{
+		tbinfo = findTableByOid(tabldids[i]);
+		rel_stats_info[j].dobj.objType = DO_REL_STATS;
+		rel_stats_info[j].dobj.catId.tableoid = 0;
+		rel_stats_info[j].dobj.catId.oid = tbinfo->dobj.catId.oid;
+		AssignDumpId(&rel_stats_info[j].dobj);
+		rel_stats_info[j].dobj.namespace = tbinfo->dobj.namespace;
+		rel_stats_info[j].dobj.name = pg_strdup(tbinfo->dobj.name);
+		rel_stats_info[j].stat_relation = tbinfo;
+
+		for (int k = 0; k < tbinfo->numIndexes; k++)
+		{
+			j++;
+			indxinfo = (&(tbinfo->indexes)[k]);
+			rel_stats_info[j].dobj.objType = DO_REL_STATS;
+			rel_stats_info[j].dobj.catId.tableoid = 0;
+			rel_stats_info[j].dobj.catId.oid = indxinfo->dobj.catId.oid;
+			AssignDumpId(&rel_stats_info[j].dobj);
+			rel_stats_info[j].dobj.namespace = indxinfo->dobj.namespace;
+			rel_stats_info[j].dobj.name = pg_strdup(indxinfo->dobj.name);
+			rel_stats_info[j].stat_relation = tbinfo;
+		}
+		j++;
+	}
+	Assert(j == tbl_cnt + index_cnt);
+	free(tabldids);
+}
+
 /*
  * getTables
  *	  read all the tables (no indexes) in the system catalogs,
@@ -10296,6 +10412,259 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * Tabular description of the parameters to pg_restore_relation_stats()
+ * param_name, param_type
+ */
+static const char *rel_stats_arginfo[][2] = {
+	{"relation", "regclass"},
+	{"version", "integer"},
+	{"relpages", "integer"},
+	{"reltuples", "real"},
+	{"relallvisible", "integer"},
+};
+
+/*
+ * Tabular description of the parameters to pg_restore_attribute_stats()
+ * param_name, param_type
+ */
+static const char *att_stats_arginfo[][2] = {
+	{"relation", "regclass"},
+	{"attname", "name"},
+	{"inherited", "boolean"},
+	{"version", "integer"},
+	{"null_frac", "float4"},
+	{"avg_width", "integer"},
+	{"n_distinct", "float4"},
+	{"most_common_vals", "text"},
+	{"most_common_freqs", "float4[]"},
+	{"histogram_bounds", "text"},
+	{"correlation", "float4"},
+	{"most_common_elems", "text"},
+	{"most_common_elem_freqs", "float4[]"},
+	{"elem_count_histogram", "float4[]"},
+	{"range_length_histogram", "text"},
+	{"range_empty_frac", "float4"},
+	{"range_bounds_histogram", "text"},
+};
+
+/*
+ * getRelStatsExportQuery --
+ *
+ * Generate a query that will fetch all relation (e.g. pg_class)
+ * stats for a given relation.
+ */
+static void
+getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "current_setting('server_version_num') AS version, "
+						 "c.relpages, c.reltuples, c.relallvisible "
+						 "FROM pg_class c "
+						 "JOIN pg_namespace n "
+						 "ON n.oid = c.relnamespace "
+						 "WHERE n.nspname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND c.relname = ");
+	appendStringLiteralAH(query, relname, fout);
+}
+
+/*
+ * getAttStatsExportQuery --
+ *
+ * Generate a query that will fetch all attribute (e.g. pg_statistic)
+ * stats for a given relation.
+ */
+static void
+getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "s.attname,"
+						 "s.inherited,"
+						 "current_setting('server_version_num') AS version, "
+						 "s.null_frac,"
+						 "s.avg_width,"
+						 "s.n_distinct,"
+						 "s.most_common_vals,"
+						 "s.most_common_freqs,"
+						 "s.histogram_bounds,"
+						 "s.correlation,"
+						 "s.most_common_elems,"
+						 "s.most_common_elem_freqs,"
+						 "s.elem_count_histogram,");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 "s.range_length_histogram,"
+							 "s.range_empty_frac,"
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(query,
+							 "NULL AS range_length_histogram,"
+							 "NULL AS range_empty_frac,"
+							 "NULL AS range_bounds_histogram ");
+
+	appendPQExpBufferStr(query,
+						 "FROM pg_stats s "
+						 "JOIN pg_namespace n "
+						 "ON n.nspname = s.schemaname "
+						 "JOIN pg_class c "
+						 "ON c.relname = s.tablename "
+						 "AND c.relnamespace = n.oid "
+						 "WHERE s.schemaname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND s.tablename = ");
+	appendStringLiteralAH(query, relname, fout);
+	appendPQExpBufferStr(query, " ORDER BY s.attname, s.inherited");
+}
+
+
+/*
+ * appendNamedArgument --
+ *
+ * Convenience routine for constructing parameters of the form:
+ * 'paraname', 'value'::type
+ */
+static void
+appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
+					const char *argval, const char *argtype)
+{
+	appendPQExpBufferStr(out, "\t");
+
+	appendStringLiteralAH(out, argname, fout);
+	appendPQExpBufferStr(out, ", ");
+
+	appendStringLiteralAH(out, argval, fout);
+	appendPQExpBuffer(out, "::%s", argtype);
+}
+
+/*
+ * appendRelStatsImport --
+ *
+ * Append a formatted pg_restore_relation_stats statement.
+ */
+static void
+appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	const char *sep = "";
+
+	if (PQntuples(res) == 0)
+		return;
+
+	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
+
+	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
+	{
+		const char *argname = rel_stats_arginfo[argno][0];
+		const char *argtype = rel_stats_arginfo[argno][1];
+		int			fieldno = PQfnumber(res, argname);
+
+		if (fieldno < 0)
+			pg_fatal("relation stats export query missing field '%s'",
+					 argname);
+
+		if (PQgetisnull(res, 0, fieldno))
+			continue;
+
+		appendPQExpBufferStr(out, sep);
+		appendNamedArgument(out, fout, argname, PQgetvalue(res, 0, fieldno), argtype);
+
+		sep = ",\n";
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+/*
+ * appendAttStatsImport --
+ *
+ * Append a series of formatted pg_restore_attribute_stats statements.
+ */
+static void
+appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	{
+		const char *sep = "";
+
+		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+		for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
+		{
+			const char *argname = att_stats_arginfo[argno][0];
+			const char *argtype = att_stats_arginfo[argno][1];
+			int			fieldno = PQfnumber(res, argname);
+
+			if (fieldno < 0)
+				pg_fatal("attribute stats export query missing field '%s'",
+						 argname);
+
+			if (PQgetisnull(res, rownum, fieldno))
+				continue;
+
+			appendPQExpBufferStr(out, sep);
+			appendNamedArgument(out, fout, argname, PQgetvalue(res, rownum, fieldno), argtype);
+			sep = ",\n";
+		}
+		appendPQExpBufferStr(out, "\n);\n");
+	}
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	PGresult	   *res;
+	PQExpBuffer		query;
+	PQExpBuffer		out;
+	PQExpBuffer		tag;
+	DumpableObject *dobj = (DumpableObject *) &rsinfo->dobj;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", "STATISTICS DATA", fmtId(dobj->name));
+
+	query = createPQExpBuffer();
+	out = createPQExpBuffer();
+
+	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendRelStatsImport(out, fout, res);
+	PQclear(res);
+
+	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendAttStatsImport(out, fout, res);
+	PQclear(res);
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATISTICS DATA",
+							  .section = SECTION_POST_DATA,
+							  .owner = rsinfo->stat_relation->rolname,
+							  .createStmt = out->data,
+							  .deps = dobj->dependencies,
+							  .nDeps = dobj->nDeps));
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(out);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -10744,6 +11113,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_SUBSCRIPTION_REL:
 			dumpSubscriptionTable(fout, (const SubRelInfo *) dobj);
 			break;
+		case DO_REL_STATS:
+			dumpRelationStats(fout, (const RelStatsInfo *) dobj);
+			break;
 		case DO_PRE_DATA_BOUNDARY:
 		case DO_POST_DATA_BOUNDARY:
 			/* never dumped, nothing to do */
@@ -18949,6 +19321,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_PUBLICATION_TABLE_IN_SCHEMA:
 			case DO_SUBSCRIPTION:
 			case DO_SUBSCRIPTION_REL:
+			case DO_REL_STATS:
 				/* Post-data objects: must come after the post-data boundary */
 				addObjectDependency(dobj, postDataBound->dumpId);
 				break;
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index f62b564ed1b..09d4970ecd2 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -84,6 +84,7 @@ typedef enum
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION,
 	DO_SUBSCRIPTION_REL,		/* see note for SubRelInfo */
+	DO_REL_STATS,
 } DumpableObjectType;
 
 /*
@@ -429,6 +430,12 @@ typedef struct _indexAttachInfo
 	IndxInfo   *partitionIdx;	/* link to index on partition */
 } IndexAttachInfo;
 
+typedef struct _relStatsInfo
+{
+	DumpableObject	dobj;
+	TableInfo  		*stat_relation; /* link to relation the stat is for */
+} RelStatsInfo;
+
 typedef struct _statsExtInfo
 {
 	DumpableObject dobj;
@@ -763,6 +770,7 @@ extern InhInfo *getInherits(Archive *fout, int *numInherits);
 extern void getPartitioningInfo(Archive *fout);
 extern void getIndexes(Archive *fout, TableInfo tblinfo[], int numTables);
 extern void getExtendedStatistics(Archive *fout);
+extern void getRelationStatistics(Archive *fout, TableInfo tblinfo[], int numTables);
 extern void getConstraints(Archive *fout, TableInfo tblinfo[], int numTables);
 extern void getRules(Archive *fout);
 extern void getTriggers(Archive *fout, TableInfo tblinfo[], int numTables);
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index dc9a28924bd..76f02db5e04 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -97,7 +97,8 @@ enum dbObjectTypePriorities
 	PRIO_SUBSCRIPTION_REL,
 	PRIO_DEFAULT_ACL,			/* done in ACL pass */
 	PRIO_EVENT_TRIGGER,			/* must be next to last! */
-	PRIO_REFRESH_MATVIEW		/* must be last! */
+	PRIO_REFRESH_MATVIEW,		/* must be last! */
+	PRIO_RELSTAT,				/* must really be last! */
 };
 
 /* This table is indexed by enum DumpableObjectType */
@@ -144,6 +145,7 @@ static const int dbObjectTypePriority[] =
 	[DO_POST_DATA_BOUNDARY] = PRIO_POST_DATA_BOUNDARY,
 	[DO_EVENT_TRIGGER] = PRIO_EVENT_TRIGGER,
 	[DO_REFRESH_MATVIEW] = PRIO_REFRESH_MATVIEW,
+	[DO_REL_STATS] = PRIO_RELSTAT,
 	[DO_POLICY] = PRIO_POLICY,
 	[DO_PUBLICATION] = PRIO_PUBLICATION,
 	[DO_PUBLICATION_REL] = PRIO_PUBLICATION_REL,
@@ -152,7 +154,7 @@ static const int dbObjectTypePriority[] =
 	[DO_SUBSCRIPTION_REL] = PRIO_SUBSCRIPTION_REL,
 };
 
-StaticAssertDecl(lengthof(dbObjectTypePriority) == (DO_SUBSCRIPTION_REL + 1),
+StaticAssertDecl(lengthof(dbObjectTypePriority) == (DO_REL_STATS + 1),
 				 "array length mismatch");
 
 static DumpId preDataBoundId;
@@ -1500,6 +1502,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "POST-DATA BOUNDARY  (ID %d)",
 					 obj->dumpId);
 			return;
+		case DO_REL_STATS:
+			snprintf(buf, bufsize,
+					 "RELATION STATISTICS FOR %s  (ID %d OID %u)",
+					 obj->name, obj->dumpId, obj->catId.oid);
+			return;
 	}
 	/* shouldn't get here */
 	snprintf(buf, bufsize,
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 396f79781c5..7effb704905 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -103,6 +103,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -172,6 +173,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -451,6 +453,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -666,6 +670,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 88ae39d938a..2cc7d3eeb08 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -63,6 +63,9 @@ main(int argc, char **argv)
 	int			numWorkers = 1;
 	Archive    *AH;
 	char	   *inputFileSpec;
+	bool		data_only = false;
+	bool		schema_only = false;
+	bool		statistics_only = false;
 	static int	disable_triggers = 0;
 	static int	enable_row_security = 0;
 	static int	if_exists = 0;
@@ -71,12 +74,13 @@ main(int argc, char **argv)
 	static int	outputNoTablespaces = 0;
 	static int	use_setsessauth = 0;
 	static int	no_comments = 0;
+	static int	no_data = 0;
 	static int	no_publications = 0;
+	static int	no_schema = 0;
 	static int	no_security_labels = 0;
+	static int  no_statistics = 0;
 	static int	no_subscriptions = 0;
 	static int	strict_names = 0;
-	bool		data_only = false;
-	bool		schema_only = false;
 
 	struct option cmdopts[] = {
 		{"clean", 0, NULL, 'c'},
@@ -108,6 +112,7 @@ main(int argc, char **argv)
 		{"username", 1, NULL, 'U'},
 		{"verbose", 0, NULL, 'v'},
 		{"single-transaction", 0, NULL, '1'},
+		{"statistics-only", no_argument, NULL, 'X'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -124,9 +129,12 @@ main(int argc, char **argv)
 		{"transaction-size", required_argument, NULL, 5},
 		{"use-set-session-authorization", no_argument, &use_setsessauth, 1},
 		{"no-comments", no_argument, &no_comments, 1},
+		{"no-data", no_argument, &no_data, 1},
 		{"no-publications", no_argument, &no_publications, 1},
+		{"no-schema", no_argument, &no_schema, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"filter", required_argument, NULL, 4},
 
 		{NULL, 0, NULL, 0}
@@ -271,6 +279,10 @@ main(int argc, char **argv)
 				opts->aclsSkip = 1;
 				break;
 
+			case 'X':			/* Restore statistics only */
+				statistics_only = true;
+				break;
+
 			case '1':			/* Restore data in a single transaction */
 				opts->single_txn = true;
 				opts->exit_on_error = true;
@@ -343,6 +355,10 @@ main(int argc, char **argv)
 
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
+	if (schema_only && statistics_only)
+		pg_fatal("options -s/--schema-only and -X/--statistics-only cannot be used together");
 
 	if (data_only && opts->dropSchema)
 		pg_fatal("options -c/--clean and -a/--data-only cannot be used together");
@@ -362,8 +378,17 @@ main(int argc, char **argv)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
 	/* set derivative flags */
-	opts->dumpSchema = (!data_only);
-	opts->dumpData = (!schema_only);
+	opts->dumpData = data_only || (!schema_only && !statistics_only);
+	opts->dumpSchema = schema_only || (!data_only && !statistics_only);
+	opts->dumpStatistics = statistics_only || (!data_only && !schema_only);
+
+	if (no_data)
+		opts->dumpData = false;
+	if (no_schema)
+		opts->dumpSchema = false;
+	if (no_statistics ||
+		(opts->dumpSections && (!(opts->dumpSections & DUMP_POST_DATA))))
+		opts->dumpStatistics = false;
 
 	opts->disable_triggers = disable_triggers;
 	opts->enable_row_security = enable_row_security;
@@ -484,6 +509,7 @@ usage(const char *progname)
 	printf(_("  -t, --table=NAME             restore named relation (table, view, etc.)\n"));
 	printf(_("  -T, --trigger=NAME           restore named trigger\n"));
 	printf(_("  -x, --no-privileges          skip restoration of access privileges (grant/revoke)\n"));
+	printf(_("  -X, --statistics-only        restore only the statistics, not schema or data\n"));
 	printf(_("  -1, --single-transaction     restore as a single transaction\n"));
 	printf(_("  --disable-triggers           disable triggers during data-only restore\n"));
 	printf(_("  --enable-row-security        enable row security\n"));
@@ -491,10 +517,14 @@ usage(const char *progname)
 			 "                               in FILENAME\n"));
 	printf(_("  --if-exists                  use IF EXISTS when dropping objects\n"));
 	printf(_("  --no-comments                do not restore comment commands\n"));
+	printf(_("  --no-data                    do not restore data\n"));
 	printf(_("  --no-data-for-failed-tables  do not restore data of tables that could not be\n"
 			 "                               created\n"));
 	printf(_("  --no-publications            do not restore publications\n"));
+	printf(_("  --no-schema                  do not restore schema\n"));
 	printf(_("  --no-security-labels         do not restore security labels\n"));
+	printf(_("  --no-statistics              do not restore statistics\n"));
+	/* This hack is only needed in a data-only restore */
 	printf(_("  --no-subscriptions           do not restore subscriptions\n"));
 	printf(_("  --no-table-access-method     do not restore table access methods\n"));
 	printf(_("  --no-tablespaces             do not restore tablespace assignments\n"));
diff --git a/src/bin/pg_dump/t/001_basic.pl b/src/bin/pg_dump/t/001_basic.pl
index 214240f1ae5..f29da06ed28 100644
--- a/src/bin/pg_dump/t/001_basic.pl
+++ b/src/bin/pg_dump/t/001_basic.pl
@@ -50,12 +50,30 @@ command_fails_like(
 	'pg_dump: options -s/--schema-only and -a/--data-only cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '-s', '-X' ],
+	qr/\Qpg_dump: error: options -s\/--schema-only and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: error: options -s/--schema-only and -X/--statistics-only cannot be used together'
+);
+
+command_fails_like(
+	[ 'pg_dump', '-a', '-X' ],
+	qr/\Qpg_dump: error: options -a\/--data-only and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: error: options -a/--data-only and -X/--statistics-only cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-s', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: options -s\/--schema-only and --include-foreign-data cannot be used together\E/,
 	'pg_dump: options -s/--schema-only and --include-foreign-data cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '--statistics-only', '--no-statistics' ],
+	qr/\Qpg_dump: error: options -X\/--statistics-only and --no-statistics cannot be used together\E/,
+	'pg_dump: options -X\/--statistics-only and --no-statistics cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-j2', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: option --include-foreign-data is not supported with parallel backup\E/,
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index bf65d44b942..da8b6e85f3e 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -66,7 +66,7 @@ my %pgdump_runs = (
 			'--format=custom',
 			"--file=$tempdir/binary_upgrade.dump",
 			'-w',
-			'--schema-only',
+			'--no-data',
 			'--binary-upgrade',
 			'-d', 'postgres',    # alternative way to specify database
 		],
@@ -645,7 +645,19 @@ my %pgdump_runs = (
 
 			'--schema=dump_test', '-b', '-B', '--no-sync', 'postgres',
 		],
-	},);
+	},
+	no_statistics => {
+		dump_cmd => [
+			'pg_dump', "--file=$tempdir/no_statistics.sql",
+			'--no-sync', '--no-statistics', 'postgres',
+		],
+	},
+	no_schema => {
+		dump_cmd => [
+			'pg_dump', "--file=$tempdir/no_schema.sql",
+			'--no-sync', '--no-schema', 'postgres',
+		],
+	});
 
 ###############################################################
 # Definition of the tests to run.
@@ -711,6 +723,7 @@ my %full_runs = (
 	no_large_objects => 1,
 	no_owner => 1,
 	no_privs => 1,
+	no_statistics => 1,
 	no_table_access_method => 1,
 	pg_dumpall_dbprivs => 1,
 	pg_dumpall_exclude => 1,
@@ -912,6 +925,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1325,6 +1339,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1346,6 +1361,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1367,6 +1383,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1533,6 +1550,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1686,6 +1704,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			only_dump_test_table => 1,
 			section_data => 1,
 		},
@@ -1713,6 +1732,7 @@ my %tests = (
 			data_only => 1,
 			exclude_test_table => 1,
 			exclude_test_table_data => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1733,7 +1753,10 @@ my %tests = (
 			\QCOPY dump_test.fk_reference_test_table (col1) FROM stdin;\E
 			\n(?:\d\n){5}\\\.\n
 			/xms,
-		like => { data_only => 1, },
+		like => {
+			data_only => 1,
+			no_schema => 1,
+		},
 	},
 
 	'COPY test_second_table' => {
@@ -1749,6 +1772,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1771,6 +1795,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1794,6 +1819,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1816,6 +1842,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1838,6 +1865,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -3234,6 +3262,7 @@ my %tests = (
 		like => {
 			%full_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 			only_dump_test_schema => 1,
 			test_schema_plus_large_objects => 1,
@@ -3404,6 +3433,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			only_dump_measurement => 1,
 			section_data => 1,
 			only_dump_test_schema => 1,
@@ -4286,6 +4316,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 			binary_upgrade => 1,
@@ -4586,6 +4617,31 @@ my %tests = (
 		},
 	},
 
+	#
+	# Table statistics should go in section=data.
+	# Materialized view statistics should go in section=post-data.
+	#
+	'statistics_import' => {
+	create_sql => '
+		CREATE TABLE dump_test.has_stats
+		AS SELECT g.g AS x, g.g / 2 AS y FROM generate_series(1,100) AS g(g);
+		CREATE TABLE dump_test.has_stats_mv AS SELECT * FROM dump_test.has_stats;
+		ANALYZE dump_test.has_stats, dump_test.has_stats_mv;',
+	regexp => qr/pg_catalog.pg_restore_attribute_stats/,
+	like => {
+		%full_runs,
+		%dump_test_schema_runs,
+		section_post_data => 1,
+		no_schema => 1,
+		},
+	unlike => {
+		exclude_dump_test_schema => 1,
+		no_statistics => 1,
+		only_dump_measurement => 1,
+		schema_only => 1,
+		},
+	},
+
 	# CREATE TABLE with partitioned table and various AMs.  One
 	# partition uses the same default as the parent, and a second
 	# uses its own AM.
diff --git a/src/bin/pg_upgrade/dump.c b/src/bin/pg_upgrade/dump.c
index 8ce0fa3020e..a29cd2cca98 100644
--- a/src/bin/pg_upgrade/dump.c
+++ b/src/bin/pg_upgrade/dump.c
@@ -21,10 +21,11 @@ generate_old_dump(void)
 
 	/* run new pg_dumpall binary for globals */
 	exec_prog(UTILITY_LOG_FILE, NULL, true, true,
-			  "\"%s/pg_dumpall\" %s --globals-only --quote-all-identifiers "
+			  "\"%s/pg_dumpall\" %s %s --globals-only --quote-all-identifiers "
 			  "--binary-upgrade %s --no-sync -f \"%s/%s\"",
 			  new_cluster.bindir, cluster_conn_opts(&old_cluster),
 			  log_opts.verbose ? "--verbose" : "",
+			  user_opts.do_statistics ? "" : "--no-statistics",
 			  log_opts.dumpdir,
 			  GLOBALS_DUMP_FILE);
 	check_ok();
@@ -52,10 +53,11 @@ generate_old_dump(void)
 		snprintf(log_file_name, sizeof(log_file_name), DB_DUMP_LOG_FILE_MASK, old_db->db_oid);
 
 		parallel_exec_prog(log_file_name, NULL,
-						   "\"%s/pg_dump\" %s --schema-only --quote-all-identifiers "
+						   "\"%s/pg_dump\" %s --no-data %s --quote-all-identifiers "
 						   "--binary-upgrade --format=custom %s --no-sync --file=\"%s/%s\" %s",
 						   new_cluster.bindir, cluster_conn_opts(&old_cluster),
 						   log_opts.verbose ? "--verbose" : "",
+						   user_opts.do_statistics ? "" : "--no-statistics",
 						   log_opts.dumpdir,
 						   sql_file_name, escaped_connstr.data);
 
diff --git a/src/bin/pg_upgrade/option.c b/src/bin/pg_upgrade/option.c
index 108eb7a1ba4..3b6c7ec994e 100644
--- a/src/bin/pg_upgrade/option.c
+++ b/src/bin/pg_upgrade/option.c
@@ -60,6 +60,8 @@ parseCommandLine(int argc, char *argv[])
 		{"copy", no_argument, NULL, 2},
 		{"copy-file-range", no_argument, NULL, 3},
 		{"sync-method", required_argument, NULL, 4},
+		{"with-statistics", no_argument, NULL, 5},
+		{"no-statistics", no_argument, NULL, 6},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -70,6 +72,7 @@ parseCommandLine(int argc, char *argv[])
 
 	user_opts.do_sync = true;
 	user_opts.transfer_mode = TRANSFER_MODE_COPY;
+	user_opts.do_statistics = true;
 
 	os_info.progname = get_progname(argv[0]);
 
@@ -212,6 +215,13 @@ parseCommandLine(int argc, char *argv[])
 				user_opts.sync_method = pg_strdup(optarg);
 				break;
 
+			case 5:
+				user_opts.do_statistics = true;
+				break;
+			case 6:
+				user_opts.do_statistics = false;
+				break;
+
 			default:
 				fprintf(stderr, _("Try \"%s --help\" for more information.\n"),
 						os_info.progname);
@@ -306,7 +316,9 @@ usage(void)
 	printf(_("  --clone                       clone instead of copying files to new cluster\n"));
 	printf(_("  --copy                        copy files to new cluster (default)\n"));
 	printf(_("  --copy-file-range             copy files to new cluster with copy_file_range\n"));
+	printf(_("  --no-statistics               do not import statistics from old cluster\n"));
 	printf(_("  --sync-method=METHOD          set method for syncing files to disk\n"));
+	printf(_("  --with-statistics             import statistics from old cluster (default)\n"));
 	printf(_("  -?, --help                    show this help, then exit\n"));
 	printf(_("\n"
 			 "Before running pg_upgrade you must:\n"
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index 0cdd675e4f1..3fe111fbde5 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -327,6 +327,7 @@ typedef struct
 	int			jobs;			/* number of processes/threads to use */
 	char	   *socketdir;		/* directory to use for Unix sockets */
 	char	   *sync_method;
+	bool		do_statistics;	/* carry over statistics from old cluster */
 } UserOpts;
 
 typedef struct
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 668bddbfcd7..8a0968aaa7b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2398,6 +2398,7 @@ RelMapFile
 RelMapping
 RelOptInfo
 RelOptKind
+RelStatsInfo
 RelToCheck
 RelToCluster
 RelabelType
-- 
2.34.1

#279Corey Huinker
corey.huinker@gmail.com
In reply to: jian he (#278)
Re: Statistics Import and Export

On Mon, Jan 20, 2025 at 3:22 AM jian he <jian.universality@gmail.com> wrote:

On Thu, Jan 16, 2025 at 4:56 AM Corey Huinker <corey.huinker@gmail.com>
wrote:

I do like the idea of a "Statistics for ..." prefix, and I think it's
doable.

And that's now implemented.

The caller needs some knowledge about that anyway, to correctly output
the statistics dump when the schema is not requested. Tests should
cover those cases, too.

Tests for pg_dump --no-statistics and pg_dump --schema-only were added.

Rebased to master as of today.

I'm not completely happy with this patch, as I had to comment out one

check in pg_backup_archiver that seemed necessary, but perhaps another set
of eyes will set me straight.

Attached is just the pg_dump stuff, and only for relation/attribute

stats. The extended stats and vacuumdb work will be in their own threads
going forward.

hi
The current v38 implementation allows statistics to be placed in
either SECTION_DATA or SECTION_POST_DATA.
IMHO, moving all statistics to the SECTION_POST_DATA section would
simplify things.
Attached is a patch that implements this change, (based on your patch,
obviously)

That is where all statistics were previously. Others felt very strongly
that they should be mixed in to SECTION_DATA and SECTION_POST_DATA.

* repairDependencyLoop, repairMatViewBoundaryMultiLoop is quite hard

to comprehend.

I don't disagree.

* There are many REQ_DATA or REQ_SCHEMA occurrences, for each
occurrence, we may need to consider REQ_STATS.

That is already in the works. Hoping to get that patch out soon.

#280Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#279)
Re: Statistics Import and Export

On Mon, 2025-01-20 at 10:58 -0500, Corey Huinker wrote:

That is where all statistics were previously. Others felt very
strongly that they should be mixed in to SECTION_DATA and
SECTION_POST_DATA.

I believe you are referring to Tom's statement that "it'll be a
serious, serious error for [stats] not to be SECTION_DATA". The
statement is somewhat softened by the sentence that follows, and
slightly more by [2]/messages/by-id/3156140.1713817153@sss.pgh.pa.us. But it's pretty clear that SECTION_POST_DATA is,
at best, an implementation comprosmise.

The reason we need to put some stats in SECTION_POST_DATA is because of
the hack to resolve MVs that depend on primary keys by moving the MV
into SECTION_POST_DATA. (An MV can depend on a primary key when the
query has a GROUP BY that relies on functional dependencies to be
valid.) That's a fairly marginal case, and one we might be able to
resolve a better way in the future, so I don't think that should drive
the design.

Reagrding [2]/messages/by-id/3156140.1713817153@sss.pgh.pa.us and [3]/messages/by-id/3228677.1713844341@sss.pgh.pa.us, we might need to reconsider the behavior of the
--data-only option. I asked for the v38 behavior out of a sense of
consistency and completeness (the ability to express whatever
combination of things the user might want). But re-reading those
messages, we might want --data-only to include the stats?

Regards,
Jeff Davis

[1]: /messages/by-id/1798867.1712376328@sss.pgh.pa.us
/messages/by-id/1798867.1712376328@sss.pgh.pa.us
[2]: /messages/by-id/3156140.1713817153@sss.pgh.pa.us
/messages/by-id/3156140.1713817153@sss.pgh.pa.us
[3]: /messages/by-id/3228677.1713844341@sss.pgh.pa.us
/messages/by-id/3228677.1713844341@sss.pgh.pa.us

#281Corey Huinker
corey.huinker@gmail.com
In reply to: Michael Paquier (#277)
2 attachment(s)
Re: Statistics Import and Export

On Sat, Jan 18, 2025 at 7:45 PM Michael Paquier <michael@paquier.xyz> wrote:

On Sun, Jan 19, 2025 at 01:00:04AM +0800, jian he wrote:

On Fri, Jan 17, 2025 at 10:20 PM jian he <jian.universality@gmail.com>

wrote:

dump and execute the above query generated a warning
WARNING: missing lock for relation "tenk1_hundred" (OID 18431,
relkind i) @ TID (15,34)

This seems to be an existing issue.
For pg_restore_relation_stats, we don't have regress tests for index

relation.

I am not sure the WARNING is ok.

This is not OK based on the rules introduced by Noah at aac2c9b4fde8.
check_lock_if_inplace_updateable_rel() expects a lock to be taken.
Note that elog() are used when reports should never be user-facing,
for states that should not be reachable.
--
Michael

Here's a patch that

Attached is the patch, along with the regression test output prior to the
change to stat_utils.c.

Other pg_dump work has been left out of v39 to focus on this.

Attachments:

regression.diffsapplication/octet-stream; name=regression.diffsDownload
diff -U3 /home/corey/src/postgres/src/test/regress/expected/stats_import.out /home/corey/src/postgres/build/testrun/regress/regress/results/stats_import.out
--- /home/corey/src/postgres/src/test/regress/expected/stats_import.out	2025-01-20 15:35:44.146761466 -0500
+++ /home/corey/src/postgres/build/testrun/regress/regress/results/stats_import.out	2025-01-20 15:36:22.528466475 -0500
@@ -1422,6 +1422,7 @@
     'reltuples', '10000'::real,
     'relallvisible', '0'::integer
 );
+WARNING:  missing lock for relation "is_odd" (OID 17415, relkind i) @ TID (21,7)
  pg_restore_relation_stats 
 ---------------------------
  t
v39-0001-Lock-table-first-when-setting-index-relation-sta.patchtext/x-patch; charset=US-ASCII; name=v39-0001-Lock-table-first-when-setting-index-relation-sta.patchDownload
From a86d16c6413e624d7785c8347fe4e9fbf9a8ce66 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Mon, 20 Jan 2025 16:18:51 -0500
Subject: [PATCH v39] Lock table first when setting index relation statistics.

Jian He reported [1] a missing lock relation bug in
pg_restore_relation_stats when restoring stats to an index.

This fix follows the steps for proper locking prior to an inplace update
of a relation as specified in aac2c9b4fde8.

Confirmed that test case generates the reported error without the code
fix.

[1] https://www.postgresql.org/message-id/CACJufxGreTY7qsCV8%2BBkuv0p5SXGTScgh%3DD%2BDq6%3D%2B_%3DXTp7FWg%40mail.gmail.com
---
 src/backend/statistics/stat_utils.c        | 29 ++++++++++++++++++++--
 src/test/regress/expected/stats_import.out | 13 ++++++++++
 src/test/regress/sql/stats_import.sql      | 10 ++++++++
 3 files changed, 50 insertions(+), 2 deletions(-)

diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c
index 0d446f55b0..381eeb38e5 100644
--- a/src/backend/statistics/stat_utils.c
+++ b/src/backend/statistics/stat_utils.c
@@ -18,13 +18,16 @@
 
 #include "access/relation.h"
 #include "catalog/pg_database.h"
+#include "catalog/index.h"
 #include "funcapi.h"
 #include "miscadmin.h"
 #include "statistics/stat_utils.h"
 #include "utils/acl.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
+#include "utils/lsyscache.h"
 #include "utils/rel.h"
+#include "utils/syscache.h"
 
 /*
  * Ensure that a given argument is not null.
@@ -126,8 +129,30 @@ stats_check_arg_pair(FunctionCallInfo fcinfo,
 void
 stats_lock_check_privileges(Oid reloid)
 {
-	Relation	rel = relation_open(reloid, ShareUpdateExclusiveLock);
-	const char	relkind = rel->rd_rel->relkind;
+	const char	relkind = get_rel_relkind(reloid);
+	Relation	rel;
+	Oid			indrelid = InvalidOid;
+
+	/*
+	 * For indexes we must lock the table first to avoid deadlocking with
+	 * analyze/vacuum.
+	 */
+	if (relkind == RELKIND_INDEX)
+	{
+		Relation	parentrel;
+
+		indrelid = IndexGetRelation(reloid, false);
+
+		parentrel = relation_open(indrelid, ShareUpdateExclusiveLock);
+
+		relation_close(parentrel, NoLock);
+	}
+
+	rel = relation_open(reloid, ShareUpdateExclusiveLock);
+
+	if (OidIsValid(indrelid) &&
+		(indrelid != IndexGetRelation(rel->rd_rel->oid, false)))
+		elog(ERROR, "index parent oid recheck failed for index %u", reloid);
 
 	/* All of the types that can be used with ANALYZE, plus indexes */
 	switch (relkind)
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index fb50da1cd8..fd9a6d2c9b 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -1414,6 +1414,19 @@ SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import.complex_type,
 UNION ALL
 SELECT 4, 'four', NULL, int4range(0,100), NULL;
 CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
+/* Test for proper locking */
+SELECT * FROM pg_catalog.pg_restore_relation_stats(
+    'relation', 'stats_import.is_odd'::regclass,
+    'version', '180000'::integer,
+    'relpages', '11'::integer,
+    'reltuples', '10000'::real,
+    'relallvisible', '0'::integer
+);
+ pg_restore_relation_stats 
+---------------------------
+ t
+(1 row)
+
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 CREATE TABLE stats_import.test_clone ( LIKE stats_import.test )
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index d3058bf8f6..043708f529 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -1062,6 +1062,16 @@ SELECT 4, 'four', NULL, int4range(0,100), NULL;
 
 CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
 
+
+/* Test for proper locking */
+SELECT * FROM pg_catalog.pg_restore_relation_stats(
+    'relation', 'stats_import.is_odd'::regclass,
+    'version', '180000'::integer,
+    'relpages', '11'::integer,
+    'reltuples', '10000'::real,
+    'relallvisible', '0'::integer
+);
+
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 
-- 
2.48.1

#282Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#280)
Re: Statistics Import and Export

I believe you are referring to Tom's statement that "it'll be a
serious, serious error for [stats] not to be SECTION_DATA". The
statement is somewhat softened by the sentence that follows, and
slightly more by [2]. But it's pretty clear that SECTION_POST_DATA is,
at best, an implementation comprosmise.

The reason we need to put some stats in SECTION_POST_DATA is because of
the hack to resolve MVs that depend on primary keys by moving the MV
into SECTION_POST_DATA. (An MV can depend on a primary key when the
query has a GROUP BY that relies on functional dependencies to be
valid.) That's a fairly marginal case, and one we might be able to
resolve a better way in the future, so I don't think that should drive
the design.

I understand the benefits of having statistics on the underlying tables
could aid the performance of the queries that populate the materialized
views. What I struggle to understand is how that purpose isn't served
better by statistics being in SECTION_NONE like COMMENTs are, so that they
are imported immediately after the object that they reference.

Reagrding [2] and [3], we might need to reconsider the behavior of the
--data-only option. I asked for the v38 behavior out of a sense of
consistency and completeness (the ability to express whatever
combination of things the user might want). But re-reading those
messages, we might want --data-only to include the stats?

I think there's going to be some friction in the user's shift from thinking
that they did want only data to realizing that they actually didn't want
schema.

#283Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#281)
Re: Statistics Import and Export

On Mon, 2025-01-20 at 16:26 -0500, Corey Huinker wrote:

Attached is the patch, along with the regression test output prior to
the change to stat_utils.c.

Comments:

* For indexes, it looks like do_analyze_rel is opening the parent table
with ShareUpdateExclusive and the indexes with just AccessShare. Let's
follow that pattern.

* The import code allows setting stats for partitioned indexes while
ANALYZE does not, so it's hard to say for sure what we should do for
partitioned indexes. I suggest we treat them the same as ordinary
indexes. Alternatively, we could refuse to set stats on a partitioned
index, but the rest of the import feature is generally permissive about
what can be set, so I'm inclined to just treat them like ordinary
indexes with respect to locking semantics.

Regards,
Jeff Davis

#284Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#282)
Re: Statistics Import and Export

On Mon, 2025-01-20 at 16:45 -0500, Corey Huinker wrote:

What I struggle to understand is how that purpose isn't served better
by statistics being in SECTION_NONE like COMMENTs are, so that they
are imported immediately after the object that they reference.

Tom, you expressed the strongest opinions on this point, can you expand
a bit?

If I understand correctly:

* We strongly want stats to be exported by default[1]/messages/by-id/3228677.1713844341@sss.pgh.pa.us.

* Adding a SECTION_STATS could work, but would be non-trivial and might
break expectations about the set of sections available[2]/messages/by-id/3156140.1713817153@sss.pgh.pa.us.

* SECTION_NONE doesn't seem right. There would be no way to get the
stats using --section. Also, if there is no section boundary for the
stats, then couldn't they appear in a surprising order?

* I'm not sure about placing stats in SECTION_POST_DATA. That doesn't
seem terrible to me, but not great either.

* I'm also not 100% sure about the flags. The default should dump the
stats, of course. And I like the idea of allowing any combination of
schema, data and stats to be exported. But that leaves a wrinkle for --
data-only, which (as of v38) does not dump stats, because stats are a
third kind of thing. Perhaps stats should be expressed as a subtype of
data somehow, but I'm not sure exactly how.

Regards,
Jeff Davis

[1]: /messages/by-id/3228677.1713844341@sss.pgh.pa.us
/messages/by-id/3228677.1713844341@sss.pgh.pa.us

[2]: /messages/by-id/3156140.1713817153@sss.pgh.pa.us
/messages/by-id/3156140.1713817153@sss.pgh.pa.us

#285Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#283)
1 attachment(s)
Re: Statistics Import and Export

* For indexes, it looks like do_analyze_rel is opening the parent table
with ShareUpdateExclusive and the indexes with just AccessShare. Let's
follow that pattern.

Done.

* The import code allows setting stats for partitioned indexes while
ANALYZE does not, so it's hard to say for sure what we should do for
partitioned indexes. I suggest we treat them the same as ordinary
indexes. Alternatively, we could refuse to set stats on a partitioned
index, but the rest of the import feature is generally permissive about
what can be set, so I'm inclined to just treat them like ordinary
indexes with respect to locking semantics.

I tried that, adding a test for it. Treating partitioned indexes like
regular indexes causes the exact same error as was initially reported for
indexes, so I took it back out.

Updated attached, burning numbers in the stats-sequence even though the
pg_dump patches have been left out for the time being.

Attachments:

v40-0001-Lock-table-first-when-setting-index-relation-sta.patchtext/x-patch; charset=US-ASCII; name=v40-0001-Lock-table-first-when-setting-index-relation-sta.patchDownload
From 03a15471b0f5a6776331d430cca2df619047271b Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Mon, 20 Jan 2025 16:18:51 -0500
Subject: [PATCH v40] Lock table first when setting index relation statistics.

Jian He reported [1] a missing lock relation bug in
pg_restore_relation_stats when restoring stats to an index.

This fix follows the steps for proper locking prior to an inplace update
of a relation as specified in aac2c9b4fde8, and mimics the locking
behavior of the analyze command.

Confirmed that test case generates the reported error without the code
fix.

[1] https://www.postgresql.org/message-id/CACJufxGreTY7qsCV8%2BBkuv0p5SXGTScgh%3DD%2BDq6%3D%2B_%3DXTp7FWg%40mail.gmail.com
---
 src/backend/statistics/stat_utils.c        | 40 ++++++++++++++++++++--
 src/test/regress/expected/stats_import.out | 24 +++++++++++++
 src/test/regress/sql/stats_import.sql      | 18 ++++++++++
 3 files changed, 80 insertions(+), 2 deletions(-)

diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c
index 0d446f55b0..dc510fe253 100644
--- a/src/backend/statistics/stat_utils.c
+++ b/src/backend/statistics/stat_utils.c
@@ -17,14 +17,19 @@
 #include "postgres.h"
 
 #include "access/relation.h"
+#include "catalog/pg_class_d.h"
 #include "catalog/pg_database.h"
+#include "catalog/index.h"
 #include "funcapi.h"
 #include "miscadmin.h"
 #include "statistics/stat_utils.h"
+#include "storage/lockdefs.h"
 #include "utils/acl.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
+#include "utils/lsyscache.h"
 #include "utils/rel.h"
+#include "utils/syscache.h"
 
 /*
  * Ensure that a given argument is not null.
@@ -126,8 +131,39 @@ stats_check_arg_pair(FunctionCallInfo fcinfo,
 void
 stats_lock_check_privileges(Oid reloid)
 {
-	Relation	rel = relation_open(reloid, ShareUpdateExclusiveLock);
-	const char	relkind = rel->rd_rel->relkind;
+	const char	relkind = get_rel_relkind(reloid);
+	Relation	rel;
+	Oid			indrelid = InvalidOid;
+	LOCKMODE	lockmode = ShareUpdateExclusiveLock;
+
+	/*
+	 * For indexes we follow what do_analyze_rel() does so as to avoid any
+	 * deadlocks with analyze/vacuum, with one exception, and that is
+	 * that we will also lock partitioned indexes because if we encounter
+	 * one then we are about to create pg_statistic rows that reference it.
+	 */
+	/* if (relkind == RELKIND_INDEX || relkind == RELKIND_PARTITIONED_INDEX) */
+	if (relkind == RELKIND_INDEX)
+	{
+		Relation	parentrel;
+
+		/*
+		 * Lock the table instead, and get a lesser lock on the index itself.
+		 */
+		indrelid = IndexGetRelation(reloid, false);
+
+		parentrel = relation_open(indrelid, ShareUpdateExclusiveLock);
+
+		relation_close(parentrel, NoLock);
+
+		lockmode = AccessShareLock;
+	}
+
+	rel = relation_open(reloid, lockmode);
+
+	if (OidIsValid(indrelid) &&
+		(indrelid != IndexGetRelation(rel->rd_rel->oid, false)))
+		elog(ERROR, "index parent oid recheck failed for index %u", reloid);
 
 	/* All of the types that can be used with ANALYZE, plus indexes */
 	switch (relkind)
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index fb50da1cd8..be283b4035 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -182,6 +182,7 @@ CREATE TABLE stats_import.part_child_1
   PARTITION OF stats_import.part_parent
   FOR VALUES FROM (0) TO (10)
   WITH (autovacuum_enabled = false);
+CREATE INDEX part_parent_i ON stats_import.part_parent(i);
 ANALYZE stats_import.part_parent;
 SELECT relpages
 FROM pg_class
@@ -202,6 +203,16 @@ SELECT
  
 (1 row)
 
+-- Check locking of partitioned index
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.part_parent_i'::regclass,
+        relpages => 2::integer);
+ pg_set_relation_stats 
+-----------------------
+ 
+(1 row)
+
 -- nothing stops us from setting it to -1
 SELECT
     pg_catalog.pg_set_relation_stats(
@@ -1414,6 +1425,19 @@ SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import.complex_type,
 UNION ALL
 SELECT 4, 'four', NULL, int4range(0,100), NULL;
 CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
+/* Test for proper locking */
+SELECT * FROM pg_catalog.pg_restore_relation_stats(
+    'relation', 'stats_import.is_odd'::regclass,
+    'version', '180000'::integer,
+    'relpages', '11'::integer,
+    'reltuples', '10000'::real,
+    'relallvisible', '0'::integer
+);
+ pg_restore_relation_stats 
+---------------------------
+ t
+(1 row)
+
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 CREATE TABLE stats_import.test_clone ( LIKE stats_import.test )
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index d3058bf8f6..298ac90fb3 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -127,6 +127,8 @@ CREATE TABLE stats_import.part_child_1
   FOR VALUES FROM (0) TO (10)
   WITH (autovacuum_enabled = false);
 
+CREATE INDEX part_parent_i ON stats_import.part_parent(i);
+
 ANALYZE stats_import.part_parent;
 
 SELECT relpages
@@ -140,6 +142,12 @@ SELECT
         relation => 'stats_import.part_parent'::regclass,
         relpages => 2::integer);
 
+-- Check locking of partitioned index
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.part_parent_i'::regclass,
+        relpages => 2::integer);
+
 -- nothing stops us from setting it to -1
 SELECT
     pg_catalog.pg_set_relation_stats(
@@ -1062,6 +1070,16 @@ SELECT 4, 'four', NULL, int4range(0,100), NULL;
 
 CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
 
+
+/* Test for proper locking */
+SELECT * FROM pg_catalog.pg_restore_relation_stats(
+    'relation', 'stats_import.is_odd'::regclass,
+    'version', '180000'::integer,
+    'relpages', '11'::integer,
+    'reltuples', '10000'::real,
+    'relallvisible', '0'::integer
+);
+
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 
-- 
2.48.1

#286Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#285)
Re: Statistics Import and Export

On Mon, 2025-01-20 at 21:25 -0500, Corey Huinker wrote:

I tried that, adding a test for it. Treating partitioned indexes like
regular indexes causes the exact same error as was initially reported
for indexes, so I took it back out.

I don't think we should declare (as a part of this feature) that
partitioned indexes should follow the table locking protocol for
updating stats. Let's just block updating stats for partitioned indexes
until there's a defined way to do so.

Also, we should check privileges on the right object, consistent with
how ANALYZE does it.

Regards,
Jeff Davis

#287Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#286)
Re: Statistics Import and Export

Let's just block updating stats for partitioned indexes
until there's a defined way to do so.

-1.
That will break pg_upgrade tests if there's any statistics out there for
any partitioned index.

Also, we should check privileges on the right object, consistent with
how ANALYZE does it.

+1

#288Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#287)
Re: Statistics Import and Export

On Mon, 2025-01-20 at 22:21 -0500, Corey Huinker wrote:

-1.
That will break pg_upgrade tests if there's any statistics out there
for any partitioned index.

Are you saying that there is a path for a partitioned index to have
stats today? If so, we can just follow that locking protocol. If not,
I'm concerned about trying to define the locking protocol for doing so
as a side-effect of this work.

Wouldn't it be easy enough to issue a WARNING and skip it?

Regards,
Jeff Davis

#289Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#288)
Re: Statistics Import and Export

Are you saying that there is a path for a partitioned index to have
stats today? If so, we can just follow that locking protocol. If not,
I'm concerned about trying to define the locking protocol for doing so
as a side-effect of this work.

A quick test on v17 indicates that, no, there is no path for a partitioned
index to have stats of its own, so I'm fine with removing
RELKIND_PARTITIONED_INDEX from the allowable relkinds. The partitions of
the partitioned index have stats but the umbrella index does not.

Wouldn't it be easy enough to issue a WARNING and skip it?

Sure.

#290Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#289)
1 attachment(s)
Re: Statistics Import and Export

On Mon, Jan 20, 2025 at 10:59 PM Corey Huinker <corey.huinker@gmail.com>
wrote:

Are you saying that there is a path for a partitioned index to have

stats today? If so, we can just follow that locking protocol. If not,
I'm concerned about trying to define the locking protocol for doing so
as a side-effect of this work.

A quick test on v17 indicates that, no, there is no path for a partitioned
index to have stats of its own, so I'm fine with removing
RELKIND_PARTITIONED_INDEX from the allowable relkinds. The partitions of
the partitioned index have stats but the umbrella index does not.

Wouldn't it be easy enough to issue a WARNING and skip it?

Sure.

So this is the next iteration - it rejects setting stats on partitioned
indexes entirely, and it does all the ACL checks against the underlying
relation instead of the indexrel. The only oddity is that if a user calls a
statistics setting function on some_index of some_table, and they do not
have permission to modify stats on some_table, the error message they get
reflects the lack of permission on some_table, not the some_index that was
invoked. That may be initially surprising, but it actually is more helpful
because the permissions that need to change are on the table not the index.

Attachments:

v41-0001-Lock-table-first-when-setting-index-relation-sta.patchtext/x-patch; charset=US-ASCII; name=v41-0001-Lock-table-first-when-setting-index-relation-sta.patchDownload
From 2d42f2e87a35eb51615df9f7617b143437b1a356 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Mon, 20 Jan 2025 16:18:51 -0500
Subject: [PATCH v41] Lock table first when setting index relation statistics.

Jian He reported [1] a missing lock relation bug in
pg_restore_relation_stats when restoring stats to an index.

This fix follows the steps for proper locking prior to an inplace update
of a relation as specified in aac2c9b4fde8, and mimics the locking
behavior of the analyze command, as well as correctly doing the ACL
checks against the underlying relation of an index rather than the index
itself.

[1] https://www.postgresql.org/message-id/CACJufxGreTY7qsCV8%2BBkuv0p5SXGTScgh%3DD%2BDq6%3D%2B_%3DXTp7FWg%40mail.gmail.com
---
 src/backend/statistics/attribute_stats.c   |  3 +-
 src/backend/statistics/stat_utils.c        | 37 ++++++++++++++++++----
 src/test/regress/expected/stats_import.out | 21 ++++++++++++
 src/test/regress/sql/stats_import.sql      | 18 +++++++++++
 4 files changed, 71 insertions(+), 8 deletions(-)

diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index 94f7dd63a0..ddb62921c1 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -480,8 +480,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 static Node *
 get_attr_expr(Relation rel, int attnum)
 {
-	if ((rel->rd_rel->relkind == RELKIND_INDEX
-		 || (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+	if ((rel->rd_rel->relkind == RELKIND_INDEX)
 		&& (rel->rd_indexprs != NIL)
 		&& (rel->rd_index->indkey.values[attnum - 1] == 0))
 	{
diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c
index 0d446f55b0..061d722908 100644
--- a/src/backend/statistics/stat_utils.c
+++ b/src/backend/statistics/stat_utils.c
@@ -17,14 +17,21 @@
 #include "postgres.h"
 
 #include "access/relation.h"
+#include "c.h"
+#include "catalog/pg_class_d.h"
 #include "catalog/pg_database.h"
+#include "catalog/index.h"
 #include "funcapi.h"
 #include "miscadmin.h"
 #include "statistics/stat_utils.h"
+#include "storage/lockdefs.h"
+#include "storage/lmgr.h"
 #include "utils/acl.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
+#include "utils/lsyscache.h"
 #include "utils/rel.h"
+#include "utils/syscache.h"
 
 /*
  * Ensure that a given argument is not null.
@@ -126,18 +133,32 @@ stats_check_arg_pair(FunctionCallInfo fcinfo,
 void
 stats_lock_check_privileges(Oid reloid)
 {
-	Relation	rel = relation_open(reloid, ShareUpdateExclusiveLock);
-	const char	relkind = rel->rd_rel->relkind;
+	Relation	rel;
+	Oid			relation_oid = reloid;
+	Oid			index_oid = InvalidOid;
 
-	/* All of the types that can be used with ANALYZE, plus indexes */
-	switch (relkind)
+
+	/*
+	 * For indexes, we follow what do_analyze_rel() does so as to avoid any
+	 * deadlocks with analyze/vacuum, which is to take out a
+	 * ShareUpdateExclusive on table/matview first and only after that take
+	 * a a AccessShareLock on the index itself.
+	 */
+	if (get_rel_relkind(reloid) == RELKIND_INDEX)
+	{
+		relation_oid = IndexGetRelation(reloid, false);
+		index_oid = reloid;
+	}
+
+	rel = relation_open(relation_oid, ShareUpdateExclusiveLock);
+
+	/* All of the types that can be used with ANALYZE */
+	switch (rel->rd_rel->relkind)
 	{
 		case RELKIND_RELATION:
-		case RELKIND_INDEX:
 		case RELKIND_MATVIEW:
 		case RELKIND_FOREIGN_TABLE:
 		case RELKIND_PARTITIONED_TABLE:
-		case RELKIND_PARTITIONED_INDEX:
 			break;
 		default:
 			ereport(ERROR,
@@ -164,7 +185,11 @@ stats_lock_check_privileges(Oid reloid)
 						   NameStr(rel->rd_rel->relname));
 	}
 
+	if (OidIsValid(index_oid))
+		LockRelationOid(index_oid, AccessShareLock);
+
 	relation_close(rel, NoLock);
+
 }
 
 /*
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index fb50da1cd8..0b706f0981 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -182,6 +182,7 @@ CREATE TABLE stats_import.part_child_1
   PARTITION OF stats_import.part_parent
   FOR VALUES FROM (0) TO (10)
   WITH (autovacuum_enabled = false);
+CREATE INDEX part_parent_i ON stats_import.part_parent(i);
 ANALYZE stats_import.part_parent;
 SELECT relpages
 FROM pg_class
@@ -202,6 +203,13 @@ SELECT
  
 (1 row)
 
+-- Cannot set stats on a partitioned index
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.part_parent_i'::regclass,
+        relpages => 2::integer);
+ERROR:  cannot modify statistics for relation "part_parent_i"
+DETAIL:  This operation is not supported for partitioned indexes.
 -- nothing stops us from setting it to -1
 SELECT
     pg_catalog.pg_set_relation_stats(
@@ -1414,6 +1422,19 @@ SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import.complex_type,
 UNION ALL
 SELECT 4, 'four', NULL, int4range(0,100), NULL;
 CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
+/* Test for proper locking */
+SELECT * FROM pg_catalog.pg_restore_relation_stats(
+    'relation', 'stats_import.is_odd'::regclass,
+    'version', '180000'::integer,
+    'relpages', '11'::integer,
+    'reltuples', '10000'::real,
+    'relallvisible', '0'::integer
+);
+ pg_restore_relation_stats 
+---------------------------
+ t
+(1 row)
+
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 CREATE TABLE stats_import.test_clone ( LIKE stats_import.test )
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index d3058bf8f6..efe17ff456 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -127,6 +127,8 @@ CREATE TABLE stats_import.part_child_1
   FOR VALUES FROM (0) TO (10)
   WITH (autovacuum_enabled = false);
 
+CREATE INDEX part_parent_i ON stats_import.part_parent(i);
+
 ANALYZE stats_import.part_parent;
 
 SELECT relpages
@@ -140,6 +142,12 @@ SELECT
         relation => 'stats_import.part_parent'::regclass,
         relpages => 2::integer);
 
+-- Cannot set stats on a partitioned index
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.part_parent_i'::regclass,
+        relpages => 2::integer);
+
 -- nothing stops us from setting it to -1
 SELECT
     pg_catalog.pg_set_relation_stats(
@@ -1062,6 +1070,16 @@ SELECT 4, 'four', NULL, int4range(0,100), NULL;
 
 CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
 
+
+/* Test for proper locking */
+SELECT * FROM pg_catalog.pg_restore_relation_stats(
+    'relation', 'stats_import.is_odd'::regclass,
+    'version', '180000'::integer,
+    'relpages', '11'::integer,
+    'reltuples', '10000'::real,
+    'relallvisible', '0'::integer
+);
+
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 
-- 
2.48.1

#291jian he
jian.universality@gmail.com
In reply to: Corey Huinker (#290)
Re: Statistics Import and Export

On Tue, Jan 21, 2025 at 2:43 PM Corey Huinker <corey.huinker@gmail.com> wrote:

On Mon, Jan 20, 2025 at 10:59 PM Corey Huinker <corey.huinker@gmail.com> wrote:

Are you saying that there is a path for a partitioned index to have
stats today? If so, we can just follow that locking protocol. If not,
I'm concerned about trying to define the locking protocol for doing so
as a side-effect of this work.

A quick test on v17 indicates that, no, there is no path for a partitioned index to have stats of its own, so I'm fine with removing RELKIND_PARTITIONED_INDEX from the allowable relkinds. The partitions of the partitioned index have stats but the umbrella index does not.

Wouldn't it be easy enough to issue a WARNING and skip it?

Sure.

So this is the next iteration - it rejects setting stats on partitioned indexes entirely, and it does all the ACL checks against the underlying relation instead of the indexrel. The only oddity is that if a user calls a statistics setting function on some_index of some_table, and they do not have permission to modify stats on some_table, the error message they get reflects the lack of permission on some_table, not the some_index that was invoked. That may be initially surprising, but it actually is more helpful because the permissions that need to change are on the table not the index.

/*
* For indexes, we follow what do_analyze_rel() does so as to avoid any
* deadlocks with analyze/vacuum, which is to take out a
* ShareUpdateExclusive on table/matview first and only after that take
* a a AccessShareLock on the index itself.
*/
there are two "a a".

 #include "access/relation.h"
+#include "c.h"
+#include "catalog/pg_class_d.h"
 #include "catalog/pg_database.h"
+#include "catalog/index.h"
+#include "storage/lockdefs.h"
+#include "storage/lmgr.h"
 #include "utils/acl.h"
+#include "utils/lsyscache.h"
 #include "utils/rel.h"
+#include "utils/syscache.h"
+#include "c.h", no need. since we have "postgres.h"
+#include "storage/lockdefs.h" no need. because #include
"storage/lmgr.h" already have it.
+#include "utils/syscache.h" no need. because get_rel_relkind is in
``#include "utils/lsyscache.h"``

in pg_class.h we have
#include "catalog/pg_class_d.h" /* IWYU pragma: export */
if i read
https://github.com/include-what-you-use/include-what-you-use/blob/master/docs/IWYUPragmas.md#iwyu-pragma-export
correctly.
here
+#include "catalog/pg_class_d.h"
should be
+#include "catalog/pg_class.h"

#292Corey Huinker
corey.huinker@gmail.com
In reply to: jian he (#291)
Re: Statistics Import and Export

/*
* For indexes, we follow what do_analyze_rel() does so as to avoid any
* deadlocks with analyze/vacuum, which is to take out a
* ShareUpdateExclusive on table/matview first and only after that take
* a a AccessShareLock on the index itself.
*/
there are two "a a".

#include "access/relation.h"
+#include "c.h"
+#include "catalog/pg_class_d.h"
#include "catalog/pg_database.h"
+#include "catalog/index.h"
+#include "storage/lockdefs.h"
+#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "utils/lsyscache.h"
#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "c.h", no need. since we have "postgres.h"
+#include "storage/lockdefs.h" no need. because #include
"storage/lmgr.h" already have it.
+#include "utils/syscache.h" no need. because get_rel_relkind is in
``#include "utils/lsyscache.h"``

in pg_class.h we have
#include "catalog/pg_class_d.h" /* IWYU pragma: export */
if i read

https://github.com/include-what-you-use/include-what-you-use/blob/master/docs/IWYUPragmas.md#iwyu-pragma-export
correctly.
here
+#include "catalog/pg_class_d.h"
should be
+#include "catalog/pg_class.h"

Sorry about that, my nvim config is auto-including stuff and it's annoying.

#293Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#292)
1 attachment(s)
Re: Statistics Import and Export

Sorry about that, my nvim config is auto-including stuff and it's annoying.

Now with less includes and fewer typos:

Attachments:

v42-0001-Lock-table-first-when-setting-index-relation-sta.patchtext/x-patch; charset=US-ASCII; name=v42-0001-Lock-table-first-when-setting-index-relation-sta.patchDownload
From 93e67e5304132e680b628b3e92f9fd242d998abd Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Tue, 21 Jan 2025 11:52:58 -0500
Subject: [PATCH v42] Lock table first when setting index relation statistics.

Jian He reported [1] a missing lock relation bug in
pg_restore_relation_stats when restoring stats to an index.

This fix follows the steps for proper locking prior to an inplace update
of a relation as specified in aac2c9b4fde8, and mimics the locking
behavior of the analyze command, as well as correctly doing the ACL
checks against the underlying relation of an index rather than the index
itself.

[1] https://www.postgresql.org/message-id/CACJufxGreTY7qsCV8%2BBkuv0p5SXGTScgh%3DD%2BDq6%3D%2B_%3DXTp7FWg%40mail.gmail.com
---
 src/backend/statistics/attribute_stats.c   |  3 +-
 src/backend/statistics/stat_utils.c        | 33 ++++++++++++++++++----
 src/test/regress/expected/stats_import.out | 21 ++++++++++++++
 src/test/regress/sql/stats_import.sql      | 18 ++++++++++++
 4 files changed, 67 insertions(+), 8 deletions(-)

diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index 94f7dd63a0..ddb62921c1 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -480,8 +480,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 static Node *
 get_attr_expr(Relation rel, int attnum)
 {
-	if ((rel->rd_rel->relkind == RELKIND_INDEX
-		 || (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+	if ((rel->rd_rel->relkind == RELKIND_INDEX)
 		&& (rel->rd_indexprs != NIL)
 		&& (rel->rd_index->indkey.values[attnum - 1] == 0))
 	{
diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c
index 0d446f55b0..1abbe7d17a 100644
--- a/src/backend/statistics/stat_utils.c
+++ b/src/backend/statistics/stat_utils.c
@@ -18,12 +18,15 @@
 
 #include "access/relation.h"
 #include "catalog/pg_database.h"
+#include "catalog/index.h"
 #include "funcapi.h"
 #include "miscadmin.h"
 #include "statistics/stat_utils.h"
+#include "storage/lmgr.h"
 #include "utils/acl.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
+#include "utils/lsyscache.h"
 #include "utils/rel.h"
 
 /*
@@ -126,18 +129,32 @@ stats_check_arg_pair(FunctionCallInfo fcinfo,
 void
 stats_lock_check_privileges(Oid reloid)
 {
-	Relation	rel = relation_open(reloid, ShareUpdateExclusiveLock);
-	const char	relkind = rel->rd_rel->relkind;
+	Relation	rel;
+	Oid			relation_oid = reloid;
+	Oid			index_oid = InvalidOid;
 
-	/* All of the types that can be used with ANALYZE, plus indexes */
-	switch (relkind)
+
+	/*
+	 * For indexes, we follow what do_analyze_rel() does so as to avoid any
+	 * deadlocks with analyze/vacuum, which is to take out a
+	 * ShareUpdateExclusive on table/matview first and only after that take
+	 * a AccessShareLock on the index itself.
+	 */
+	if (get_rel_relkind(reloid) == RELKIND_INDEX)
+	{
+		relation_oid = IndexGetRelation(reloid, false);
+		index_oid = reloid;
+	}
+
+	rel = relation_open(relation_oid, ShareUpdateExclusiveLock);
+
+	/* All of the types that can be used with ANALYZE */
+	switch (rel->rd_rel->relkind)
 	{
 		case RELKIND_RELATION:
-		case RELKIND_INDEX:
 		case RELKIND_MATVIEW:
 		case RELKIND_FOREIGN_TABLE:
 		case RELKIND_PARTITIONED_TABLE:
-		case RELKIND_PARTITIONED_INDEX:
 			break;
 		default:
 			ereport(ERROR,
@@ -164,7 +181,11 @@ stats_lock_check_privileges(Oid reloid)
 						   NameStr(rel->rd_rel->relname));
 	}
 
+	if (OidIsValid(index_oid))
+		LockRelationOid(index_oid, AccessShareLock);
+
 	relation_close(rel, NoLock);
+
 }
 
 /*
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index fb50da1cd8..0b706f0981 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -182,6 +182,7 @@ CREATE TABLE stats_import.part_child_1
   PARTITION OF stats_import.part_parent
   FOR VALUES FROM (0) TO (10)
   WITH (autovacuum_enabled = false);
+CREATE INDEX part_parent_i ON stats_import.part_parent(i);
 ANALYZE stats_import.part_parent;
 SELECT relpages
 FROM pg_class
@@ -202,6 +203,13 @@ SELECT
  
 (1 row)
 
+-- Cannot set stats on a partitioned index
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.part_parent_i'::regclass,
+        relpages => 2::integer);
+ERROR:  cannot modify statistics for relation "part_parent_i"
+DETAIL:  This operation is not supported for partitioned indexes.
 -- nothing stops us from setting it to -1
 SELECT
     pg_catalog.pg_set_relation_stats(
@@ -1414,6 +1422,19 @@ SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import.complex_type,
 UNION ALL
 SELECT 4, 'four', NULL, int4range(0,100), NULL;
 CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
+/* Test for proper locking */
+SELECT * FROM pg_catalog.pg_restore_relation_stats(
+    'relation', 'stats_import.is_odd'::regclass,
+    'version', '180000'::integer,
+    'relpages', '11'::integer,
+    'reltuples', '10000'::real,
+    'relallvisible', '0'::integer
+);
+ pg_restore_relation_stats 
+---------------------------
+ t
+(1 row)
+
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 CREATE TABLE stats_import.test_clone ( LIKE stats_import.test )
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index d3058bf8f6..efe17ff456 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -127,6 +127,8 @@ CREATE TABLE stats_import.part_child_1
   FOR VALUES FROM (0) TO (10)
   WITH (autovacuum_enabled = false);
 
+CREATE INDEX part_parent_i ON stats_import.part_parent(i);
+
 ANALYZE stats_import.part_parent;
 
 SELECT relpages
@@ -140,6 +142,12 @@ SELECT
         relation => 'stats_import.part_parent'::regclass,
         relpages => 2::integer);
 
+-- Cannot set stats on a partitioned index
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.part_parent_i'::regclass,
+        relpages => 2::integer);
+
 -- nothing stops us from setting it to -1
 SELECT
     pg_catalog.pg_set_relation_stats(
@@ -1062,6 +1070,16 @@ SELECT 4, 'four', NULL, int4range(0,100), NULL;
 
 CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
 
+
+/* Test for proper locking */
+SELECT * FROM pg_catalog.pg_restore_relation_stats(
+    'relation', 'stats_import.is_odd'::regclass,
+    'version', '180000'::integer,
+    'relpages', '11'::integer,
+    'reltuples', '10000'::real,
+    'relallvisible', '0'::integer
+);
+
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 
-- 
2.48.1

#294jian he
jian.universality@gmail.com
In reply to: Corey Huinker (#293)
Re: Statistics Import and Export

hi.
now stats_lock_check_privileges comments need to change?

* Lock relation in ShareUpdateExclusive mode, check privileges, and close the
* relation (but retain the lock).

since the above comments will not be true for RELKIND_INDEX.
Yes, there are comments within the function explaining the exception.

maybe we can incorporate the following
/*
* For indexes, we follow what do_analyze_rel() does so as to avoid any
* deadlocks with analyze/vacuum, which is to take out a
* ShareUpdateExclusive on table/matview first and only after that take
* a AccessShareLock on the index itself.
*/
comments into stats_lock_check_privileges comments section.

other than that, the patch fix looks good to me.

#295Corey Huinker
corey.huinker@gmail.com
In reply to: jian he (#294)
2 attachment(s)
Re: Statistics Import and Export

On Tue, Jan 21, 2025 at 8:37 PM jian he <jian.universality@gmail.com> wrote:

hi.
now stats_lock_check_privileges comments need to change?

* Lock relation in ShareUpdateExclusive mode, check privileges, and close
the
* relation (but retain the lock).

since the above comments will not be true for RELKIND_INDEX.
Yes, there are comments within the function explaining the exception.

maybe we can incorporate the following
/*
* For indexes, we follow what do_analyze_rel() does so as to avoid any
* deadlocks with analyze/vacuum, which is to take out a
* ShareUpdateExclusive on table/matview first and only after that take
* a AccessShareLock on the index itself.
*/
comments into stats_lock_check_privileges comments section.

other than that, the patch fix looks good to me.

After some research, I think that we should treat partitioned indexes like
we were before, and just handle the existing special case for regular
indexes. That patch is attached below, along with a few updates to pg_dump.
There are more changes in the works addressing your earlier feedback, but
those seem to break as many things as they fix, so I'm still working on
them.

Attachments:

v43-0001-Lock-table-first-when-setting-index-relation-sta.patchtext/x-patch; charset=US-ASCII; name=v43-0001-Lock-table-first-when-setting-index-relation-sta.patchDownload
From 40ec06c622c4be1de0e87509b2655e1d3ff62b4b Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Tue, 21 Jan 2025 11:52:58 -0500
Subject: [PATCH v43 1/2] Lock table first when setting index relation
 statistics.

Jian He reported [1] a missing lock relation bug in
pg_restore_relation_stats when restoring stats to an index.

This fix follows the steps for proper locking prior to an inplace update
of a relation as specified in aac2c9b4fde8, and mimics the locking
behavior of the analyze command, as well as correctly doing the ACL
checks against the underlying relation of an index rather than the index
itself.

These special rules do not apply to partitioned indexes, which have no
special case i check_inplace_rel_lock().

[1] https://www.postgresql.org/message-id/CACJufxGreTY7qsCV8%2BBkuv0p5SXGTScgh%3DD%2BDq6%3D%2B_%3DXTp7FWg%40mail.gmail.com
---
 src/backend/statistics/attribute_stats.c   |  2 +-
 src/backend/statistics/stat_utils.c        | 34 ++++++++++++--
 src/test/regress/expected/stats_import.out | 53 ++++++++++++++++++++++
 src/test/regress/sql/stats_import.sql      | 36 +++++++++++++++
 4 files changed, 119 insertions(+), 6 deletions(-)

diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index 94f7dd63a0..5d4762e404 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -481,7 +481,7 @@ static Node *
 get_attr_expr(Relation rel, int attnum)
 {
 	if ((rel->rd_rel->relkind == RELKIND_INDEX
-		 || (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+		|| (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
 		&& (rel->rd_indexprs != NIL)
 		&& (rel->rd_index->indkey.values[attnum - 1] == 0))
 	{
diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c
index 0d446f55b0..4af9c94174 100644
--- a/src/backend/statistics/stat_utils.c
+++ b/src/backend/statistics/stat_utils.c
@@ -18,12 +18,16 @@
 
 #include "access/relation.h"
 #include "catalog/pg_database.h"
+#include "catalog/index.h"
 #include "funcapi.h"
 #include "miscadmin.h"
 #include "statistics/stat_utils.h"
+#include "storage/lmgr.h"
+#include "storage/lockdefs.h"
 #include "utils/acl.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
+#include "utils/lsyscache.h"
 #include "utils/rel.h"
 
 /*
@@ -126,14 +130,31 @@ stats_check_arg_pair(FunctionCallInfo fcinfo,
 void
 stats_lock_check_privileges(Oid reloid)
 {
-	Relation	rel = relation_open(reloid, ShareUpdateExclusiveLock);
-	const char	relkind = rel->rd_rel->relkind;
+	Relation	rel;
+	Oid			relation_oid = reloid;
+	Oid			index_oid = InvalidOid;
 
-	/* All of the types that can be used with ANALYZE, plus indexes */
-	switch (relkind)
+	/*
+	 * For indexes, we follow what do_analyze_rel() does so as to avoid any
+	 * deadlocks with analyze/vacuum, which is to take out a
+	 * ShareUpdateExclusive on table/matview first and then take an
+	 * AccessShareLock on the index itself. See check_inplace_rel_lock()
+	 * to see how this special case is implemented.
+	 */
+	if (get_rel_relkind(reloid) == RELKIND_INDEX)
+	{
+		relation_oid = IndexGetRelation(reloid, false);
+		index_oid = reloid;
+	}
+
+	rel = relation_open(relation_oid, ShareUpdateExclusiveLock);
+
+	/*
+	 * All of the types that can hold statistics, except for regular indexes
+	 */
+	switch (rel->rd_rel->relkind)
 	{
 		case RELKIND_RELATION:
-		case RELKIND_INDEX:
 		case RELKIND_MATVIEW:
 		case RELKIND_FOREIGN_TABLE:
 		case RELKIND_PARTITIONED_TABLE:
@@ -164,6 +185,9 @@ stats_lock_check_privileges(Oid reloid)
 						   NameStr(rel->rd_rel->relname));
 	}
 
+	if (OidIsValid(index_oid))
+		LockRelationOid(index_oid, AccessShareLock);
+
 	relation_close(rel, NoLock);
 }
 
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index fb50da1cd8..2e80cce11d 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -85,6 +85,26 @@ WHERE oid = 'stats_import.test'::regclass;
        17 |       400 |             4
 (1 row)
 
+CREATE INDEX test_i ON stats_import.test(id);
+-- regular indexes have special case locking rules
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test_i'::regclass,
+        relpages => 18::integer);
+ pg_set_relation_stats 
+-----------------------
+ 
+(1 row)
+
+SELECT
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.test_i'::regclass,
+        'relpages', 19::integer );
+ pg_restore_relation_stats 
+---------------------------
+ t
+(1 row)
+
 -- positional arguments
 SELECT
     pg_catalog.pg_set_relation_stats(
@@ -182,6 +202,7 @@ CREATE TABLE stats_import.part_child_1
   PARTITION OF stats_import.part_parent
   FOR VALUES FROM (0) TO (10)
   WITH (autovacuum_enabled = false);
+CREATE INDEX part_parent_i ON stats_import.part_parent(i);
 ANALYZE stats_import.part_parent;
 SELECT relpages
 FROM pg_class
@@ -202,6 +223,25 @@ SELECT
  
 (1 row)
 
+-- Partitioned indexes aren't analyzed but it is possible to set stats.
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.part_parent_i'::regclass,
+        relpages => 2::integer);
+ pg_set_relation_stats 
+-----------------------
+ 
+(1 row)
+
+SELECT
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.part_parent_i'::regclass,
+        'relpages', 2::integer);
+ pg_restore_relation_stats 
+---------------------------
+ t
+(1 row)
+
 -- nothing stops us from setting it to -1
 SELECT
     pg_catalog.pg_set_relation_stats(
@@ -1414,6 +1454,19 @@ SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import.complex_type,
 UNION ALL
 SELECT 4, 'four', NULL, int4range(0,100), NULL;
 CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
+/* Test for proper locking */
+SELECT * FROM pg_catalog.pg_restore_relation_stats(
+    'relation', 'stats_import.is_odd'::regclass,
+    'version', '180000'::integer,
+    'relpages', '11'::integer,
+    'reltuples', '10000'::real,
+    'relallvisible', '0'::integer
+);
+ pg_restore_relation_stats 
+---------------------------
+ t
+(1 row)
+
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 CREATE TABLE stats_import.test_clone ( LIKE stats_import.test )
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index d3058bf8f6..d4d30a7368 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -64,6 +64,19 @@ SELECT relpages, reltuples, relallvisible
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
+CREATE INDEX test_i ON stats_import.test(id);
+
+-- regular indexes have special case locking rules
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test_i'::regclass,
+        relpages => 18::integer);
+
+SELECT
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.test_i'::regclass,
+        'relpages', 19::integer );
+
 -- positional arguments
 SELECT
     pg_catalog.pg_set_relation_stats(
@@ -127,6 +140,8 @@ CREATE TABLE stats_import.part_child_1
   FOR VALUES FROM (0) TO (10)
   WITH (autovacuum_enabled = false);
 
+CREATE INDEX part_parent_i ON stats_import.part_parent(i);
+
 ANALYZE stats_import.part_parent;
 
 SELECT relpages
@@ -140,6 +155,17 @@ SELECT
         relation => 'stats_import.part_parent'::regclass,
         relpages => 2::integer);
 
+-- Partitioned indexes aren't analyzed but it is possible to set stats.
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.part_parent_i'::regclass,
+        relpages => 2::integer);
+
+SELECT
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.part_parent_i'::regclass,
+        'relpages', 2::integer);
+
 -- nothing stops us from setting it to -1
 SELECT
     pg_catalog.pg_set_relation_stats(
@@ -1062,6 +1088,16 @@ SELECT 4, 'four', NULL, int4range(0,100), NULL;
 
 CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
 
+
+/* Test for proper locking */
+SELECT * FROM pg_catalog.pg_restore_relation_stats(
+    'relation', 'stats_import.is_odd'::regclass,
+    'version', '180000'::integer,
+    'relpages', '11'::integer,
+    'reltuples', '10000'::real,
+    'relallvisible', '0'::integer
+);
+
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 
-- 
2.48.1

v43-0002-Enable-dumping-of-table-index-stats-in-pg_dump.patchtext/x-patch; charset=US-ASCII; name=v43-0002-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From f51806a82024b4bd2486b0680c08ccbf7c422bad Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH v43 2/2] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will generate a statement that
calls pg_set_relation_stats(), and it will generate a series of
statements that call pg_set_attribute_stats(), one per attribute. These
statements will restore the statistics of the current system onto the
destination system.

Adds the command-line options -X / --statistics-only, which are mutually
exclusive to --schema-only and --data-only.

Statistics are not dumped when --schema-only is specified, except during
a binary upgrade.

As is the pattern with pg_dump options, statistics can be disabled using
--no-statistics.

Table statistics are dumped in the data section. This is true even if
dumping stats in a binary upgrade. Index and Materialized View
statistics are dumped in the post-data section.

Add --no-data option.

This option is useful for situations where someone wishes to test
query plans from a production database without copying production data.

This also makes the corresponding change to the simulated pg_upgrade in
the TAP tests for pg_dump.

This checks that dumping statistics is now the default, and that
--no-statistics will suppress statistics.

Add --no-schema option to pg_dump, etc.

Previously, users could use --data-only when they wanted to suppress
schema from a dump. However, that no longer makes sense now that the
data/schema binary has become the data/schema/statistics trinary.
---
 src/bin/pg_dump/pg_backup.h          |  10 +-
 src/bin/pg_dump/pg_backup_archiver.c |  70 ++++-
 src/bin/pg_dump/pg_backup_archiver.h |   3 +-
 src/bin/pg_dump/pg_dump.c            | 384 ++++++++++++++++++++++++++-
 src/bin/pg_dump/pg_dump.h            |   9 +
 src/bin/pg_dump/pg_dump_sort.c       |  32 ++-
 src/bin/pg_dump/pg_dumpall.c         |   5 +
 src/bin/pg_dump/pg_restore.c         |  32 ++-
 src/bin/pg_dump/t/001_basic.pl       |  18 ++
 src/bin/pg_dump/t/002_pg_dump.pl     |  61 ++++-
 src/bin/pg_upgrade/dump.c            |   6 +-
 src/bin/pg_upgrade/option.c          |  12 +
 src/bin/pg_upgrade/pg_upgrade.h      |   1 +
 doc/src/sgml/ref/pg_dump.sgml        |  69 +++--
 doc/src/sgml/ref/pg_dumpall.sgml     |  38 +++
 doc/src/sgml/ref/pg_restore.sgml     |  51 +++-
 doc/src/sgml/ref/pgupgrade.sgml      |  18 ++
 src/tools/pgindent/typedefs.list     |   1 +
 18 files changed, 771 insertions(+), 49 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index f0f19bb0b2..3fa1474fad 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -110,9 +110,12 @@ typedef struct _restoreOptions
 	int			column_inserts;
 	int			if_exists;
 	int			no_comments;	/* Skip comments */
+	int			no_data;			/* Skip data */
 	int			no_publications;	/* Skip publication entries */
+	int			no_schema;			/* Skip schema generation */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
@@ -160,6 +163,7 @@ typedef struct _restoreOptions
 	/* flags derived from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } RestoreOptions;
 
 typedef struct _dumpOptions
@@ -179,8 +183,11 @@ typedef struct _dumpOptions
 	int			column_inserts;
 	int			if_exists;
 	int			no_comments;
-	int			no_security_labels;
+	int			no_data;
 	int			no_publications;
+	int			no_schema;
+	int			no_security_labels;
+	int			no_statistics;
 	int			no_subscriptions;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
@@ -208,6 +215,7 @@ typedef struct _dumpOptions
 	/* flags derived from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } DumpOptions;
 
 /*
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 707a3fc844..d651b9b764 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -46,6 +46,11 @@
 #define TEXT_DUMP_HEADER "--\n-- PostgreSQL database dump\n--\n\n"
 #define TEXT_DUMPALL_HEADER "--\n-- PostgreSQL database cluster dump\n--\n\n"
 
+typedef enum entryType {
+	default_entry,
+	data_entry,
+	statistics_entry
+} entryType;
 
 static ArchiveHandle *_allocAH(const char *FileSpec, const ArchiveFormat fmt,
 							   const pg_compress_specification compression_spec,
@@ -53,7 +58,7 @@ static ArchiveHandle *_allocAH(const char *FileSpec, const ArchiveFormat fmt,
 							   SetupWorkerPtrType setupWorkerPtr,
 							   DataDirSyncMethod sync_method);
 static void _getObjectDescription(PQExpBuffer buf, const TocEntry *te);
-static void _printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData);
+static void _printTocEntry(ArchiveHandle *AH, TocEntry *te, entryType entry_type);
 static char *sanitize_line(const char *str, bool want_hyphen);
 static void _doSetFixedOutputState(ArchiveHandle *AH);
 static void _doSetSessionAuth(ArchiveHandle *AH, const char *user);
@@ -149,6 +154,7 @@ InitDumpOptions(DumpOptions *opts)
 	opts->dumpSections = DUMP_UNSECTIONED;
 	opts->dumpSchema = true;
 	opts->dumpData = true;
+	opts->dumpStatistics = true;
 }
 
 /*
@@ -169,9 +175,10 @@ dumpOptionsFromRestoreOptions(RestoreOptions *ropt)
 	dopt->outputClean = ropt->dropSchema;
 	dopt->dumpData = ropt->dumpData;
 	dopt->dumpSchema = ropt->dumpSchema;
+	dopt->dumpSections = ropt->dumpSections;
+	dopt->dumpStatistics = ropt->dumpStatistics;
 	dopt->if_exists = ropt->if_exists;
 	dopt->column_inserts = ropt->column_inserts;
-	dopt->dumpSections = ropt->dumpSections;
 	dopt->aclsSkip = ropt->aclsSkip;
 	dopt->outputSuperuser = ropt->superuser;
 	dopt->outputCreateDB = ropt->createDB;
@@ -186,6 +193,9 @@ dumpOptionsFromRestoreOptions(RestoreOptions *ropt)
 	dopt->no_publications = ropt->no_publications;
 	dopt->no_security_labels = ropt->no_security_labels;
 	dopt->no_subscriptions = ropt->no_subscriptions;
+	dopt->no_data = ropt->no_data;
+	dopt->no_schema = ropt->no_schema;
+	dopt->no_statistics = ropt->no_statistics;
 	dopt->lockWaitTimeout = ropt->lockWaitTimeout;
 	dopt->include_everything = ropt->include_everything;
 	dopt->enable_row_security = ropt->enable_row_security;
@@ -739,7 +749,7 @@ RestoreArchive(Archive *AHX)
 
 		for (te = AH->toc->next; te != AH->toc; te = te->next)
 		{
-			if ((te->reqs & (REQ_SCHEMA | REQ_DATA)) == 0)
+			if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) == 0)
 				continue;		/* ignore if not to be dumped at all */
 
 			switch (_tocEntryRestorePass(te))
@@ -760,7 +770,7 @@ RestoreArchive(Archive *AHX)
 		{
 			for (te = AH->toc->next; te != AH->toc; te = te->next)
 			{
-				if ((te->reqs & (REQ_SCHEMA | REQ_DATA)) != 0 &&
+				if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) != 0 &&
 					_tocEntryRestorePass(te) == RESTORE_PASS_ACL)
 					(void) restore_toc_entry(AH, te, false);
 			}
@@ -770,7 +780,7 @@ RestoreArchive(Archive *AHX)
 		{
 			for (te = AH->toc->next; te != AH->toc; te = te->next)
 			{
-				if ((te->reqs & (REQ_SCHEMA | REQ_DATA)) != 0 &&
+				if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) != 0 &&
 					_tocEntryRestorePass(te) == RESTORE_PASS_POST_ACL)
 					(void) restore_toc_entry(AH, te, false);
 			}
@@ -869,7 +879,7 @@ restore_toc_entry(ArchiveHandle *AH, TocEntry *te, bool is_parallel)
 			pg_log_info("creating %s \"%s\"",
 						te->desc, te->tag);
 
-		_printTocEntry(AH, te, false);
+		_printTocEntry(AH, te, default_entry);
 		defnDumped = true;
 
 		if (strcmp(te->desc, "TABLE") == 0)
@@ -938,7 +948,7 @@ restore_toc_entry(ArchiveHandle *AH, TocEntry *te, bool is_parallel)
 			 */
 			if (AH->PrintTocDataPtr != NULL)
 			{
-				_printTocEntry(AH, te, true);
+				_printTocEntry(AH, te, data_entry);
 
 				if (strcmp(te->desc, "BLOBS") == 0 ||
 					strcmp(te->desc, "BLOB COMMENTS") == 0)
@@ -1036,15 +1046,24 @@ restore_toc_entry(ArchiveHandle *AH, TocEntry *te, bool is_parallel)
 		{
 			/* If we haven't already dumped the defn part, do so now */
 			pg_log_info("executing %s %s", te->desc, te->tag);
-			_printTocEntry(AH, te, false);
+			_printTocEntry(AH, te, default_entry);
 		}
 	}
 
+	/*
+	 * If it has a statistics component that we want, then process that
+	 */
+	if ((reqs & REQ_STATS) != 0)
+	{
+		_printTocEntry(AH, te, statistics_entry);
+		defnDumped = true;
+	}
+
 	/*
 	 * If we emitted anything for this TOC entry, that counts as one action
 	 * against the transaction-size limit.  Commit if it's time to.
 	 */
-	if ((reqs & (REQ_SCHEMA | REQ_DATA)) != 0 && ropt->txn_size > 0)
+	if ((reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) != 0 && ropt->txn_size > 0)
 	{
 		if (++AH->txnCount >= ropt->txn_size)
 		{
@@ -1084,6 +1103,7 @@ NewRestoreOptions(void)
 	opts->compression_spec.level = 0;
 	opts->dumpSchema = true;
 	opts->dumpData = true;
+	opts->dumpStatistics = true;
 
 	return opts;
 }
@@ -1093,6 +1113,7 @@ _disableTriggersIfNecessary(ArchiveHandle *AH, TocEntry *te)
 {
 	RestoreOptions *ropt = AH->public.ropt;
 
+	ahprintf(AH, "-- ALTER TABLE DISABLE TRIGGER ALL %d %d;   \n\n", ropt->dumpSchema, ropt->disable_triggers);
 	/* This hack is only needed in a data-only restore */
 	if (ropt->dumpSchema || !ropt->disable_triggers)
 		return;
@@ -2962,6 +2983,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's statistics and we don't want statistics, maybe ignore it */
+	if (!ropt->dumpStatistics && strcmp(te->desc, "STATISTICS DATA") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2991,6 +3016,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS DATA") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
@@ -3107,6 +3133,15 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 		}
 	}
 
+	/*
+	 * Statistics Data entries have no other components.
+	 */
+	/* 
+	 * TODO: removed for now
+	if (strcmp(te->desc, "STATISTICS DATA") == 0)
+		return REQ_STATS;
+	*/
+
 	/*
 	 * Determine whether the TOC entry contains schema and/or data components,
 	 * and mask off inapplicable REQ bits.  If it had a dataDumper, assume
@@ -3729,7 +3764,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te)
  * will remain at default, until the matching ACL TOC entry is restored.
  */
 static void
-_printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData)
+_printTocEntry(ArchiveHandle *AH, TocEntry *te, entryType entry_type)
 {
 	RestoreOptions *ropt = AH->public.ropt;
 
@@ -3753,10 +3788,17 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData)
 		char	   *sanitized_schema;
 		char	   *sanitized_owner;
 
-		if (isData)
-			pfx = "Data for ";
-		else
-			pfx = "";
+		switch (entry_type)
+		{
+			case data_entry:
+				pfx = "Data for ";
+				break;
+			case statistics_entry:
+				pfx = "Statistics for ";
+				break;
+			default:
+				pfx = "";
+		}
 
 		ahprintf(AH, "--\n");
 		if (AH->public.verbose)
diff --git a/src/bin/pg_dump/pg_backup_archiver.h b/src/bin/pg_dump/pg_backup_archiver.h
index ce5ed1dd39..a2064f471e 100644
--- a/src/bin/pg_dump/pg_backup_archiver.h
+++ b/src/bin/pg_dump/pg_backup_archiver.h
@@ -209,7 +209,8 @@ typedef enum
 
 #define REQ_SCHEMA	0x01		/* want schema */
 #define REQ_DATA	0x02		/* want data */
-#define REQ_SPECIAL	0x04		/* for special TOC entries */
+#define REQ_STATS	0x04
+#define REQ_SPECIAL	0x08		/* for special TOC entries */
 
 struct _archiveHandle
 {
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 8f73a5df95..9e7cb2c48f 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -430,6 +430,7 @@ main(int argc, char **argv)
 	DataDirSyncMethod sync_method = DATA_DIR_SYNC_METHOD_FSYNC;
 	bool		data_only = false;
 	bool		schema_only = false;
+	bool		statistics_only = false;
 
 	static DumpOptions dopt;
 
@@ -466,6 +467,7 @@ main(int argc, char **argv)
 		{"encoding", required_argument, NULL, 'E'},
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
+		{"statistics-only", no_argument, NULL, 'X'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -492,8 +494,11 @@ main(int argc, char **argv)
 		{"strict-names", no_argument, &strict_names, 1},
 		{"use-set-session-authorization", no_argument, &dopt.use_setsessauth, 1},
 		{"no-comments", no_argument, &dopt.no_comments, 1},
+		{"no-data", no_argument, &dopt.no_data, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
+		{"no-schema", no_argument, &dopt.no_schema, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -539,7 +544,7 @@ main(int argc, char **argv)
 
 	InitDumpOptions(&dopt);
 
-	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:RsS:t:T:U:vwWxZ:",
+	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:PRsS:t:T:U:vwWxXZ:",
 							long_options, &optindex)) != -1)
 	{
 		switch (c)
@@ -613,6 +618,10 @@ main(int argc, char **argv)
 				dopt.cparams.pgport = pg_strdup(optarg);
 				break;
 
+			case 'X':			/* Dump statistics only */
+				statistics_only = true;
+				break;
+
 			case 'R':
 				/* no-op, still accepted for backwards compatibility */
 				break;
@@ -784,6 +793,17 @@ main(int argc, char **argv)
 
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (schema_only && statistics_only)
+		pg_fatal("options -s/--schema-only and -X/--statistics-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
+
+	if (data_only && dopt.no_data)
+		pg_fatal("options -a/--data-only and --no-data cannot be used together");
+	if (schema_only && dopt.no_schema)
+		pg_fatal("options -s/--schema-only and --no-schema cannot be used together");
+	if (statistics_only && dopt.no_statistics)
+		pg_fatal("options -X/--statistics-only and --no-statistics cannot be used together");
 
 	if (schema_only && foreign_servers_include_patterns.head != NULL)
 		pg_fatal("options -s/--schema-only and --include-foreign-data cannot be used together");
@@ -798,8 +818,9 @@ main(int argc, char **argv)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
 	/* set derivative flags */
-	dopt.dumpSchema = (!data_only);
-	dopt.dumpData = (!schema_only);
+	dopt.dumpData = data_only || (!schema_only && !statistics_only && !dopt.no_data);
+	dopt.dumpSchema = schema_only || (!data_only && !statistics_only && !dopt.no_schema);
+	dopt.dumpStatistics = statistics_only || (!data_only && !schema_only && !dopt.no_statistics);
 
 	/*
 	 * --inserts are already implied above if --column-inserts or
@@ -1099,6 +1120,7 @@ main(int argc, char **argv)
 	ropt->dropSchema = dopt.outputClean;
 	ropt->dumpData = dopt.dumpData;
 	ropt->dumpSchema = dopt.dumpSchema;
+	ropt->dumpStatistics = dopt.dumpStatistics;
 	ropt->if_exists = dopt.if_exists;
 	ropt->column_inserts = dopt.column_inserts;
 	ropt->dumpSections = dopt.dumpSections;
@@ -1177,7 +1199,7 @@ help(const char *progname)
 	printf(_("  -?, --help                   show this help, then exit\n"));
 
 	printf(_("\nOptions controlling the output content:\n"));
-	printf(_("  -a, --data-only              dump only the data, not the schema\n"));
+	printf(_("  -a, --data-only              dump only the data, not the schema or statistics\n"));
 	printf(_("  -b, --large-objects          include large objects in dump\n"));
 	printf(_("  --blobs                      (same as --large-objects, deprecated)\n"));
 	printf(_("  -B, --no-large-objects       exclude large objects in dump\n"));
@@ -1190,11 +1212,12 @@ help(const char *progname)
 	printf(_("  -N, --exclude-schema=PATTERN do NOT dump the specified schema(s)\n"));
 	printf(_("  -O, --no-owner               skip restoration of object ownership in\n"
 			 "                               plain-text format\n"));
-	printf(_("  -s, --schema-only            dump only the schema, no data\n"));
+	printf(_("  -s, --schema-only            dump only the schema, no data or statistics\n"));
 	printf(_("  -S, --superuser=NAME         superuser user name to use in plain-text format\n"));
 	printf(_("  -t, --table=PATTERN          dump only the specified table(s)\n"));
 	printf(_("  -T, --exclude-table=PATTERN  do NOT dump the specified table(s)\n"));
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
+	printf(_("  -X, --statistics-only        dump only the statistics, not schema or data\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
@@ -1219,8 +1242,11 @@ help(const char *progname)
 	printf(_("  --inserts                    dump data as INSERT commands, rather than COPY\n"));
 	printf(_("  --load-via-partition-root    load partitions via the root table\n"));
 	printf(_("  --no-comments                do not dump comment commands\n"));
+	printf(_("  --no-data                    do not dump data\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
+	printf(_("  --no-schema                  do not dump schema\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -6777,6 +6803,43 @@ getFuncs(Archive *fout)
 	destroyPQExpBuffer(query);
 }
 
+/*
+ * getRelationStatistics
+ *    register the statistics object as a dependent of the relation.
+ *
+ */
+static RelStatsInfo *
+getRelationStatistics(Archive *fout, DumpableObject *rel, char relkind)
+{
+	if ((relkind == RELKIND_RELATION) ||
+		(relkind == RELKIND_PARTITIONED_TABLE) ||
+		(relkind == RELKIND_INDEX) ||
+		(relkind == RELKIND_PARTITIONED_INDEX) ||
+		(relkind == RELKIND_MATVIEW))
+	{
+		RelStatsInfo	   *info = pg_malloc0(sizeof(RelStatsInfo));
+		DumpableObject	   *dobj = &info->dobj;
+
+		dobj->objType = DO_REL_STATS;
+		dobj->catId.tableoid = 0;
+		dobj->catId.oid = 0;
+		AssignDumpId(dobj);
+		dobj->dependencies = (DumpId *) pg_malloc(sizeof(DumpId));
+		dobj->dependencies[0] = rel->dumpId;
+		dobj->nDeps = 1;
+		dobj->allocDeps = 1;
+		dobj->components |= DUMP_COMPONENT_STATISTICS;
+		dobj->dump = rel->dump;
+		dobj->name = pg_strdup(rel->name);
+		dobj->namespace = rel->namespace;
+		info->relkind = relkind;
+		info->postponed_def = false;
+
+		return info;
+	}
+	return NULL;
+}
+
 /*
  * getTables
  *	  read all the tables (no indexes) in the system catalogs,
@@ -7154,6 +7217,7 @@ getTables(Archive *fout, int *numTables)
 
 		/* Tables have data */
 		tblinfo[i].dobj.components |= DUMP_COMPONENT_DATA;
+		tblinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
 
 		/* Mark whether table has an ACL */
 		if (!PQgetisnull(res, i, i_relacl))
@@ -7202,6 +7266,8 @@ getTables(Archive *fout, int *numTables)
 				}
 			}
 		}
+		if (tblinfo[i].interesting)
+			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relkind);
 	}
 
 	if (query->len != 0)
@@ -7648,11 +7714,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 		for (int c = 0; c < numinds; c++, j++)
 		{
 			char		contype;
+			char		indexkind;
+			RelStatsInfo   *relstats;
 
 			indxinfo[j].dobj.objType = DO_INDEX;
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -7675,7 +7744,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			{
 				NULL, NULL
 			};
+
+			if (indxinfo[j].parentidx == 0)
+				indexkind = RELKIND_INDEX;
+			else
+				indexkind = RELKIND_PARTITIONED_INDEX;
+
 			contype = *(PQgetvalue(res, j, i_contype));
+			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, indexkind);
 
 			if (contype == 'p' || contype == 'u' || contype == 'x')
 			{
@@ -7709,6 +7785,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				constrinfo->separate = true;
 
 				indxinfo[j].indexconstraint = constrinfo->dobj.dumpId;
+				if (relstats != NULL)
+					addObjectDependency(&relstats->dobj, constrinfo->dobj.dumpId);
 			}
 			else
 			{
@@ -10296,6 +10374,287 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * Tabular description of the parameters to pg_restore_relation_stats()
+ * param_name, param_type
+ */
+static const char *rel_stats_arginfo[][2] = {
+	{"relation", "regclass"},
+	{"version", "integer"},
+	{"relpages", "integer"},
+	{"reltuples", "real"},
+	{"relallvisible", "integer"},
+};
+
+/*
+ * Tabular description of the parameters to pg_restore_attribute_stats()
+ * param_name, param_type
+ */
+static const char *att_stats_arginfo[][2] = {
+	{"relation", "regclass"},
+	{"attname", "name"},
+	{"inherited", "boolean"},
+	{"version", "integer"},
+	{"null_frac", "float4"},
+	{"avg_width", "integer"},
+	{"n_distinct", "float4"},
+	{"most_common_vals", "text"},
+	{"most_common_freqs", "float4[]"},
+	{"histogram_bounds", "text"},
+	{"correlation", "float4"},
+	{"most_common_elems", "text"},
+	{"most_common_elem_freqs", "float4[]"},
+	{"elem_count_histogram", "float4[]"},
+	{"range_length_histogram", "text"},
+	{"range_empty_frac", "float4"},
+	{"range_bounds_histogram", "text"},
+};
+
+/*
+ * getRelStatsExportQuery --
+ *
+ * Generate a query that will fetch all relation (e.g. pg_class)
+ * stats for a given relation.
+ */
+static void
+getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "current_setting('server_version_num') AS version, "
+						 "c.relpages, c.reltuples, c.relallvisible "
+						 "FROM pg_class c "
+						 "JOIN pg_namespace n "
+						 "ON n.oid = c.relnamespace "
+						 "WHERE n.nspname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND c.relname = ");
+	appendStringLiteralAH(query, relname, fout);
+}
+
+/*
+ * getAttStatsExportQuery --
+ *
+ * Generate a query that will fetch all attribute (e.g. pg_statistic)
+ * stats for a given relation.
+ */
+static void
+getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "s.attname,"
+						 "s.inherited,"
+						 "current_setting('server_version_num') AS version, "
+						 "s.null_frac,"
+						 "s.avg_width,"
+						 "s.n_distinct,"
+						 "s.most_common_vals,"
+						 "s.most_common_freqs,"
+						 "s.histogram_bounds,"
+						 "s.correlation,"
+						 "s.most_common_elems,"
+						 "s.most_common_elem_freqs,"
+						 "s.elem_count_histogram,");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 "s.range_length_histogram,"
+							 "s.range_empty_frac,"
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(query,
+							 "NULL AS range_length_histogram,"
+							 "NULL AS range_empty_frac,"
+							 "NULL AS range_bounds_histogram ");
+
+	appendPQExpBufferStr(query,
+						 "FROM pg_stats s "
+						 "JOIN pg_namespace n "
+						 "ON n.nspname = s.schemaname "
+						 "JOIN pg_class c "
+						 "ON c.relname = s.tablename "
+						 "AND c.relnamespace = n.oid "
+						 "WHERE s.schemaname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND s.tablename = ");
+	appendStringLiteralAH(query, relname, fout);
+	appendPQExpBufferStr(query, " ORDER BY s.attname, s.inherited");
+}
+
+
+/*
+ * appendNamedArgument --
+ *
+ * Convenience routine for constructing parameters of the form:
+ * 'paraname', 'value'::type
+ */
+static void
+appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
+					const char *argval, const char *argtype)
+{
+	appendPQExpBufferStr(out, "\t");
+
+	appendStringLiteralAH(out, argname, fout);
+	appendPQExpBufferStr(out, ", ");
+
+	appendStringLiteralAH(out, argval, fout);
+	appendPQExpBuffer(out, "::%s", argtype);
+}
+
+/*
+ * appendRelStatsImport --
+ *
+ * Append a formatted pg_restore_relation_stats statement.
+ */
+static void
+appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	const char *sep = "";
+
+	if (PQntuples(res) == 0)
+		return;
+
+	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
+
+	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
+	{
+		const char *argname = rel_stats_arginfo[argno][0];
+		const char *argtype = rel_stats_arginfo[argno][1];
+		int			fieldno = PQfnumber(res, argname);
+
+		if (fieldno < 0)
+			pg_fatal("relation stats export query missing field '%s'",
+					 argname);
+
+		if (PQgetisnull(res, 0, fieldno))
+			continue;
+
+		appendPQExpBufferStr(out, sep);
+		appendNamedArgument(out, fout, argname, PQgetvalue(res, 0, fieldno), argtype);
+
+		sep = ",\n";
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+/*
+ * appendAttStatsImport --
+ *
+ * Append a series of formatted pg_restore_attribute_stats statements.
+ */
+static void
+appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	{
+		const char *sep = "";
+
+		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+		for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
+		{
+			const char *argname = att_stats_arginfo[argno][0];
+			const char *argtype = att_stats_arginfo[argno][1];
+			int			fieldno = PQfnumber(res, argname);
+
+			if (fieldno < 0)
+				pg_fatal("attribute stats export query missing field '%s'",
+						 argname);
+
+			if (PQgetisnull(res, rownum, fieldno))
+				continue;
+
+			appendPQExpBufferStr(out, sep);
+			appendNamedArgument(out, fout, argname, PQgetvalue(res, rownum, fieldno), argtype);
+			sep = ",\n";
+		}
+		appendPQExpBufferStr(out, "\n);\n");
+	}
+}
+
+/*
+ * Decide which section to use based on the relkind of the parent object.
+ *
+ * NB: materialized views may be postponed from SECTION_PRE_DATA to
+ * SECTION_POST_DATA to resolve some kinds of dependency problems. If so, the
+ * matview stats will also be postponed to SECTION_POST_DATA. See
+ * repairMatViewBoundaryMultiLoop().
+ */
+static teSection
+statisticsDumpSection(const RelStatsInfo *rsinfo)
+{
+	switch (rsinfo->relkind)
+	{
+		case RELKIND_RELATION:
+		case RELKIND_PARTITIONED_TABLE:
+		case RELKIND_MATVIEW:
+			return SECTION_DATA;
+		case RELKIND_INDEX:
+		case RELKIND_PARTITIONED_INDEX:
+			return SECTION_POST_DATA;
+		default:
+			pg_fatal("cannot dump statistics for relation kind '%c'",
+					 rsinfo->relkind);
+	}
+
+	return 0;				/* keep compiler quiet */
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	PGresult	   *res;
+	PQExpBuffer		query;
+	PQExpBuffer		out;
+	PQExpBuffer		tag;
+	DumpableObject *dobj = (DumpableObject *) &rsinfo->dobj;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", "STATISTICS DATA", fmtId(dobj->name));
+
+	query = createPQExpBuffer();
+	out = createPQExpBuffer();
+
+	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendRelStatsImport(out, fout, res);
+	PQclear(res);
+
+	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendAttStatsImport(out, fout, res);
+	PQclear(res);
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATISTICS DATA",
+							  .section = rsinfo->postponed_def ?
+								SECTION_POST_DATA :  statisticsDumpSection(rsinfo),
+							  .createStmt = out->data,
+							  .deps = dobj->dependencies,
+							  .nDeps = dobj->nDeps));
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(out);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -10744,6 +11103,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_SUBSCRIPTION_REL:
 			dumpSubscriptionTable(fout, (const SubRelInfo *) dobj);
 			break;
+		case DO_REL_STATS:
+			dumpRelationStats(fout, (const RelStatsInfo *) dobj);
+			break;
 		case DO_PRE_DATA_BOUNDARY:
 		case DO_POST_DATA_BOUNDARY:
 			/* never dumped, nothing to do */
@@ -17183,6 +17545,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 		free(indstatvalsarray);
 	}
 
+	/* Comments and stats share same .dep */
+
 	/* Dump Index Comments */
 	if (indxinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
 		dumpComment(fout, "INDEX", qindxname,
@@ -18970,6 +19334,16 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 				/* must come after the pre-data boundary */
 				addObjectDependency(dobj, preDataBound->dumpId);
 				break;
+			case DO_REL_STATS:
+				/* stats section varies by parent object type, DATA or POST */
+				if (statisticsDumpSection((RelStatsInfo *) dobj) == SECTION_DATA)
+				{
+					addObjectDependency(dobj, preDataBound->dumpId);
+					addObjectDependency(postDataBound, dobj->dumpId);
+				}
+				else
+					addObjectDependency(dobj, postDataBound->dumpId);
+				break;
 		}
 	}
 }
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index f62b564ed1..4edd88a54b 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -84,6 +84,7 @@ typedef enum
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION,
 	DO_SUBSCRIPTION_REL,		/* see note for SubRelInfo */
+	DO_REL_STATS,
 } DumpableObjectType;
 
 /*
@@ -109,6 +110,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
@@ -429,6 +431,13 @@ typedef struct _indexAttachInfo
 	IndxInfo   *partitionIdx;	/* link to index on partition */
 } IndexAttachInfo;
 
+typedef struct _relStatsInfo
+{
+	DumpableObject	dobj;
+	char			relkind;		/* 'r', 'v', 'c', etc */
+	bool			postponed_def;
+} RelStatsInfo;
+
 typedef struct _statsExtInfo
 {
 	DumpableObject dobj;
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index dc9a28924b..3a3602e3d2 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -801,11 +801,21 @@ repairMatViewBoundaryMultiLoop(DumpableObject *boundaryobj,
 {
 	/* remove boundary's dependency on object after it in loop */
 	removeObjectDependency(boundaryobj, nextobj->dumpId);
-	/* if that object is a matview, mark it as postponed into post-data */
+	/*
+	 * If that object is a matview or matview status, mark it as postponed into
+	 * post-data.
+	 */
 	if (nextobj->objType == DO_TABLE)
 	{
 		TableInfo  *nextinfo = (TableInfo *) nextobj;
 
+		if (nextinfo->relkind == RELKIND_MATVIEW)
+			nextinfo->postponed_def = true;
+	}
+	else if (nextobj->objType == DO_REL_STATS)
+	{
+		RelStatsInfo *nextinfo = (RelStatsInfo *) nextobj;
+
 		if (nextinfo->relkind == RELKIND_MATVIEW)
 			nextinfo->postponed_def = true;
 	}
@@ -1018,6 +1028,21 @@ repairDependencyLoop(DumpableObject **loop,
 					{
 						DumpableObject *nextobj;
 
+						nextobj = (j < nLoop - 1) ? loop[j + 1] : loop[0];
+						repairMatViewBoundaryMultiLoop(loop[j], nextobj);
+						return;
+					}
+				}
+			}
+			else if (loop[i]->objType == DO_REL_STATS &&
+					 ((RelStatsInfo *) loop[i])->relkind == RELKIND_MATVIEW)
+			{
+				for (j = 0; j < nLoop; j++)
+				{
+					if (loop[j]->objType == DO_POST_DATA_BOUNDARY)
+					{
+						DumpableObject *nextobj;
+
 						nextobj = (j < nLoop - 1) ? loop[j + 1] : loop[0];
 						repairMatViewBoundaryMultiLoop(loop[j], nextobj);
 						return;
@@ -1500,6 +1525,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "POST-DATA BOUNDARY  (ID %d)",
 					 obj->dumpId);
 			return;
+		case DO_REL_STATS:
+			snprintf(buf, bufsize,
+					 "RELATION STATISTICS FOR %s  (ID %d OID %u)",
+					 obj->name, obj->dumpId, obj->catId.oid);
+			return;
 	}
 	/* shouldn't get here */
 	snprintf(buf, bufsize,
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 396f79781c..7effb70490 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -103,6 +103,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -172,6 +173,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -451,6 +453,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -666,6 +670,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 88ae39d938..9586bd032c 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -63,6 +63,9 @@ main(int argc, char **argv)
 	int			numWorkers = 1;
 	Archive    *AH;
 	char	   *inputFileSpec;
+	bool		data_only = false;
+	bool		schema_only = false;
+	bool		statistics_only = false;
 	static int	disable_triggers = 0;
 	static int	enable_row_security = 0;
 	static int	if_exists = 0;
@@ -71,12 +74,13 @@ main(int argc, char **argv)
 	static int	outputNoTablespaces = 0;
 	static int	use_setsessauth = 0;
 	static int	no_comments = 0;
+	static int	no_data = 0;
 	static int	no_publications = 0;
+	static int	no_schema = 0;
 	static int	no_security_labels = 0;
+	static int  no_statistics = 0;
 	static int	no_subscriptions = 0;
 	static int	strict_names = 0;
-	bool		data_only = false;
-	bool		schema_only = false;
 
 	struct option cmdopts[] = {
 		{"clean", 0, NULL, 'c'},
@@ -108,6 +112,7 @@ main(int argc, char **argv)
 		{"username", 1, NULL, 'U'},
 		{"verbose", 0, NULL, 'v'},
 		{"single-transaction", 0, NULL, '1'},
+		{"statistics-only", no_argument, NULL, 'P'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -124,9 +129,12 @@ main(int argc, char **argv)
 		{"transaction-size", required_argument, NULL, 5},
 		{"use-set-session-authorization", no_argument, &use_setsessauth, 1},
 		{"no-comments", no_argument, &no_comments, 1},
+		{"no-data", no_argument, &no_data, 1},
 		{"no-publications", no_argument, &no_publications, 1},
+		{"no-schema", no_argument, &no_schema, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"filter", required_argument, NULL, 4},
 
 		{NULL, 0, NULL, 0}
@@ -271,6 +279,10 @@ main(int argc, char **argv)
 				opts->aclsSkip = 1;
 				break;
 
+			case 'X':			/* Restore statistics only */
+				statistics_only = true;
+				break;
+
 			case '1':			/* Restore data in a single transaction */
 				opts->single_txn = true;
 				opts->exit_on_error = true;
@@ -343,6 +355,10 @@ main(int argc, char **argv)
 
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
+	if (schema_only && statistics_only)
+		pg_fatal("options -s/--schema-only and -X/--statistics-only cannot be used together");
 
 	if (data_only && opts->dropSchema)
 		pg_fatal("options -c/--clean and -a/--data-only cannot be used together");
@@ -362,8 +378,9 @@ main(int argc, char **argv)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
 	/* set derivative flags */
-	opts->dumpSchema = (!data_only);
-	opts->dumpData = (!schema_only);
+	opts->dumpData = data_only || (!no_data && !schema_only && !statistics_only);
+	opts->dumpSchema = schema_only || (!no_schema && !data_only && !statistics_only);
+	opts->dumpStatistics = statistics_only || (!no_statistics && !data_only && !schema_only);
 
 	opts->disable_triggers = disable_triggers;
 	opts->enable_row_security = enable_row_security;
@@ -375,6 +392,8 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
+	opts->no_data = no_data;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
@@ -484,6 +503,7 @@ usage(const char *progname)
 	printf(_("  -t, --table=NAME             restore named relation (table, view, etc.)\n"));
 	printf(_("  -T, --trigger=NAME           restore named trigger\n"));
 	printf(_("  -x, --no-privileges          skip restoration of access privileges (grant/revoke)\n"));
+	printf(_("  -X, --statistics-only        restore only the statistics, not schema or data\n"));
 	printf(_("  -1, --single-transaction     restore as a single transaction\n"));
 	printf(_("  --disable-triggers           disable triggers during data-only restore\n"));
 	printf(_("  --enable-row-security        enable row security\n"));
@@ -491,10 +511,14 @@ usage(const char *progname)
 			 "                               in FILENAME\n"));
 	printf(_("  --if-exists                  use IF EXISTS when dropping objects\n"));
 	printf(_("  --no-comments                do not restore comment commands\n"));
+	printf(_("  --no-data                    do not restore data\n"));
 	printf(_("  --no-data-for-failed-tables  do not restore data of tables that could not be\n"
 			 "                               created\n"));
 	printf(_("  --no-publications            do not restore publications\n"));
+	printf(_("  --no-schema                  do not restore schema\n"));
 	printf(_("  --no-security-labels         do not restore security labels\n"));
+	printf(_("  --no-statistics              do not restore statistics\n"));
+	/* This hack is only needed in a data-only restore */
 	printf(_("  --no-subscriptions           do not restore subscriptions\n"));
 	printf(_("  --no-table-access-method     do not restore table access methods\n"));
 	printf(_("  --no-tablespaces             do not restore tablespace assignments\n"));
diff --git a/src/bin/pg_dump/t/001_basic.pl b/src/bin/pg_dump/t/001_basic.pl
index 214240f1ae..f29da06ed2 100644
--- a/src/bin/pg_dump/t/001_basic.pl
+++ b/src/bin/pg_dump/t/001_basic.pl
@@ -50,12 +50,30 @@ command_fails_like(
 	'pg_dump: options -s/--schema-only and -a/--data-only cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '-s', '-X' ],
+	qr/\Qpg_dump: error: options -s\/--schema-only and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: error: options -s/--schema-only and -X/--statistics-only cannot be used together'
+);
+
+command_fails_like(
+	[ 'pg_dump', '-a', '-X' ],
+	qr/\Qpg_dump: error: options -a\/--data-only and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: error: options -a/--data-only and -X/--statistics-only cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-s', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: options -s\/--schema-only and --include-foreign-data cannot be used together\E/,
 	'pg_dump: options -s/--schema-only and --include-foreign-data cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '--statistics-only', '--no-statistics' ],
+	qr/\Qpg_dump: error: options -X\/--statistics-only and --no-statistics cannot be used together\E/,
+	'pg_dump: options -X\/--statistics-only and --no-statistics cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-j2', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: option --include-foreign-data is not supported with parallel backup\E/,
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index bf65d44b94..737b184ea9 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -66,7 +66,7 @@ my %pgdump_runs = (
 			'--format=custom',
 			"--file=$tempdir/binary_upgrade.dump",
 			'-w',
-			'--schema-only',
+			'--no-data',
 			'--binary-upgrade',
 			'-d', 'postgres',    # alternative way to specify database
 		],
@@ -645,7 +645,19 @@ my %pgdump_runs = (
 
 			'--schema=dump_test', '-b', '-B', '--no-sync', 'postgres',
 		],
-	},);
+	},
+	no_statistics => {
+		dump_cmd => [
+			'pg_dump', "--file=$tempdir/no_statistics.sql",
+			'--no-sync', '--no-statistics', 'postgres',
+		],
+	},
+	no_schema => {
+		dump_cmd => [
+			'pg_dump', "--file=$tempdir/no_schema.sql",
+			'--no-sync', '--no-schema', 'postgres',
+		],
+	});
 
 ###############################################################
 # Definition of the tests to run.
@@ -711,6 +723,7 @@ my %full_runs = (
 	no_large_objects => 1,
 	no_owner => 1,
 	no_privs => 1,
+	no_statistics => 1,
 	no_table_access_method => 1,
 	pg_dumpall_dbprivs => 1,
 	pg_dumpall_exclude => 1,
@@ -912,6 +925,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1325,6 +1339,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1346,6 +1361,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1367,6 +1383,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1533,6 +1550,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1, 
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1686,6 +1704,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			only_dump_test_table => 1,
 			section_data => 1,
 		},
@@ -1713,6 +1732,7 @@ my %tests = (
 			data_only => 1,
 			exclude_test_table => 1,
 			exclude_test_table_data => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1733,7 +1753,10 @@ my %tests = (
 			\QCOPY dump_test.fk_reference_test_table (col1) FROM stdin;\E
 			\n(?:\d\n){5}\\\.\n
 			/xms,
-		like => { data_only => 1, },
+		like => {
+			data_only => 1,
+			no_schema => 1,
+		},
 	},
 
 	'COPY test_second_table' => {
@@ -1749,6 +1772,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1771,6 +1795,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1794,6 +1819,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1816,6 +1842,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1838,6 +1865,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -3234,6 +3262,7 @@ my %tests = (
 		like => {
 			%full_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 			only_dump_test_schema => 1,
 			test_schema_plus_large_objects => 1,
@@ -3404,6 +3433,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			only_dump_measurement => 1,
 			section_data => 1,
 			only_dump_test_schema => 1,
@@ -4286,6 +4316,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 			binary_upgrade => 1,
@@ -4586,6 +4617,30 @@ my %tests = (
 		},
 	},
 
+	#
+	# Table statistics should go in section=data.
+	# Materialized view statistics should go in section=post-data.
+	#
+	'statistics_import' => {
+	create_sql => '
+		CREATE TABLE dump_test.has_stats
+		AS SELECT g.g AS x, g.g / 2 AS y FROM generate_series(1,100) AS g(g);
+		CREATE TABLE dump_test.has_stats_mv AS SELECT * FROM dump_test.has_stats;
+		ANALYZE dump_test.has_stats, dump_test.has_stats_mv;',
+	regexp => qr/pg_catalog.pg_restore_attribute_stats/,
+	like => {
+		%full_runs,
+		%dump_test_schema_runs,
+		section_data => 1,
+		},
+	unlike => {
+		exclude_dump_test_schema => 1,
+		no_statistics => 1,
+		only_dump_measurement => 1,
+		schema_only => 1,
+		},
+	},
+
 	# CREATE TABLE with partitioned table and various AMs.  One
 	# partition uses the same default as the parent, and a second
 	# uses its own AM.
diff --git a/src/bin/pg_upgrade/dump.c b/src/bin/pg_upgrade/dump.c
index 8ce0fa3020..a29cd2cca9 100644
--- a/src/bin/pg_upgrade/dump.c
+++ b/src/bin/pg_upgrade/dump.c
@@ -21,10 +21,11 @@ generate_old_dump(void)
 
 	/* run new pg_dumpall binary for globals */
 	exec_prog(UTILITY_LOG_FILE, NULL, true, true,
-			  "\"%s/pg_dumpall\" %s --globals-only --quote-all-identifiers "
+			  "\"%s/pg_dumpall\" %s %s --globals-only --quote-all-identifiers "
 			  "--binary-upgrade %s --no-sync -f \"%s/%s\"",
 			  new_cluster.bindir, cluster_conn_opts(&old_cluster),
 			  log_opts.verbose ? "--verbose" : "",
+			  user_opts.do_statistics ? "" : "--no-statistics",
 			  log_opts.dumpdir,
 			  GLOBALS_DUMP_FILE);
 	check_ok();
@@ -52,10 +53,11 @@ generate_old_dump(void)
 		snprintf(log_file_name, sizeof(log_file_name), DB_DUMP_LOG_FILE_MASK, old_db->db_oid);
 
 		parallel_exec_prog(log_file_name, NULL,
-						   "\"%s/pg_dump\" %s --schema-only --quote-all-identifiers "
+						   "\"%s/pg_dump\" %s --no-data %s --quote-all-identifiers "
 						   "--binary-upgrade --format=custom %s --no-sync --file=\"%s/%s\" %s",
 						   new_cluster.bindir, cluster_conn_opts(&old_cluster),
 						   log_opts.verbose ? "--verbose" : "",
+						   user_opts.do_statistics ? "" : "--no-statistics",
 						   log_opts.dumpdir,
 						   sql_file_name, escaped_connstr.data);
 
diff --git a/src/bin/pg_upgrade/option.c b/src/bin/pg_upgrade/option.c
index 108eb7a1ba..3b6c7ec994 100644
--- a/src/bin/pg_upgrade/option.c
+++ b/src/bin/pg_upgrade/option.c
@@ -60,6 +60,8 @@ parseCommandLine(int argc, char *argv[])
 		{"copy", no_argument, NULL, 2},
 		{"copy-file-range", no_argument, NULL, 3},
 		{"sync-method", required_argument, NULL, 4},
+		{"with-statistics", no_argument, NULL, 5},
+		{"no-statistics", no_argument, NULL, 6},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -70,6 +72,7 @@ parseCommandLine(int argc, char *argv[])
 
 	user_opts.do_sync = true;
 	user_opts.transfer_mode = TRANSFER_MODE_COPY;
+	user_opts.do_statistics = true;
 
 	os_info.progname = get_progname(argv[0]);
 
@@ -212,6 +215,13 @@ parseCommandLine(int argc, char *argv[])
 				user_opts.sync_method = pg_strdup(optarg);
 				break;
 
+			case 5:
+				user_opts.do_statistics = true;
+				break;
+			case 6:
+				user_opts.do_statistics = false;
+				break;
+
 			default:
 				fprintf(stderr, _("Try \"%s --help\" for more information.\n"),
 						os_info.progname);
@@ -306,7 +316,9 @@ usage(void)
 	printf(_("  --clone                       clone instead of copying files to new cluster\n"));
 	printf(_("  --copy                        copy files to new cluster (default)\n"));
 	printf(_("  --copy-file-range             copy files to new cluster with copy_file_range\n"));
+	printf(_("  --no-statistics               do not import statistics from old cluster\n"));
 	printf(_("  --sync-method=METHOD          set method for syncing files to disk\n"));
+	printf(_("  --with-statistics             import statistics from old cluster (default)\n"));
 	printf(_("  -?, --help                    show this help, then exit\n"));
 	printf(_("\n"
 			 "Before running pg_upgrade you must:\n"
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index 0cdd675e4f..3fe111fbde 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -327,6 +327,7 @@ typedef struct
 	int			jobs;			/* number of processes/threads to use */
 	char	   *socketdir;		/* directory to use for Unix sockets */
 	char	   *sync_method;
+	bool		do_statistics;	/* carry over statistics from old cluster */
 } UserOpts;
 
 typedef struct
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index d66e901f51..5e58f24d21 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -123,7 +123,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Dump only the data, not the schema (data definitions).
+        Dump only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are dumped.
        </para>
 
@@ -141,13 +141,11 @@ PostgreSQL documentation
       <listitem>
        <para>
         Include large objects in the dump.  This is the default behavior
-        except when <option>--schema</option>, <option>--table</option>, or
-        <option>--schema-only</option> is specified.  The <option>-b</option>
-        switch is therefore only useful to add large objects to dumps
-        where a specific schema or table has been requested.  Note that
-        large objects are considered data and therefore will be included when
-        <option>--data-only</option> is used, but not
-        when <option>--schema-only</option> is.
+        except when <option>--schema</option>, <option>--table</option>,
+        <option>--schema-only</option>, or <option>--statistics-only</option>, or
+        <option>--no-data</option> is specified.  The <option>-b</option>
+        switch is therefore only useful to add large objects to dumps where a
+        specific schema or table has been requested.
        </para>
       </listitem>
      </varlistentry>
@@ -516,10 +514,11 @@ PostgreSQL documentation
       <term><option>--schema-only</option></term>
       <listitem>
        <para>
-        Dump only the object definitions (schema), not data.
+        Dump only the object definitions (schema), not data or statistics.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive to <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
         <option>--section=pre-data --section=post-data</option>.
@@ -652,6 +651,18 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Dump only the statistics, not the schema (data definitions) or data.
+        Statistics for tables, materialized views, and indexes are dumped.
+       </para>
+
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-Z <replaceable class="parameter">level</replaceable></option></term>
       <term><option>-Z <replaceable class="parameter">method</replaceable></option>[:<replaceable>detail</replaceable>]</term>
@@ -824,16 +835,17 @@ PostgreSQL documentation
       <term><option>--exclude-table-data=<replaceable class="parameter">pattern</replaceable></option></term>
       <listitem>
        <para>
-        Do not dump data for any tables matching <replaceable
+        Do not dump data or statistics for any tables matching <replaceable
         class="parameter">pattern</replaceable>. The pattern is
         interpreted according to the same rules as for <option>-t</option>.
         <option>--exclude-table-data</option> can be given more than once to
-        exclude tables matching any of several patterns. This option is
-        useful when you need the definition of a particular table even
-        though you do not need the data in it.
+        exclude tables matching any of several patterns. This option is useful
+        when you need the definition of a particular table even though you do
+        not need the data in it.
        </para>
        <para>
-        To exclude data for all tables in the database, see <option>--schema-only</option>.
+        To exclude data for all tables in the database, see <option>--schema-only</option>
+        or <option>--statistics-only</option>.
        </para>
       </listitem>
      </varlistentry>
@@ -1080,6 +1092,15 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-data</option></term>
+      <listitem>
+       <para>
+        Do not dump data.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-publications</option></term>
       <listitem>
@@ -1098,6 +1119,24 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-schema</option></term>
+      <listitem>
+       <para>
+        Do not dump schema (data definitions).
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_dumpall.sgml b/doc/src/sgml/ref/pg_dumpall.sgml
index 014f279258..d423153a93 100644
--- a/doc/src/sgml/ref/pg_dumpall.sgml
+++ b/doc/src/sgml/ref/pg_dumpall.sgml
@@ -265,6 +265,17 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Dump only the statistics, not the schema (data definitions) or data.
+        Statistics for tables, materialized views, and indexes are dumped.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--binary-upgrade</option></term>
       <listitem>
@@ -422,6 +433,15 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-data</option></term>
+      <listitem>
+       <para>
+        Do not dump data.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-publications</option></term>
       <listitem>
@@ -447,6 +467,15 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-schema</option></term>
+      <listitem>
+       <para>
+        Do not dump schema (data definitions).
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-security-labels</option></term>
       <listitem>
@@ -456,6 +485,15 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index b8b27e1719..22c3c118ad 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -94,7 +94,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Restore only the data, not the schema (data definitions).
+        Restore only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are restored,
         if present in the archive.
        </para>
@@ -483,10 +483,11 @@ PostgreSQL documentation
         to the extent that schema entries are present in the archive.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive of <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
-        <option>--section=pre-data --section=post-data</option>.
+        <option>--section=pre-data --section=post-data --no-statistics</option>.
        </para>
        <para>
         (Do not confuse this with the <option>--schema</option> option, which
@@ -599,6 +600,20 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Restore only the statistics, not schema (data definitions) or data.
+       </para>
+       <para>
+        (Do not confuse this with the <option>--schema</option> option, which
+        uses the word <quote>schema</quote> in a different meaning.)
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-1</option></term>
       <term><option>--single-transaction</option></term>
@@ -681,6 +696,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-data</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore data, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-data-for-failed-tables</option></term>
       <listitem>
@@ -713,6 +738,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-schema</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore schema (data definitions), even if
+        the archive contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-security-labels</option></term>
       <listitem>
@@ -723,6 +758,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore statistics, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pgupgrade.sgml b/doc/src/sgml/ref/pgupgrade.sgml
index 4777381dac..64a1ebd613 100644
--- a/doc/src/sgml/ref/pgupgrade.sgml
+++ b/doc/src/sgml/ref/pgupgrade.sgml
@@ -145,6 +145,24 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--with-statistics</option></term>
+      <listitem>
+       <para>
+        Restore statistics from the old cluster into the new cluster.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not restore statistics from the old cluster into the new cluster.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-o</option> <replaceable class="parameter">options</replaceable></term>
       <term><option>--old-options</option> <replaceable class="parameter">options</replaceable></term>
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index d5aa5c295a..9212e6dea9 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2399,6 +2399,7 @@ RelMapFile
 RelMapping
 RelOptInfo
 RelOptKind
+RelStatsInfo
 RelToCheck
 RelToCluster
 RelabelType
-- 
2.48.1

#296Michael Paquier
michael@paquier.xyz
In reply to: Corey Huinker (#295)
Re: Statistics Import and Export

On Tue, Jan 21, 2025 at 10:21:51PM -0500, Corey Huinker wrote:

After some research, I think that we should treat partitioned indexes like
we were before, and just handle the existing special case for regular
indexes.

Hmm, why? Sounds strange to me to not have the same locking semantics
for the partitioned parts, and this even if partitioned indexes don't
have stats that can be manipulated in relation_stats.c as far as I
can see. These stats APIs are designed to be permissive as Jeff says.
Having a better locking from the start makes the whole picture more
consistent, while opening the door for actually setting real stat
numbers for partitioned indexes (if some make sense, at some point)?

That patch is attached below, along with a few updates to pg_dump.
There are more changes in the works addressing your earlier feedback, but
those seem to break as many things as they fix, so I'm still working on
them.

 	if ((rel->rd_rel->relkind == RELKIND_INDEX
-		 || (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+		|| (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
 		&& (rel->rd_indexprs != NIL)
 		&& (rel->rd_index->indkey.values[attnum - 1] == 0))
 	{

Some noise.

+/* Test for proper locking */

Could it be possible to not mix the style of the comments in
stats_import.sql?

stats_utils.c does not need storage/lockdefs.h. The order of
catalog/index.h is incorrect.
--
Michael

#297jian he
jian.universality@gmail.com
In reply to: Jeff Davis (#284)
Re: Statistics Import and Export

On Tue, Jan 21, 2025 at 7:31 AM Jeff Davis <pgsql@j-davis.com> wrote:

On Mon, 2025-01-20 at 16:45 -0500, Corey Huinker wrote:

What I struggle to understand is how that purpose isn't served better
by statistics being in SECTION_NONE like COMMENTs are, so that they
are imported immediately after the object that they reference.

Tom, you expressed the strongest opinions on this point, can you expand
a bit?

If I understand correctly:

* We strongly want stats to be exported by default[1].

* Adding a SECTION_STATS could work, but would be non-trivial and might
break expectations about the set of sections available[2].

* SECTION_NONE doesn't seem right. There would be no way to get the
stats using --section. Also, if there is no section boundary for the
stats, then couldn't they appear in a surprising order?

* I'm not sure about placing stats in SECTION_POST_DATA. That doesn't
seem terrible to me, but not great either.

index is on SECTION_POST_DATA.
To dump all the statistics, we have to go through SECTION_POST_DATA.
place it there would be more convenient.

Tomas Vondra also mentioned this on [1]/messages/by-id/bf724b21-914a-4497-84e3-49944f9776f6@enterprisedb.com
[1]: /messages/by-id/bf724b21-914a-4497-84e3-49944f9776f6@enterprisedb.com

* I'm also not 100% sure about the flags. The default should dump the
stats, of course. And I like the idea of allowing any combination of
schema, data and stats to be exported. But that leaves a wrinkle for --
data-only, which (as of v38) does not dump stats, because stats are a
third kind of thing. Perhaps stats should be expressed as a subtype of
data somehow, but I'm not sure exactly how.

if we have --data-only, --schema-only, --statistics-only, three options, then
--data-only also dump statistics would be unintuitive?

#298Corey Huinker
corey.huinker@gmail.com
In reply to: jian he (#297)
Re: Statistics Import and Export

On Mon, Jan 27, 2025 at 9:05 AM jian he <jian.universality@gmail.com> wrote:

On Tue, Jan 21, 2025 at 7:31 AM Jeff Davis <pgsql@j-davis.com> wrote:

On Mon, 2025-01-20 at 16:45 -0500, Corey Huinker wrote:

What I struggle to understand is how that purpose isn't served better
by statistics being in SECTION_NONE like COMMENTs are, so that they
are imported immediately after the object that they reference.

Tom, you expressed the strongest opinions on this point, can you expand
a bit?

If I understand correctly:

* We strongly want stats to be exported by default[1].

* Adding a SECTION_STATS could work, but would be non-trivial and might
break expectations about the set of sections available[2].

* SECTION_NONE doesn't seem right. There would be no way to get the
stats using --section. Also, if there is no section boundary for the
stats, then couldn't they appear in a surprising order?

* I'm not sure about placing stats in SECTION_POST_DATA. That doesn't
seem terrible to me, but not great either.

index is on SECTION_POST_DATA.
To dump all the statistics, we have to go through SECTION_POST_DATA.
place it there would be more convenient.

That would be the simpler solution, but those statistics may come in handy
for refreshing mviews, so some may want table stats to stay in SECTION_DATA.

Tomas Vondra also mentioned this on [1]
[1]
/messages/by-id/bf724b21-914a-4497-84e3-49944f9776f6@enterprisedb.com

* I'm also not 100% sure about the flags. The default should dump the
stats, of course. And I like the idea of allowing any combination of
schema, data and stats to be exported. But that leaves a wrinkle for --
data-only, which (as of v38) does not dump stats, because stats are a
third kind of thing. Perhaps stats should be expressed as a subtype of
data somehow, but I'm not sure exactly how.

if we have --data-only, --schema-only, --statistics-only, three options,
then
--data-only also dump statistics would be unintuitive?

Yeah, I think the codebase and the user flags both have confusing bits
where the not-wanting of one type of thing was specified by only-wanting
the other thing, and those choices fall apart when the binary becomes
trinary.

#299Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#286)
Re: Statistics Import and Export

On Sat, Jan 25, 2025 at 10:02 AM Corey Huinker <corey.huinker@gmail.com>
wrote:

Fixed. Holding off on posting updated patch pending decision on what's the

best thing to do with partitioned indexes.

Though I was able to get it to work multiple ways, the one that seems to
make the most sense given Michael and Jeff's feedback is to handle
partitioned indexes, do the ACL checks against the underlying table just
like indexes, but take a ShareUpdateExclusiveLock on the partitioned index
as well, because it doesn't have the same special case
in check_inplace_rel_lock() that regular indexes do.

In the future, if check_inplace_rel_lock() changes its special case to
include partitioned indexes, then this code can get marginal simpler.

New patchset, no changes to 0002 as work continues there.

Thought I sent this to the list, but apparently I only sent to Michael. The
changes referenced are in v45, already posted to the list.

#300Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#298)
2 attachment(s)
Re: Statistics Import and Export

On Mon, Jan 27, 2025 at 11:09 AM Corey Huinker <corey.huinker@gmail.com>
wrote:

On Mon, Jan 27, 2025 at 9:05 AM jian he <jian.universality@gmail.com>
wrote:

On Tue, Jan 21, 2025 at 7:31 AM Jeff Davis <pgsql@j-davis.com> wrote:

On Mon, 2025-01-20 at 16:45 -0500, Corey Huinker wrote:

What I struggle to understand is how that purpose isn't served better
by statistics being in SECTION_NONE like COMMENTs are, so that they
are imported immediately after the object that they reference.

Tom, you expressed the strongest opinions on this point, can you expand
a bit?

If I understand correctly:

* We strongly want stats to be exported by default[1].

* Adding a SECTION_STATS could work, but would be non-trivial and might
break expectations about the set of sections available[2].

* SECTION_NONE doesn't seem right. There would be no way to get the
stats using --section. Also, if there is no section boundary for the
stats, then couldn't they appear in a surprising order?

* I'm not sure about placing stats in SECTION_POST_DATA. That doesn't
seem terrible to me, but not great either.

index is on SECTION_POST_DATA.
To dump all the statistics, we have to go through SECTION_POST_DATA.
place it there would be more convenient.

That would be the simpler solution, but those statistics may come in handy
for refreshing mviews, so some may want table stats to stay in SECTION_DATA.

Tomas Vondra also mentioned this on [1]
[1]
/messages/by-id/bf724b21-914a-4497-84e3-49944f9776f6@enterprisedb.com

* I'm also not 100% sure about the flags. The default should dump the
stats, of course. And I like the idea of allowing any combination of
schema, data and stats to be exported. But that leaves a wrinkle for --
data-only, which (as of v38) does not dump stats, because stats are a
third kind of thing. Perhaps stats should be expressed as a subtype of
data somehow, but I'm not sure exactly how.

if we have --data-only, --schema-only, --statistics-only, three options,
then
--data-only also dump statistics would be unintuitive?

Yeah, I think the codebase and the user flags both have confusing bits
where the not-wanting of one type of thing was specified by only-wanting
the other thing, and those choices fall apart when the binary becomes
trinary.

Seems I also replied only to Micahel with the v45 patch.

And here's an update to the pg_dump code itself. This currently has failing
TAP tests for statistics in the custom and dir formats, but is working
otherwise.

Attachments:

v45-0001-Lock-table-first-when-setting-index-relation-sta.patchtext/x-patch; charset=US-ASCII; name=v45-0001-Lock-table-first-when-setting-index-relation-sta.patchDownload
From a2948c851191977af6fe22f79a13589204a20df8 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Tue, 21 Jan 2025 11:52:58 -0500
Subject: [PATCH v45 1/2] Lock table first when setting index relation
 statistics.

Jian He reported [1] a missing lock relation bug in
pg_restore_relation_stats when restoring stats to an index.

This fix follows the steps for proper locking prior to an inplace update
of a relation as specified in aac2c9b4fde8, and mimics the locking
behavior of the analyze command, as well as correctly doing the ACL
checks against the underlying relation of an index rather than the index
itself.

There is no special case for partitioned indexes, so while we want to
the ACL checks against the underlying relation, we need to take out
the more restrictive ShareUpdateExclusiveLock on the partitioned index.

[1] https://www.postgresql.org/message-id/CACJufxGreTY7qsCV8%2BBkuv0p5SXGTScgh%3DD%2BDq6%3D%2B_%3DXTp7FWg%40mail.gmail.com
---
 src/backend/statistics/stat_utils.c        | 45 +++++++++++++++---
 src/test/regress/expected/stats_import.out | 53 ++++++++++++++++++++++
 src/test/regress/sql/stats_import.sql      | 36 +++++++++++++++
 3 files changed, 128 insertions(+), 6 deletions(-)

diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c
index 0d446f55b0..f87007e72c 100644
--- a/src/backend/statistics/stat_utils.c
+++ b/src/backend/statistics/stat_utils.c
@@ -17,13 +17,16 @@
 #include "postgres.h"
 
 #include "access/relation.h"
+#include "catalog/index.h"
 #include "catalog/pg_database.h"
 #include "funcapi.h"
 #include "miscadmin.h"
 #include "statistics/stat_utils.h"
+#include "storage/lmgr.h"
 #include "utils/acl.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
+#include "utils/lsyscache.h"
 #include "utils/rel.h"
 
 /*
@@ -126,18 +129,45 @@ stats_check_arg_pair(FunctionCallInfo fcinfo,
 void
 stats_lock_check_privileges(Oid reloid)
 {
-	Relation	rel = relation_open(reloid, ShareUpdateExclusiveLock);
-	const char	relkind = rel->rd_rel->relkind;
+	Relation	rel;
+	Oid			relation_oid = reloid;
+	Oid			index_oid = InvalidOid;
+	LOCKMODE	index_lockmode = AccessShareLock;
 
-	/* All of the types that can be used with ANALYZE, plus indexes */
-	switch (relkind)
+	/*
+	 * For indexes, we follow what do_analyze_rel() does so as to avoid any
+	 * deadlocks with analyze/vacuum, which is to take out a
+	 * ShareUpdateExclusive on table/matview first and then take an
+	 * AccessShareLock on the index itself. See check_inplace_rel_lock()
+	 * to see how this special case is implemented.
+	 *
+	 * Partitioned indexes do not have an exception in check_inplace_rel_lock(),
+	 * so we want to take a ShareUpdateExclusive lock there instead.
+	 */
+	switch(get_rel_relkind(reloid))
 	{
-		case RELKIND_RELATION:
 		case RELKIND_INDEX:
+			relation_oid = IndexGetRelation(reloid, false);
+			index_oid = reloid;
+			break;
+		case RELKIND_PARTITIONED_INDEX:
+			relation_oid = IndexGetRelation(reloid, false);
+			index_oid = reloid;
+			index_lockmode = ShareUpdateExclusiveLock;
+			break;
+		default:
+			break;
+	}
+
+	rel = relation_open(relation_oid, ShareUpdateExclusiveLock);
+
+	switch (rel->rd_rel->relkind)
+	{
+		/* All of the types that can be used with ANALYZE */
+		case RELKIND_RELATION:
 		case RELKIND_MATVIEW:
 		case RELKIND_FOREIGN_TABLE:
 		case RELKIND_PARTITIONED_TABLE:
-		case RELKIND_PARTITIONED_INDEX:
 			break;
 		default:
 			ereport(ERROR,
@@ -164,6 +194,9 @@ stats_lock_check_privileges(Oid reloid)
 						   NameStr(rel->rd_rel->relname));
 	}
 
+	if (OidIsValid(index_oid))
+		LockRelationOid(index_oid, index_lockmode);
+
 	relation_close(rel, NoLock);
 }
 
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index fb50da1cd8..6cd584da68 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -85,6 +85,26 @@ WHERE oid = 'stats_import.test'::regclass;
        17 |       400 |             4
 (1 row)
 
+CREATE INDEX test_i ON stats_import.test(id);
+-- regular indexes have special case locking rules
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test_i'::regclass,
+        relpages => 18::integer);
+ pg_set_relation_stats 
+-----------------------
+ 
+(1 row)
+
+SELECT
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.test_i'::regclass,
+        'relpages', 19::integer );
+ pg_restore_relation_stats 
+---------------------------
+ t
+(1 row)
+
 -- positional arguments
 SELECT
     pg_catalog.pg_set_relation_stats(
@@ -182,6 +202,7 @@ CREATE TABLE stats_import.part_child_1
   PARTITION OF stats_import.part_parent
   FOR VALUES FROM (0) TO (10)
   WITH (autovacuum_enabled = false);
+CREATE INDEX part_parent_i ON stats_import.part_parent(i);
 ANALYZE stats_import.part_parent;
 SELECT relpages
 FROM pg_class
@@ -202,6 +223,25 @@ SELECT
  
 (1 row)
 
+-- Partitioned indexes aren't analyzed but it is possible to set stats.
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.part_parent_i'::regclass,
+        relpages => 2::integer);
+ pg_set_relation_stats 
+-----------------------
+ 
+(1 row)
+
+SELECT
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.part_parent_i'::regclass,
+        'relpages', 2::integer);
+ pg_restore_relation_stats 
+---------------------------
+ t
+(1 row)
+
 -- nothing stops us from setting it to -1
 SELECT
     pg_catalog.pg_set_relation_stats(
@@ -1414,6 +1454,19 @@ SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import.complex_type,
 UNION ALL
 SELECT 4, 'four', NULL, int4range(0,100), NULL;
 CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
+-- Test for proper locking
+SELECT * FROM pg_catalog.pg_restore_relation_stats(
+    'relation', 'stats_import.is_odd'::regclass,
+    'version', '180000'::integer,
+    'relpages', '11'::integer,
+    'reltuples', '10000'::real,
+    'relallvisible', '0'::integer
+);
+ pg_restore_relation_stats 
+---------------------------
+ t
+(1 row)
+
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 CREATE TABLE stats_import.test_clone ( LIKE stats_import.test )
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index d3058bf8f6..23e85fc6ba 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -64,6 +64,19 @@ SELECT relpages, reltuples, relallvisible
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
+CREATE INDEX test_i ON stats_import.test(id);
+
+-- regular indexes have special case locking rules
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test_i'::regclass,
+        relpages => 18::integer);
+
+SELECT
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.test_i'::regclass,
+        'relpages', 19::integer );
+
 -- positional arguments
 SELECT
     pg_catalog.pg_set_relation_stats(
@@ -127,6 +140,8 @@ CREATE TABLE stats_import.part_child_1
   FOR VALUES FROM (0) TO (10)
   WITH (autovacuum_enabled = false);
 
+CREATE INDEX part_parent_i ON stats_import.part_parent(i);
+
 ANALYZE stats_import.part_parent;
 
 SELECT relpages
@@ -140,6 +155,17 @@ SELECT
         relation => 'stats_import.part_parent'::regclass,
         relpages => 2::integer);
 
+-- Partitioned indexes aren't analyzed but it is possible to set stats.
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.part_parent_i'::regclass,
+        relpages => 2::integer);
+
+SELECT
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.part_parent_i'::regclass,
+        'relpages', 2::integer);
+
 -- nothing stops us from setting it to -1
 SELECT
     pg_catalog.pg_set_relation_stats(
@@ -1062,6 +1088,16 @@ SELECT 4, 'four', NULL, int4range(0,100), NULL;
 
 CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
 
+
+-- Test for proper locking
+SELECT * FROM pg_catalog.pg_restore_relation_stats(
+    'relation', 'stats_import.is_odd'::regclass,
+    'version', '180000'::integer,
+    'relpages', '11'::integer,
+    'reltuples', '10000'::real,
+    'relallvisible', '0'::integer
+);
+
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 
-- 
2.48.1

v45-0002-Enable-dumping-of-table-index-stats-in-pg_dump.patchtext/x-patch; charset=US-ASCII; name=v45-0002-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From 8b25dfd095dba60c78434ec812e06f3aad71e2f1 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH v45 2/2] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will generate a statement that
calls pg_set_relation_stats(), and it will generate a series of
statements that call pg_set_attribute_stats(), one per attribute. These
statements will restore the statistics of the current system onto the
destination system.

Adds the command-line options -X / --statistics-only, which are mutually
exclusive to --schema-only and --data-only.

Statistics are not dumped when --schema-only is specified, except during
a binary upgrade.

As is the pattern with pg_dump options, statistics can be disabled using
--no-statistics.

Table statistics are dumped in the data section. This is true even if
dumping stats in a binary upgrade. Index and Materialized View
statistics are dumped in the post-data section.

Add --no-data option.

This option is useful for situations where someone wishes to test
query plans from a production database without copying production data.

This also makes the corresponding change to the simulated pg_upgrade in
the TAP tests for pg_dump.

This checks that dumping statistics is now the default, and that
--no-statistics will suppress statistics.

Add --no-schema option to pg_dump, etc.

Previously, users could use --data-only when they wanted to suppress
schema from a dump. However, that no longer makes sense now that the
data/schema binary has become the data/schema/statistics trinary.
---
 src/bin/pg_dump/pg_backup.h           |  10 +-
 src/bin/pg_dump/pg_backup_archiver.c  |  91 ++++--
 src/bin/pg_dump/pg_backup_archiver.h  |   3 +-
 src/bin/pg_dump/pg_backup_directory.c |   2 +-
 src/bin/pg_dump/pg_dump.c             | 395 +++++++++++++++++++++++++-
 src/bin/pg_dump/pg_dump.h             |  11 +
 src/bin/pg_dump/pg_dump_sort.c        |  36 ++-
 src/bin/pg_dump/pg_dumpall.c          |   5 +
 src/bin/pg_dump/pg_restore.c          |  32 ++-
 src/bin/pg_dump/t/001_basic.pl        |  18 ++
 src/bin/pg_dump/t/002_pg_dump.pl      | 109 ++++++-
 src/bin/pg_upgrade/dump.c             |   6 +-
 src/bin/pg_upgrade/option.c           |  12 +
 src/bin/pg_upgrade/pg_upgrade.h       |   1 +
 doc/src/sgml/ref/pg_dump.sgml         |  69 ++++-
 doc/src/sgml/ref/pg_dumpall.sgml      |  38 +++
 doc/src/sgml/ref/pg_restore.sgml      |  51 +++-
 doc/src/sgml/ref/pgupgrade.sgml       |  18 ++
 src/tools/pgindent/typedefs.list      |   1 +
 19 files changed, 847 insertions(+), 61 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index f0f19bb0b2..3fa1474fad 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -110,9 +110,12 @@ typedef struct _restoreOptions
 	int			column_inserts;
 	int			if_exists;
 	int			no_comments;	/* Skip comments */
+	int			no_data;			/* Skip data */
 	int			no_publications;	/* Skip publication entries */
+	int			no_schema;			/* Skip schema generation */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
@@ -160,6 +163,7 @@ typedef struct _restoreOptions
 	/* flags derived from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } RestoreOptions;
 
 typedef struct _dumpOptions
@@ -179,8 +183,11 @@ typedef struct _dumpOptions
 	int			column_inserts;
 	int			if_exists;
 	int			no_comments;
-	int			no_security_labels;
+	int			no_data;
 	int			no_publications;
+	int			no_schema;
+	int			no_security_labels;
+	int			no_statistics;
 	int			no_subscriptions;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
@@ -208,6 +215,7 @@ typedef struct _dumpOptions
 	/* flags derived from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } DumpOptions;
 
 /*
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 707a3fc844..1cbe07c64f 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -46,6 +46,11 @@
 #define TEXT_DUMP_HEADER "--\n-- PostgreSQL database dump\n--\n\n"
 #define TEXT_DUMPALL_HEADER "--\n-- PostgreSQL database cluster dump\n--\n\n"
 
+typedef enum entryType {
+	default_entry,
+	data_entry,
+	statistics_entry
+} entryType;
 
 static ArchiveHandle *_allocAH(const char *FileSpec, const ArchiveFormat fmt,
 							   const pg_compress_specification compression_spec,
@@ -53,7 +58,7 @@ static ArchiveHandle *_allocAH(const char *FileSpec, const ArchiveFormat fmt,
 							   SetupWorkerPtrType setupWorkerPtr,
 							   DataDirSyncMethod sync_method);
 static void _getObjectDescription(PQExpBuffer buf, const TocEntry *te);
-static void _printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData);
+static void _printTocEntry(ArchiveHandle *AH, TocEntry *te, entryType entry_type);
 static char *sanitize_line(const char *str, bool want_hyphen);
 static void _doSetFixedOutputState(ArchiveHandle *AH);
 static void _doSetSessionAuth(ArchiveHandle *AH, const char *user);
@@ -149,6 +154,7 @@ InitDumpOptions(DumpOptions *opts)
 	opts->dumpSections = DUMP_UNSECTIONED;
 	opts->dumpSchema = true;
 	opts->dumpData = true;
+	opts->dumpStatistics = true;
 }
 
 /*
@@ -169,9 +175,10 @@ dumpOptionsFromRestoreOptions(RestoreOptions *ropt)
 	dopt->outputClean = ropt->dropSchema;
 	dopt->dumpData = ropt->dumpData;
 	dopt->dumpSchema = ropt->dumpSchema;
+	dopt->dumpSections = ropt->dumpSections;
+	dopt->dumpStatistics = ropt->dumpStatistics;
 	dopt->if_exists = ropt->if_exists;
 	dopt->column_inserts = ropt->column_inserts;
-	dopt->dumpSections = ropt->dumpSections;
 	dopt->aclsSkip = ropt->aclsSkip;
 	dopt->outputSuperuser = ropt->superuser;
 	dopt->outputCreateDB = ropt->createDB;
@@ -186,6 +193,9 @@ dumpOptionsFromRestoreOptions(RestoreOptions *ropt)
 	dopt->no_publications = ropt->no_publications;
 	dopt->no_security_labels = ropt->no_security_labels;
 	dopt->no_subscriptions = ropt->no_subscriptions;
+	dopt->no_data = ropt->no_data;
+	dopt->no_schema = ropt->no_schema;
+	dopt->no_statistics = ropt->no_statistics;
 	dopt->lockWaitTimeout = ropt->lockWaitTimeout;
 	dopt->include_everything = ropt->include_everything;
 	dopt->enable_row_security = ropt->enable_row_security;
@@ -418,30 +428,31 @@ RestoreArchive(Archive *AHX)
 	}
 
 	/*
-	 * Work out if we have an implied data-only restore. This can happen if
-	 * the dump was data only or if the user has used a toc list to exclude
-	 * all of the schema data. All we do is look for schema entries - if none
-	 * are found then we unset the dumpSchema flag.
+	 * Work out if we have an schema-less restore. This can happen if the dump
+	 * was data-only or statistics-only or no-schema or if the user has used a
+	 * toc list to exclude all of the schema data. All we do is look for schema
+	 * entries - if none are found then we unset the dumpSchema flag.
 	 *
 	 * We could scan for wanted TABLE entries, but that is not the same as
 	 * data-only. At this stage, it seems unnecessary (6-Mar-2001).
 	 */
 	if (ropt->dumpSchema)
 	{
-		int			impliedDataOnly = 1;
+		bool	no_schema_found = true;
 
 		for (te = AH->toc->next; te != AH->toc; te = te->next)
 		{
+			/* TODO: should this now be (REQ_SCHEMA | REQ_STATS)? */
 			if ((te->reqs & REQ_SCHEMA) != 0)
-			{					/* It's schema, and it's wanted */
-				impliedDataOnly = 0;
+			{
+				no_schema_found = false;
 				break;
 			}
 		}
-		if (impliedDataOnly)
+		if (no_schema_found)
 		{
 			ropt->dumpSchema = false;
-			pg_log_info("implied data-only restore");
+			pg_log_info("implied no-schema restore");
 		}
 	}
 
@@ -739,7 +750,7 @@ RestoreArchive(Archive *AHX)
 
 		for (te = AH->toc->next; te != AH->toc; te = te->next)
 		{
-			if ((te->reqs & (REQ_SCHEMA | REQ_DATA)) == 0)
+			if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) == 0)
 				continue;		/* ignore if not to be dumped at all */
 
 			switch (_tocEntryRestorePass(te))
@@ -760,7 +771,7 @@ RestoreArchive(Archive *AHX)
 		{
 			for (te = AH->toc->next; te != AH->toc; te = te->next)
 			{
-				if ((te->reqs & (REQ_SCHEMA | REQ_DATA)) != 0 &&
+				if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) != 0 &&
 					_tocEntryRestorePass(te) == RESTORE_PASS_ACL)
 					(void) restore_toc_entry(AH, te, false);
 			}
@@ -770,7 +781,7 @@ RestoreArchive(Archive *AHX)
 		{
 			for (te = AH->toc->next; te != AH->toc; te = te->next)
 			{
-				if ((te->reqs & (REQ_SCHEMA | REQ_DATA)) != 0 &&
+				if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) != 0 &&
 					_tocEntryRestorePass(te) == RESTORE_PASS_POST_ACL)
 					(void) restore_toc_entry(AH, te, false);
 			}
@@ -869,7 +880,7 @@ restore_toc_entry(ArchiveHandle *AH, TocEntry *te, bool is_parallel)
 			pg_log_info("creating %s \"%s\"",
 						te->desc, te->tag);
 
-		_printTocEntry(AH, te, false);
+		_printTocEntry(AH, te, default_entry);
 		defnDumped = true;
 
 		if (strcmp(te->desc, "TABLE") == 0)
@@ -938,7 +949,7 @@ restore_toc_entry(ArchiveHandle *AH, TocEntry *te, bool is_parallel)
 			 */
 			if (AH->PrintTocDataPtr != NULL)
 			{
-				_printTocEntry(AH, te, true);
+				_printTocEntry(AH, te, data_entry);
 
 				if (strcmp(te->desc, "BLOBS") == 0 ||
 					strcmp(te->desc, "BLOB COMMENTS") == 0)
@@ -1036,15 +1047,21 @@ restore_toc_entry(ArchiveHandle *AH, TocEntry *te, bool is_parallel)
 		{
 			/* If we haven't already dumped the defn part, do so now */
 			pg_log_info("executing %s %s", te->desc, te->tag);
-			_printTocEntry(AH, te, false);
+			_printTocEntry(AH, te, default_entry);
 		}
 	}
 
+	/*
+	 * If it has a statistics component that we want, then process that
+	 */
+	if ((reqs & REQ_STATS) != 0)
+		_printTocEntry(AH, te, statistics_entry);
+
 	/*
 	 * If we emitted anything for this TOC entry, that counts as one action
 	 * against the transaction-size limit.  Commit if it's time to.
 	 */
-	if ((reqs & (REQ_SCHEMA | REQ_DATA)) != 0 && ropt->txn_size > 0)
+	if ((reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) != 0 && ropt->txn_size > 0)
 	{
 		if (++AH->txnCount >= ropt->txn_size)
 		{
@@ -1084,6 +1101,7 @@ NewRestoreOptions(void)
 	opts->compression_spec.level = 0;
 	opts->dumpSchema = true;
 	opts->dumpData = true;
+	opts->dumpStatistics = true;
 
 	return opts;
 }
@@ -1329,7 +1347,7 @@ PrintTOCSummary(Archive *AHX)
 		te->reqs = _tocEntryRequired(te, curSection, AH);
 		/* Now, should we print it? */
 		if (ropt->verbose ||
-			(te->reqs & (REQ_SCHEMA | REQ_DATA)) != 0)
+			(te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) != 0)
 		{
 			char	   *sanitized_name;
 			char	   *sanitized_schema;
@@ -2918,6 +2936,14 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 		strcmp(te->desc, "SEARCHPATH") == 0)
 		return REQ_SPECIAL;
 
+	if (strcmp(te->desc, "STATISTICS DATA") == 0)
+	{
+		if (!ropt->dumpStatistics)
+			return 0;
+		else
+			res = REQ_STATS; /* return REQ_STATS;  */
+	}
+
 	/*
 	 * DATABASE and DATABASE PROPERTIES also have a special rule: they are
 	 * restored in createDB mode, and not restored otherwise, independently of
@@ -2962,6 +2988,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's statistics and we don't want statistics, maybe ignore it */
+	if (!ropt->dumpStatistics && strcmp(te->desc, "STATISTICS DATA") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2991,6 +3021,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS DATA") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
@@ -3107,6 +3138,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 		}
 	}
 
+
 	/*
 	 * Determine whether the TOC entry contains schema and/or data components,
 	 * and mask off inapplicable REQ bits.  If it had a dataDumper, assume
@@ -3172,12 +3204,12 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 				strncmp(te->tag, "LARGE OBJECT", 12) == 0) ||
 			   (strcmp(te->desc, "SECURITY LABEL") == 0 &&
 				strncmp(te->tag, "LARGE OBJECT", 12) == 0))))
-			res = res & REQ_SCHEMA;
+			res = res & (REQ_SCHEMA | REQ_STATS);
 	}
 
 	/* Mask it if we don't want schema */
 	if (!ropt->dumpSchema)
-		res = res & REQ_DATA;
+		res = res & (REQ_DATA | REQ_STATS);
 
 	return res;
 }
@@ -3729,7 +3761,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te)
  * will remain at default, until the matching ACL TOC entry is restored.
  */
 static void
-_printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData)
+_printTocEntry(ArchiveHandle *AH, TocEntry *te, entryType entry_type)
 {
 	RestoreOptions *ropt = AH->public.ropt;
 
@@ -3753,10 +3785,17 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData)
 		char	   *sanitized_schema;
 		char	   *sanitized_owner;
 
-		if (isData)
-			pfx = "Data for ";
-		else
-			pfx = "";
+		switch (entry_type)
+		{
+			case data_entry:
+				pfx = "Data for ";
+				break;
+			case statistics_entry:
+				pfx = "Statistics for ";
+				break;
+			default:
+				pfx = "";
+		}
 
 		ahprintf(AH, "--\n");
 		if (AH->public.verbose)
diff --git a/src/bin/pg_dump/pg_backup_archiver.h b/src/bin/pg_dump/pg_backup_archiver.h
index ce5ed1dd39..a2064f471e 100644
--- a/src/bin/pg_dump/pg_backup_archiver.h
+++ b/src/bin/pg_dump/pg_backup_archiver.h
@@ -209,7 +209,8 @@ typedef enum
 
 #define REQ_SCHEMA	0x01		/* want schema */
 #define REQ_DATA	0x02		/* want data */
-#define REQ_SPECIAL	0x04		/* for special TOC entries */
+#define REQ_STATS	0x04
+#define REQ_SPECIAL	0x08		/* for special TOC entries */
 
 struct _archiveHandle
 {
diff --git a/src/bin/pg_dump/pg_backup_directory.c b/src/bin/pg_dump/pg_backup_directory.c
index 240a1d4106..b2a841bb0f 100644
--- a/src/bin/pg_dump/pg_backup_directory.c
+++ b/src/bin/pg_dump/pg_backup_directory.c
@@ -780,7 +780,7 @@ _PrepParallelRestore(ArchiveHandle *AH)
 			continue;
 
 		/* We may ignore items not due to be restored */
-		if ((te->reqs & REQ_DATA) == 0)
+		if ((te->reqs & (REQ_DATA | REQ_STATS)) == 0)
 			continue;
 
 		/*
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index af857f00c7..60621302dd 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -431,6 +431,7 @@ main(int argc, char **argv)
 	DataDirSyncMethod sync_method = DATA_DIR_SYNC_METHOD_FSYNC;
 	bool		data_only = false;
 	bool		schema_only = false;
+	bool		statistics_only = false;
 
 	static DumpOptions dopt;
 
@@ -467,6 +468,7 @@ main(int argc, char **argv)
 		{"encoding", required_argument, NULL, 'E'},
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
+		{"statistics-only", no_argument, NULL, 'X'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -493,8 +495,11 @@ main(int argc, char **argv)
 		{"strict-names", no_argument, &strict_names, 1},
 		{"use-set-session-authorization", no_argument, &dopt.use_setsessauth, 1},
 		{"no-comments", no_argument, &dopt.no_comments, 1},
+		{"no-data", no_argument, &dopt.no_data, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
+		{"no-schema", no_argument, &dopt.no_schema, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -540,7 +545,7 @@ main(int argc, char **argv)
 
 	InitDumpOptions(&dopt);
 
-	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:RsS:t:T:U:vwWxZ:",
+	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:RsS:t:T:U:vwWxXZ:",
 							long_options, &optindex)) != -1)
 	{
 		switch (c)
@@ -614,6 +619,10 @@ main(int argc, char **argv)
 				dopt.cparams.pgport = pg_strdup(optarg);
 				break;
 
+			case 'X':			/* Dump statistics only */
+				statistics_only = true;
+				break;
+
 			case 'R':
 				/* no-op, still accepted for backwards compatibility */
 				break;
@@ -785,6 +794,17 @@ main(int argc, char **argv)
 
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (schema_only && statistics_only)
+		pg_fatal("options -s/--schema-only and -X/--statistics-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
+
+	if (data_only && dopt.no_data)
+		pg_fatal("options -a/--data-only and --no-data cannot be used together");
+	if (schema_only && dopt.no_schema)
+		pg_fatal("options -s/--schema-only and --no-schema cannot be used together");
+	if (statistics_only && dopt.no_statistics)
+		pg_fatal("options -X/--statistics-only and --no-statistics cannot be used together");
 
 	if (schema_only && foreign_servers_include_patterns.head != NULL)
 		pg_fatal("options -s/--schema-only and --include-foreign-data cannot be used together");
@@ -799,8 +819,9 @@ main(int argc, char **argv)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
 	/* set derivative flags */
-	dopt.dumpSchema = (!data_only);
-	dopt.dumpData = (!schema_only);
+	dopt.dumpData = data_only || (!schema_only && !statistics_only && !dopt.no_data);
+	dopt.dumpSchema = schema_only || (!data_only && !statistics_only && !dopt.no_schema);
+	dopt.dumpStatistics = statistics_only || (!data_only && !schema_only && !dopt.no_statistics);
 
 	/*
 	 * --inserts are already implied above if --column-inserts or
@@ -1100,6 +1121,7 @@ main(int argc, char **argv)
 	ropt->dropSchema = dopt.outputClean;
 	ropt->dumpData = dopt.dumpData;
 	ropt->dumpSchema = dopt.dumpSchema;
+	ropt->dumpStatistics = dopt.dumpStatistics;
 	ropt->if_exists = dopt.if_exists;
 	ropt->column_inserts = dopt.column_inserts;
 	ropt->dumpSections = dopt.dumpSections;
@@ -1178,7 +1200,7 @@ help(const char *progname)
 	printf(_("  -?, --help                   show this help, then exit\n"));
 
 	printf(_("\nOptions controlling the output content:\n"));
-	printf(_("  -a, --data-only              dump only the data, not the schema\n"));
+	printf(_("  -a, --data-only              dump only the data, not the schema or statistics\n"));
 	printf(_("  -b, --large-objects          include large objects in dump\n"));
 	printf(_("  --blobs                      (same as --large-objects, deprecated)\n"));
 	printf(_("  -B, --no-large-objects       exclude large objects in dump\n"));
@@ -1191,11 +1213,12 @@ help(const char *progname)
 	printf(_("  -N, --exclude-schema=PATTERN do NOT dump the specified schema(s)\n"));
 	printf(_("  -O, --no-owner               skip restoration of object ownership in\n"
 			 "                               plain-text format\n"));
-	printf(_("  -s, --schema-only            dump only the schema, no data\n"));
+	printf(_("  -s, --schema-only            dump only the schema, no data or statistics\n"));
 	printf(_("  -S, --superuser=NAME         superuser user name to use in plain-text format\n"));
 	printf(_("  -t, --table=PATTERN          dump only the specified table(s)\n"));
 	printf(_("  -T, --exclude-table=PATTERN  do NOT dump the specified table(s)\n"));
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
+	printf(_("  -X, --statistics-only        dump only the statistics, not schema or data\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
@@ -1220,8 +1243,11 @@ help(const char *progname)
 	printf(_("  --inserts                    dump data as INSERT commands, rather than COPY\n"));
 	printf(_("  --load-via-partition-root    load partitions via the root table\n"));
 	printf(_("  --no-comments                do not dump comment commands\n"));
+	printf(_("  --no-data                    do not dump data\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
+	printf(_("  --no-schema                  do not dump schema\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -6778,6 +6804,43 @@ getFuncs(Archive *fout)
 	destroyPQExpBuffer(query);
 }
 
+/*
+ * getRelationStatistics
+ *    register the statistics object as a dependent of the relation.
+ *
+ */
+static RelStatsInfo *
+getRelationStatistics(Archive *fout, DumpableObject *rel, char relkind)
+{
+	if ((relkind == RELKIND_RELATION) ||
+		(relkind == RELKIND_PARTITIONED_TABLE) ||
+		(relkind == RELKIND_INDEX) ||
+		(relkind == RELKIND_PARTITIONED_INDEX) ||
+		(relkind == RELKIND_MATVIEW))
+	{
+		RelStatsInfo	   *info = pg_malloc0(sizeof(RelStatsInfo));
+		DumpableObject	   *dobj = &info->dobj;
+
+		dobj->objType = DO_REL_STATS;
+		dobj->catId.tableoid = 0;
+		dobj->catId.oid = 0;
+		AssignDumpId(dobj);
+		dobj->dependencies = (DumpId *) pg_malloc(sizeof(DumpId));
+		dobj->dependencies[0] = rel->dumpId;
+		dobj->nDeps = 1;
+		dobj->allocDeps = 1;
+		dobj->components |= DUMP_COMPONENT_STATISTICS;
+		dobj->dump = rel->dump;
+		dobj->name = pg_strdup(rel->name);
+		dobj->namespace = rel->namespace;
+		info->relkind = relkind;
+		info->postponed_def = false;
+
+		return info;
+	}
+	return NULL;
+}
+
 /*
  * getTables
  *	  read all the tables (no indexes) in the system catalogs,
@@ -7155,6 +7218,9 @@ getTables(Archive *fout, int *numTables)
 
 		/* Tables have data */
 		tblinfo[i].dobj.components |= DUMP_COMPONENT_DATA;
+		/*
+		tblinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
+		*/
 
 		/* Mark whether table has an ACL */
 		if (!PQgetisnull(res, i, i_relacl))
@@ -7203,6 +7269,8 @@ getTables(Archive *fout, int *numTables)
 				}
 			}
 		}
+		if (tblinfo[i].interesting || dopt->dumpStatistics)
+			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relkind);
 	}
 
 	if (query->len != 0)
@@ -7649,11 +7717,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 		for (int c = 0; c < numinds; c++, j++)
 		{
 			char		contype;
+			char		indexkind;
+			RelStatsInfo   *relstats;
 
 			indxinfo[j].dobj.objType = DO_INDEX;
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -7676,7 +7747,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			{
 				NULL, NULL
 			};
+
+			if (indxinfo[j].parentidx == 0)
+				indexkind = RELKIND_INDEX;
+			else
+				indexkind = RELKIND_PARTITIONED_INDEX;
+
 			contype = *(PQgetvalue(res, j, i_contype));
+			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, indexkind);
 
 			if (contype == 'p' || contype == 'u' || contype == 'x')
 			{
@@ -7710,6 +7788,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				constrinfo->separate = true;
 
 				indxinfo[j].indexconstraint = constrinfo->dobj.dumpId;
+				if (relstats != NULL)
+					addObjectDependency(&relstats->dobj, constrinfo->dobj.dumpId);
 			}
 			else
 			{
@@ -10297,6 +10377,296 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * Tabular description of the parameters to pg_restore_relation_stats()
+ * param_name, param_type
+ */
+static const char *rel_stats_arginfo[][2] = {
+	{"relation", "regclass"},
+	{"version", "integer"},
+	{"relpages", "integer"},
+	{"reltuples", "real"},
+	{"relallvisible", "integer"},
+};
+
+/*
+ * Tabular description of the parameters to pg_restore_attribute_stats()
+ * param_name, param_type
+ */
+static const char *att_stats_arginfo[][2] = {
+	{"relation", "regclass"},
+	{"attname", "name"},
+	{"inherited", "boolean"},
+	{"version", "integer"},
+	{"null_frac", "float4"},
+	{"avg_width", "integer"},
+	{"n_distinct", "float4"},
+	{"most_common_vals", "text"},
+	{"most_common_freqs", "float4[]"},
+	{"histogram_bounds", "text"},
+	{"correlation", "float4"},
+	{"most_common_elems", "text"},
+	{"most_common_elem_freqs", "float4[]"},
+	{"elem_count_histogram", "float4[]"},
+	{"range_length_histogram", "text"},
+	{"range_empty_frac", "float4"},
+	{"range_bounds_histogram", "text"},
+};
+
+/*
+ * getRelStatsExportQuery --
+ *
+ * Generate a query that will fetch all relation (e.g. pg_class)
+ * stats for a given relation.
+ */
+static void
+getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "current_setting('server_version_num') AS version, "
+						 "c.relpages, c.reltuples, c.relallvisible "
+						 "FROM pg_class c "
+						 "JOIN pg_namespace n "
+						 "ON n.oid = c.relnamespace "
+						 "WHERE n.nspname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND c.relname = ");
+	appendStringLiteralAH(query, relname, fout);
+}
+
+/*
+ * getAttStatsExportQuery --
+ *
+ * Generate a query that will fetch all attribute (e.g. pg_statistic)
+ * stats for a given relation.
+ */
+static void
+getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "s.attname,"
+						 "s.inherited,"
+						 "current_setting('server_version_num') AS version, "
+						 "s.null_frac,"
+						 "s.avg_width,"
+						 "s.n_distinct,"
+						 "s.most_common_vals,"
+						 "s.most_common_freqs,"
+						 "s.histogram_bounds,"
+						 "s.correlation,"
+						 "s.most_common_elems,"
+						 "s.most_common_elem_freqs,"
+						 "s.elem_count_histogram,");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 "s.range_length_histogram,"
+							 "s.range_empty_frac,"
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(query,
+							 "NULL AS range_length_histogram,"
+							 "NULL AS range_empty_frac,"
+							 "NULL AS range_bounds_histogram ");
+
+	appendPQExpBufferStr(query,
+						 "FROM pg_stats s "
+						 "JOIN pg_namespace n "
+						 "ON n.nspname = s.schemaname "
+						 "JOIN pg_class c "
+						 "ON c.relname = s.tablename "
+						 "AND c.relnamespace = n.oid "
+						 "WHERE s.schemaname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND s.tablename = ");
+	appendStringLiteralAH(query, relname, fout);
+	appendPQExpBufferStr(query, " ORDER BY s.attname, s.inherited");
+}
+
+
+/*
+ * appendNamedArgument --
+ *
+ * Convenience routine for constructing parameters of the form:
+ * 'paraname', 'value'::type
+ */
+static void
+appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
+					const char *argval, const char *argtype)
+{
+	appendPQExpBufferStr(out, "\t");
+
+	appendStringLiteralAH(out, argname, fout);
+	appendPQExpBufferStr(out, ", ");
+
+	appendStringLiteralAH(out, argval, fout);
+	appendPQExpBuffer(out, "::%s", argtype);
+}
+
+/*
+ * appendRelStatsImport --
+ *
+ * Append a formatted pg_restore_relation_stats statement.
+ */
+static void
+appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	const char *sep = "";
+
+	if (PQntuples(res) == 0)
+		return;
+
+	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
+
+	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
+	{
+		const char *argname = rel_stats_arginfo[argno][0];
+		const char *argtype = rel_stats_arginfo[argno][1];
+		int			fieldno = PQfnumber(res, argname);
+
+		if (fieldno < 0)
+			pg_fatal("relation stats export query missing field '%s'",
+					 argname);
+
+		if (PQgetisnull(res, 0, fieldno))
+			continue;
+
+		appendPQExpBufferStr(out, sep);
+		appendNamedArgument(out, fout, argname, PQgetvalue(res, 0, fieldno), argtype);
+
+		sep = ",\n";
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+/*
+ * appendAttStatsImport --
+ *
+ * Append a series of formatted pg_restore_attribute_stats statements.
+ */
+static void
+appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	{
+		const char *sep = "";
+
+		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+		for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
+		{
+			const char *argname = att_stats_arginfo[argno][0];
+			const char *argtype = att_stats_arginfo[argno][1];
+			int			fieldno = PQfnumber(res, argname);
+
+			if (fieldno < 0)
+				pg_fatal("attribute stats export query missing field '%s'",
+						 argname);
+
+			if (PQgetisnull(res, rownum, fieldno))
+				continue;
+
+			appendPQExpBufferStr(out, sep);
+			appendNamedArgument(out, fout, argname, PQgetvalue(res, rownum, fieldno), argtype);
+			sep = ",\n";
+		}
+		appendPQExpBufferStr(out, "\n);\n");
+	}
+}
+
+/*
+ * Decide which section to use based on the relkind of the parent object.
+ *
+ * NB: materialized views may be postponed from SECTION_PRE_DATA to
+ * SECTION_POST_DATA to resolve some kinds of dependency problems. If so, the
+ * matview stats will also be postponed to SECTION_POST_DATA. See
+ * repairMatViewBoundaryMultiLoop().
+ */
+static teSection
+statisticsDumpSection(const RelStatsInfo *rsinfo)
+{
+	switch (rsinfo->relkind)
+	{
+		case RELKIND_RELATION:
+		case RELKIND_PARTITIONED_TABLE:
+		case RELKIND_MATVIEW:
+			return SECTION_DATA;
+		case RELKIND_INDEX:
+		case RELKIND_PARTITIONED_INDEX:
+			return SECTION_POST_DATA;
+		default:
+			pg_fatal("cannot dump statistics for relation kind '%c'",
+					 rsinfo->relkind);
+	}
+
+	return 0;				/* keep compiler quiet */
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	PGresult	   *res;
+	PQExpBuffer		query;
+	PQExpBuffer		out;
+	PQExpBuffer		tag;
+	DumpableObject *dobj = (DumpableObject *) &rsinfo->dobj;
+	DumpId		   *deps = NULL;
+	int				ndeps = 0;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
+	/* dependent on the relation definition, if doing schema */
+	if (fout->dopt->dumpSchema)
+	{
+		deps = dobj->dependencies;
+		ndeps = dobj->nDeps;
+	}
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", "STATISTICS DATA", fmtId(dobj->name));
+
+	query = createPQExpBuffer();
+	out = createPQExpBuffer();
+
+	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendRelStatsImport(out, fout, res);
+	PQclear(res);
+
+	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendAttStatsImport(out, fout, res);
+	PQclear(res);
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATISTICS DATA",
+							  .section = rsinfo->postponed_def ?
+								SECTION_POST_DATA :  statisticsDumpSection(rsinfo),
+							  .createStmt = out->data,
+							  .deps = deps,
+							  .nDeps = ndeps));
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(out);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -10745,6 +11115,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_SUBSCRIPTION_REL:
 			dumpSubscriptionTable(fout, (const SubRelInfo *) dobj);
 			break;
+		case DO_REL_STATS:
+			dumpRelationStats(fout, (const RelStatsInfo *) dobj);
+			break;
 		case DO_PRE_DATA_BOUNDARY:
 		case DO_POST_DATA_BOUNDARY:
 			/* never dumped, nothing to do */
@@ -17184,6 +17557,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 		free(indstatvalsarray);
 	}
 
+	/* Comments and stats share same .dep */
+
 	/* Dump Index Comments */
 	if (indxinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
 		dumpComment(fout, "INDEX", qindxname,
@@ -18971,6 +19346,16 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 				/* must come after the pre-data boundary */
 				addObjectDependency(dobj, preDataBound->dumpId);
 				break;
+			case DO_REL_STATS:
+				/* stats section varies by parent object type, DATA or POST */
+				if (statisticsDumpSection((RelStatsInfo *) dobj) == SECTION_DATA)
+				{
+					addObjectDependency(dobj, preDataBound->dumpId);
+					addObjectDependency(postDataBound, dobj->dumpId);
+				}
+				else
+					addObjectDependency(dobj, postDataBound->dumpId);
+				break;
 		}
 	}
 }
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 7139c88a69..410c162a85 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -83,10 +83,13 @@ typedef enum
 	DO_PUBLICATION,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
+	DO_REL_STATS,
 	DO_SUBSCRIPTION,
 	DO_SUBSCRIPTION_REL,		/* see note for SubRelInfo */
 } DumpableObjectType;
 
+#define NUM_DUMPABLE_OBJECT_TYPES (DO_SUBSCRIPTION_REL + 1)
+
 /*
  * DumpComponents is a bitmask of the potentially dumpable components of
  * a database object: its core definition, plus optional attributes such
@@ -110,6 +113,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
@@ -430,6 +434,13 @@ typedef struct _indexAttachInfo
 	IndxInfo   *partitionIdx;	/* link to index on partition */
 } IndexAttachInfo;
 
+typedef struct _relStatsInfo
+{
+	DumpableObject	dobj;
+	char			relkind;		/* 'r', 'm', 'i', etc */
+	bool			postponed_def;  /* stats must be postponed into post-data */
+} RelStatsInfo;
+
 typedef struct _statsExtInfo
 {
 	DumpableObject dobj;
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index dc9a28924b..201eeedde7 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -81,6 +81,7 @@ enum dbObjectTypePriorities
 	PRIO_TABLE_DATA,
 	PRIO_SEQUENCE_SET,
 	PRIO_LARGE_OBJECT_DATA,
+	PRIO_STATISTICS_DATA_DATA,
 	PRIO_POST_DATA_BOUNDARY,	/* boundary! */
 	PRIO_CONSTRAINT,
 	PRIO_INDEX,
@@ -148,11 +149,12 @@ static const int dbObjectTypePriority[] =
 	[DO_PUBLICATION] = PRIO_PUBLICATION,
 	[DO_PUBLICATION_REL] = PRIO_PUBLICATION_REL,
 	[DO_PUBLICATION_TABLE_IN_SCHEMA] = PRIO_PUBLICATION_TABLE_IN_SCHEMA,
+	[DO_REL_STATS] = PRIO_STATISTICS_DATA_DATA,
 	[DO_SUBSCRIPTION] = PRIO_SUBSCRIPTION,
 	[DO_SUBSCRIPTION_REL] = PRIO_SUBSCRIPTION_REL,
 };
 
-StaticAssertDecl(lengthof(dbObjectTypePriority) == (DO_SUBSCRIPTION_REL + 1),
+StaticAssertDecl(lengthof(dbObjectTypePriority) == NUM_DUMPABLE_OBJECT_TYPES,
 				 "array length mismatch");
 
 static DumpId preDataBoundId;
@@ -801,11 +803,21 @@ repairMatViewBoundaryMultiLoop(DumpableObject *boundaryobj,
 {
 	/* remove boundary's dependency on object after it in loop */
 	removeObjectDependency(boundaryobj, nextobj->dumpId);
-	/* if that object is a matview, mark it as postponed into post-data */
+	/*
+	 * If that object is a matview or matview status, mark it as postponed into
+	 * post-data.
+	 */
 	if (nextobj->objType == DO_TABLE)
 	{
 		TableInfo  *nextinfo = (TableInfo *) nextobj;
 
+		if (nextinfo->relkind == RELKIND_MATVIEW)
+			nextinfo->postponed_def = true;
+	}
+	else if (nextobj->objType == DO_REL_STATS)
+	{
+		RelStatsInfo *nextinfo = (RelStatsInfo *) nextobj;
+
 		if (nextinfo->relkind == RELKIND_MATVIEW)
 			nextinfo->postponed_def = true;
 	}
@@ -1018,6 +1030,21 @@ repairDependencyLoop(DumpableObject **loop,
 					{
 						DumpableObject *nextobj;
 
+						nextobj = (j < nLoop - 1) ? loop[j + 1] : loop[0];
+						repairMatViewBoundaryMultiLoop(loop[j], nextobj);
+						return;
+					}
+				}
+			}
+			else if (loop[i]->objType == DO_REL_STATS &&
+					 ((RelStatsInfo *) loop[i])->relkind == RELKIND_MATVIEW)
+			{
+				for (j = 0; j < nLoop; j++)
+				{
+					if (loop[j]->objType == DO_POST_DATA_BOUNDARY)
+					{
+						DumpableObject *nextobj;
+
 						nextobj = (j < nLoop - 1) ? loop[j + 1] : loop[0];
 						repairMatViewBoundaryMultiLoop(loop[j], nextobj);
 						return;
@@ -1500,6 +1527,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "POST-DATA BOUNDARY  (ID %d)",
 					 obj->dumpId);
 			return;
+		case DO_REL_STATS:
+			snprintf(buf, bufsize,
+					 "RELATION STATISTICS FOR %s  (ID %d OID %u)",
+					 obj->name, obj->dumpId, obj->catId.oid);
+			return;
 	}
 	/* shouldn't get here */
 	snprintf(buf, bufsize,
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 396f79781c..7effb70490 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -103,6 +103,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -172,6 +173,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -451,6 +453,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -666,6 +670,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index c602272d7d..fa08f0a186 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -63,6 +63,9 @@ main(int argc, char **argv)
 	int			numWorkers = 1;
 	Archive    *AH;
 	char	   *inputFileSpec;
+	bool		data_only = false;
+	bool		schema_only = false;
+	bool		statistics_only = false;
 	static int	disable_triggers = 0;
 	static int	enable_row_security = 0;
 	static int	if_exists = 0;
@@ -71,12 +74,13 @@ main(int argc, char **argv)
 	static int	outputNoTablespaces = 0;
 	static int	use_setsessauth = 0;
 	static int	no_comments = 0;
+	static int	no_data = 0;
 	static int	no_publications = 0;
+	static int	no_schema = 0;
 	static int	no_security_labels = 0;
+	static int  no_statistics = 0;
 	static int	no_subscriptions = 0;
 	static int	strict_names = 0;
-	bool		data_only = false;
-	bool		schema_only = false;
 
 	struct option cmdopts[] = {
 		{"clean", 0, NULL, 'c'},
@@ -108,6 +112,7 @@ main(int argc, char **argv)
 		{"username", 1, NULL, 'U'},
 		{"verbose", 0, NULL, 'v'},
 		{"single-transaction", 0, NULL, '1'},
+		{"statistics-only", no_argument, NULL, 'X'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -124,9 +129,12 @@ main(int argc, char **argv)
 		{"transaction-size", required_argument, NULL, 5},
 		{"use-set-session-authorization", no_argument, &use_setsessauth, 1},
 		{"no-comments", no_argument, &no_comments, 1},
+		{"no-data", no_argument, &no_data, 1},
 		{"no-publications", no_argument, &no_publications, 1},
+		{"no-schema", no_argument, &no_schema, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"filter", required_argument, NULL, 4},
 
 		{NULL, 0, NULL, 0}
@@ -271,6 +279,10 @@ main(int argc, char **argv)
 				opts->aclsSkip = 1;
 				break;
 
+			case 'X':			/* Restore statistics only */
+				statistics_only = true;
+				break;
+
 			case '1':			/* Restore data in a single transaction */
 				opts->single_txn = true;
 				opts->exit_on_error = true;
@@ -343,6 +355,10 @@ main(int argc, char **argv)
 
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
+	if (schema_only && statistics_only)
+		pg_fatal("options -s/--schema-only and -X/--statistics-only cannot be used together");
 
 	if (data_only && opts->dropSchema)
 		pg_fatal("options -c/--clean and -a/--data-only cannot be used together");
@@ -362,8 +378,9 @@ main(int argc, char **argv)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
 	/* set derivative flags */
-	opts->dumpSchema = (!data_only);
-	opts->dumpData = (!schema_only);
+	opts->dumpData = data_only || (!no_data && !schema_only && !statistics_only);
+	opts->dumpSchema = schema_only || (!no_schema && !data_only && !statistics_only);
+	opts->dumpStatistics = statistics_only || (!no_statistics && !data_only && !schema_only);
 
 	opts->disable_triggers = disable_triggers;
 	opts->enable_row_security = enable_row_security;
@@ -375,6 +392,8 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
+	opts->no_data = no_data;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
@@ -482,6 +501,7 @@ usage(const char *progname)
 	printf(_("  -t, --table=NAME             restore named relation (table, view, etc.)\n"));
 	printf(_("  -T, --trigger=NAME           restore named trigger\n"));
 	printf(_("  -x, --no-privileges          skip restoration of access privileges (grant/revoke)\n"));
+	printf(_("  -X, --statistics-only        restore only the statistics, not schema or data\n"));
 	printf(_("  -1, --single-transaction     restore as a single transaction\n"));
 	printf(_("  --disable-triggers           disable triggers during data-only restore\n"));
 	printf(_("  --enable-row-security        enable row security\n"));
@@ -489,10 +509,14 @@ usage(const char *progname)
 			 "                               in FILENAME\n"));
 	printf(_("  --if-exists                  use IF EXISTS when dropping objects\n"));
 	printf(_("  --no-comments                do not restore comment commands\n"));
+	printf(_("  --no-data                    do not restore data\n"));
 	printf(_("  --no-data-for-failed-tables  do not restore data of tables that could not be\n"
 			 "                               created\n"));
 	printf(_("  --no-publications            do not restore publications\n"));
+	printf(_("  --no-schema                  do not restore schema\n"));
 	printf(_("  --no-security-labels         do not restore security labels\n"));
+	printf(_("  --no-statistics              do not restore statistics\n"));
+	/* This hack is only needed in a data-only restore */
 	printf(_("  --no-subscriptions           do not restore subscriptions\n"));
 	printf(_("  --no-table-access-method     do not restore table access methods\n"));
 	printf(_("  --no-tablespaces             do not restore tablespace assignments\n"));
diff --git a/src/bin/pg_dump/t/001_basic.pl b/src/bin/pg_dump/t/001_basic.pl
index 214240f1ae..f29da06ed2 100644
--- a/src/bin/pg_dump/t/001_basic.pl
+++ b/src/bin/pg_dump/t/001_basic.pl
@@ -50,12 +50,30 @@ command_fails_like(
 	'pg_dump: options -s/--schema-only and -a/--data-only cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '-s', '-X' ],
+	qr/\Qpg_dump: error: options -s\/--schema-only and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: error: options -s/--schema-only and -X/--statistics-only cannot be used together'
+);
+
+command_fails_like(
+	[ 'pg_dump', '-a', '-X' ],
+	qr/\Qpg_dump: error: options -a\/--data-only and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: error: options -a/--data-only and -X/--statistics-only cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-s', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: options -s\/--schema-only and --include-foreign-data cannot be used together\E/,
 	'pg_dump: options -s/--schema-only and --include-foreign-data cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '--statistics-only', '--no-statistics' ],
+	qr/\Qpg_dump: error: options -X\/--statistics-only and --no-statistics cannot be used together\E/,
+	'pg_dump: options -X\/--statistics-only and --no-statistics cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-j2', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: option --include-foreign-data is not supported with parallel backup\E/,
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 805ba9f49f..7e1f392d21 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -66,6 +66,7 @@ my %pgdump_runs = (
 			'--file' => "$tempdir/binary_upgrade.dump",
 			'--no-password',
 			'--schema-only',
+			'--no-data',
 			'--binary-upgrade',
 			'--dbname' => 'postgres',    # alternative way to specify database
 		],
@@ -710,6 +711,39 @@ my %pgdump_runs = (
 			'--no-large-objects',
 			'postgres',
 		],
+	},
+	no_statistics => {
+		dump_cmd => [
+			'pg_dump', '--no-sync',
+			"--file=$tempdir/no_statistics.sql",
+			'--no-statistics',
+			'postgres',
+		],
+	},
+	no_data_no_schema => {
+		dump_cmd => [
+			'pg_dump', '--no-sync',
+			"--file=$tempdir/no_data_no_schema.sql",
+			'--no-data',
+			'--no-schema',
+			'postgres',
+		],
+	},
+	statistics_only => {
+		dump_cmd => [
+			'pg_dump', '--no-sync',
+			"--file=$tempdir/statistics_only.sql",
+			'--statistics-only',
+			'postgres',
+		],
+	},
+	no_schema => {
+		dump_cmd => [
+			'pg_dump', '--no-sync',
+			"--file=$tempdir/no_schema.sql",
+			'--no-schema',
+			'postgres',
+		],
 	},);
 
 ###############################################################
@@ -776,6 +810,7 @@ my %full_runs = (
 	no_large_objects => 1,
 	no_owner => 1,
 	no_privs => 1,
+	no_statistics => 1,
 	no_table_access_method => 1,
 	pg_dumpall_dbprivs => 1,
 	pg_dumpall_exclude => 1,
@@ -977,6 +1012,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1390,6 +1426,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1411,6 +1448,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1432,6 +1470,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1598,6 +1637,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1751,6 +1791,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			only_dump_test_table => 1,
 			section_data => 1,
 		},
@@ -1778,6 +1819,7 @@ my %tests = (
 			data_only => 1,
 			exclude_test_table => 1,
 			exclude_test_table_data => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1798,7 +1840,10 @@ my %tests = (
 			\QCOPY dump_test.fk_reference_test_table (col1) FROM stdin;\E
 			\n(?:\d\n){5}\\\.\n
 			/xms,
-		like => { data_only => 1, },
+		like => {
+			data_only => 1,
+			no_schema => 1,
+		},
 	},
 
 	'COPY test_second_table' => {
@@ -1814,6 +1859,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1836,6 +1882,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1859,6 +1906,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1881,6 +1929,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1903,6 +1952,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -3299,6 +3349,7 @@ my %tests = (
 		like => {
 			%full_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 			only_dump_test_schema => 1,
 			test_schema_plus_large_objects => 1,
@@ -3469,6 +3520,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			only_dump_measurement => 1,
 			section_data => 1,
 			only_dump_test_schema => 1,
@@ -4351,6 +4403,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 			binary_upgrade => 1,
@@ -4651,6 +4704,60 @@ my %tests = (
 		},
 	},
 
+	#
+	# Table statistics should go in section=data.
+	# Materialized view statistics should go in section=post-data.
+	#
+	# TABLE and MATVIEW stats will end up in SECTION_DATA.
+	# INDEX stats (expression columns only) will end up in SECTION_POST_DATA.
+	#
+	'statistics_import' => {
+		create_sql => '
+			CREATE TABLE dump_test.has_stats
+			AS SELECT g.g AS x, g.g / 2 AS y FROM generate_series(1,100) AS g(g);
+			CREATE MATERIALIZED VIEW dump_test.has_stats_mv AS SELECT * FROM dump_test.has_stats;
+			CREATE INDEX dup_test_post_data_ix ON dump_test.has_stats((x - 1));
+			ANALYZE dump_test.has_stats, dump_test.has_stats_mv;',
+		regexp => qr/pg_catalog.pg_restore_attribute_stats/,
+		like => {
+			%full_runs,
+			%dump_test_schema_runs,
+			no_data_no_schema => 1,
+			no_schema => 1,
+			section_data => 1,
+			section_post_data => 1,
+			statistics_only => 1,
+			},
+		unlike => {
+			exclude_dump_test_schema => 1,
+			no_statistics => 1,
+			only_dump_measurement => 1,
+			schema_only => 1,
+			},
+	},
+
+	'relstats_on_unanalyzed_tables' => {
+		regexp => qr/pg_catalog.pg_restore_relation_stats/,
+
+		# this shouldn't ever get emitted anymore
+		like => {
+			%full_runs,
+			%dump_test_schema_runs,
+			no_data_no_schema => 1,
+			no_schema => 1,
+			only_dump_test_table => 1,
+			role => 1,
+			role_parallel => 1,
+			section_data => 1,
+			section_post_data => 1,
+			statistics_only => 1,
+			},
+		unlike => {
+			no_statistics => 1,
+			schema_only => 1,
+			},
+	},
+
 	# CREATE TABLE with partitioned table and various AMs.  One
 	# partition uses the same default as the parent, and a second
 	# uses its own AM.
diff --git a/src/bin/pg_upgrade/dump.c b/src/bin/pg_upgrade/dump.c
index 8ce0fa3020..a29cd2cca9 100644
--- a/src/bin/pg_upgrade/dump.c
+++ b/src/bin/pg_upgrade/dump.c
@@ -21,10 +21,11 @@ generate_old_dump(void)
 
 	/* run new pg_dumpall binary for globals */
 	exec_prog(UTILITY_LOG_FILE, NULL, true, true,
-			  "\"%s/pg_dumpall\" %s --globals-only --quote-all-identifiers "
+			  "\"%s/pg_dumpall\" %s %s --globals-only --quote-all-identifiers "
 			  "--binary-upgrade %s --no-sync -f \"%s/%s\"",
 			  new_cluster.bindir, cluster_conn_opts(&old_cluster),
 			  log_opts.verbose ? "--verbose" : "",
+			  user_opts.do_statistics ? "" : "--no-statistics",
 			  log_opts.dumpdir,
 			  GLOBALS_DUMP_FILE);
 	check_ok();
@@ -52,10 +53,11 @@ generate_old_dump(void)
 		snprintf(log_file_name, sizeof(log_file_name), DB_DUMP_LOG_FILE_MASK, old_db->db_oid);
 
 		parallel_exec_prog(log_file_name, NULL,
-						   "\"%s/pg_dump\" %s --schema-only --quote-all-identifiers "
+						   "\"%s/pg_dump\" %s --no-data %s --quote-all-identifiers "
 						   "--binary-upgrade --format=custom %s --no-sync --file=\"%s/%s\" %s",
 						   new_cluster.bindir, cluster_conn_opts(&old_cluster),
 						   log_opts.verbose ? "--verbose" : "",
+						   user_opts.do_statistics ? "" : "--no-statistics",
 						   log_opts.dumpdir,
 						   sql_file_name, escaped_connstr.data);
 
diff --git a/src/bin/pg_upgrade/option.c b/src/bin/pg_upgrade/option.c
index 108eb7a1ba..3b6c7ec994 100644
--- a/src/bin/pg_upgrade/option.c
+++ b/src/bin/pg_upgrade/option.c
@@ -60,6 +60,8 @@ parseCommandLine(int argc, char *argv[])
 		{"copy", no_argument, NULL, 2},
 		{"copy-file-range", no_argument, NULL, 3},
 		{"sync-method", required_argument, NULL, 4},
+		{"with-statistics", no_argument, NULL, 5},
+		{"no-statistics", no_argument, NULL, 6},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -70,6 +72,7 @@ parseCommandLine(int argc, char *argv[])
 
 	user_opts.do_sync = true;
 	user_opts.transfer_mode = TRANSFER_MODE_COPY;
+	user_opts.do_statistics = true;
 
 	os_info.progname = get_progname(argv[0]);
 
@@ -212,6 +215,13 @@ parseCommandLine(int argc, char *argv[])
 				user_opts.sync_method = pg_strdup(optarg);
 				break;
 
+			case 5:
+				user_opts.do_statistics = true;
+				break;
+			case 6:
+				user_opts.do_statistics = false;
+				break;
+
 			default:
 				fprintf(stderr, _("Try \"%s --help\" for more information.\n"),
 						os_info.progname);
@@ -306,7 +316,9 @@ usage(void)
 	printf(_("  --clone                       clone instead of copying files to new cluster\n"));
 	printf(_("  --copy                        copy files to new cluster (default)\n"));
 	printf(_("  --copy-file-range             copy files to new cluster with copy_file_range\n"));
+	printf(_("  --no-statistics               do not import statistics from old cluster\n"));
 	printf(_("  --sync-method=METHOD          set method for syncing files to disk\n"));
+	printf(_("  --with-statistics             import statistics from old cluster (default)\n"));
 	printf(_("  -?, --help                    show this help, then exit\n"));
 	printf(_("\n"
 			 "Before running pg_upgrade you must:\n"
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index 0cdd675e4f..3fe111fbde 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -327,6 +327,7 @@ typedef struct
 	int			jobs;			/* number of processes/threads to use */
 	char	   *socketdir;		/* directory to use for Unix sockets */
 	char	   *sync_method;
+	bool		do_statistics;	/* carry over statistics from old cluster */
 } UserOpts;
 
 typedef struct
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index 24fcc76d72..ecf7d3d632 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -123,7 +123,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Dump only the data, not the schema (data definitions).
+        Dump only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are dumped.
        </para>
 
@@ -141,13 +141,11 @@ PostgreSQL documentation
       <listitem>
        <para>
         Include large objects in the dump.  This is the default behavior
-        except when <option>--schema</option>, <option>--table</option>, or
-        <option>--schema-only</option> is specified.  The <option>-b</option>
-        switch is therefore only useful to add large objects to dumps
-        where a specific schema or table has been requested.  Note that
-        large objects are considered data and therefore will be included when
-        <option>--data-only</option> is used, but not
-        when <option>--schema-only</option> is.
+        except when <option>--schema</option>, <option>--table</option>,
+        <option>--schema-only</option>, or <option>--statistics-only</option>, or
+        <option>--no-data</option> is specified.  The <option>-b</option>
+        switch is therefore only useful to add large objects to dumps where a
+        specific schema or table has been requested.
        </para>
       </listitem>
      </varlistentry>
@@ -516,10 +514,11 @@ PostgreSQL documentation
       <term><option>--schema-only</option></term>
       <listitem>
        <para>
-        Dump only the object definitions (schema), not data.
+        Dump only the object definitions (schema), not data or statistics.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive to <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
         <option>--section=pre-data --section=post-data</option>.
@@ -652,6 +651,18 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Dump only the statistics, not the schema (data definitions) or data.
+        Statistics for tables, materialized views, and indexes are dumped.
+       </para>
+
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-Z <replaceable class="parameter">level</replaceable></option></term>
       <term><option>-Z <replaceable class="parameter">method</replaceable></option>[:<replaceable>detail</replaceable>]</term>
@@ -824,16 +835,17 @@ PostgreSQL documentation
       <term><option>--exclude-table-data=<replaceable class="parameter">pattern</replaceable></option></term>
       <listitem>
        <para>
-        Do not dump data for any tables matching <replaceable
+        Do not dump data or statistics for any tables matching <replaceable
         class="parameter">pattern</replaceable>. The pattern is
         interpreted according to the same rules as for <option>-t</option>.
         <option>--exclude-table-data</option> can be given more than once to
-        exclude tables matching any of several patterns. This option is
-        useful when you need the definition of a particular table even
-        though you do not need the data in it.
+        exclude tables matching any of several patterns. This option is useful
+        when you need the definition of a particular table even though you do
+        not need the data in it.
        </para>
        <para>
-        To exclude data for all tables in the database, see <option>--schema-only</option>.
+        To exclude data for all tables in the database, see <option>--schema-only</option>
+        or <option>--statistics-only</option>.
        </para>
       </listitem>
      </varlistentry>
@@ -1080,6 +1092,15 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-data</option></term>
+      <listitem>
+       <para>
+        Do not dump data.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-publications</option></term>
       <listitem>
@@ -1098,6 +1119,24 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-schema</option></term>
+      <listitem>
+       <para>
+        Do not dump schema (data definitions).
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_dumpall.sgml b/doc/src/sgml/ref/pg_dumpall.sgml
index 39d93c2c0e..3834756973 100644
--- a/doc/src/sgml/ref/pg_dumpall.sgml
+++ b/doc/src/sgml/ref/pg_dumpall.sgml
@@ -265,6 +265,17 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Dump only the statistics, not the schema (data definitions) or data.
+        Statistics for tables, materialized views, and indexes are dumped.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--binary-upgrade</option></term>
       <listitem>
@@ -422,6 +433,15 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-data</option></term>
+      <listitem>
+       <para>
+        Do not dump data.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-publications</option></term>
       <listitem>
@@ -447,6 +467,15 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-schema</option></term>
+      <listitem>
+       <para>
+        Do not dump schema (data definitions).
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-security-labels</option></term>
       <listitem>
@@ -456,6 +485,15 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index b8b27e1719..22c3c118ad 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -94,7 +94,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Restore only the data, not the schema (data definitions).
+        Restore only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are restored,
         if present in the archive.
        </para>
@@ -483,10 +483,11 @@ PostgreSQL documentation
         to the extent that schema entries are present in the archive.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive of <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
-        <option>--section=pre-data --section=post-data</option>.
+        <option>--section=pre-data --section=post-data --no-statistics</option>.
        </para>
        <para>
         (Do not confuse this with the <option>--schema</option> option, which
@@ -599,6 +600,20 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Restore only the statistics, not schema (data definitions) or data.
+       </para>
+       <para>
+        (Do not confuse this with the <option>--schema</option> option, which
+        uses the word <quote>schema</quote> in a different meaning.)
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-1</option></term>
       <term><option>--single-transaction</option></term>
@@ -681,6 +696,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-data</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore data, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-data-for-failed-tables</option></term>
       <listitem>
@@ -713,6 +738,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-schema</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore schema (data definitions), even if
+        the archive contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-security-labels</option></term>
       <listitem>
@@ -723,6 +758,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore statistics, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pgupgrade.sgml b/doc/src/sgml/ref/pgupgrade.sgml
index 4777381dac..64a1ebd613 100644
--- a/doc/src/sgml/ref/pgupgrade.sgml
+++ b/doc/src/sgml/ref/pgupgrade.sgml
@@ -145,6 +145,24 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--with-statistics</option></term>
+      <listitem>
+       <para>
+        Restore statistics from the old cluster into the new cluster.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not restore statistics from the old cluster into the new cluster.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-o</option> <replaceable class="parameter">options</replaceable></term>
       <term><option>--old-options</option> <replaceable class="parameter">options</replaceable></term>
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index a2644a2e65..3bb1e0e447 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2400,6 +2400,7 @@ RelMapFile
 RelMapping
 RelOptInfo
 RelOptKind
+RelStatsInfo
 RelToCheck
 RelToCluster
 RelabelType
-- 
2.48.1

#301Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#300)
Re: Statistics Import and Export

On Wed, 2025-02-05 at 23:01 -0500, Corey Huinker wrote:

And here's an update to the pg_dump code itself. This currently has
failing TAP tests for statistics in the custom and dir formats, but
is working otherwise.

This thread got slightly mixed up, so I'm replying to the v45-0001
posted here, and also in response to Michael's and Corey's comments
from:

/messages/by-id/Z5H0iRaJc1wnDVLE@paquier.xyz

On Thu, 2025-01-23 at 16:49 +0900, Michael Paquier wrote:

On Tue, Jan 21, 2025 at 10:21:51PM -0500, Corey Huinker wrote:

After some research, I think that we should treat partitioned
indexes like
we were before, and just handle the existing special case for
regular
indexes.

Hmm, why? Sounds strange to me to not have the same locking
semantics
for the partitioned parts, and this even if partitioned indexes don't
have stats that can be manipulated in relation_stats.c as far as I
can see. These stats APIs are designed to be permissive as Jeff
says.
Having a better locking from the start makes the whole picture more
consistent, while opening the door for actually setting real stat
numbers for partitioned indexes (if some make sense, at some point)?

v45-0001 addresses this by locking both the partitioned index, as well
as its table, in ShareUpdateExclusive mode. That satisfies the in-place
update requirement to take a ShareUpdateExclusiveLock on the
partitioned index, while otherwise being the same as normal indexes
(and therefore unlikely to cause a problem if ANALYZE sets stats on
partitioned indexes in the future).

That means:
* For indexes: ShareUpdateExclusiveLock on table and AccessShareLock
on index
* For partitioned indexes: ShareUpdateExclusiveLock on table and
ShareUpdateExclusiveLock on index
* Otherwise, ShareupdateExclusiveLock on the relation

which makes sense to me. The v45-0001 patch itself could use some
cleanup, but I can take care of that at commit time if we agree on the
locking scheme.

Regards,
Jeff Davis

#302Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#301)
2 attachment(s)
Re: Statistics Import and Export

This thread got slightly mixed up, so I'm replying to the v45-0001
posted here, and also in response to Michael's and Corey's comments
from:

/messages/by-id/Z5H0iRaJc1wnDVLE@paquier.xyz

Thanks.

which makes sense to me. The v45-0001 patch itself could use some
cleanup, but I can take care of that at commit time if we agree on the
locking scheme.

While no changes were made to 0001 since v45, 0002 has fixed the failing
regression tests from v45. Other cleanup and a review of documentation
wording has been done, some of which I'm still not totally satisfied with,
so I'm going to give it another look tomorrow, but am putting this out for
reviewers in the mean time.

Attachments:

v46-0001-Lock-table-first-when-setting-index-relation-sta.patchtext/x-patch; charset=US-ASCII; name=v46-0001-Lock-table-first-when-setting-index-relation-sta.patchDownload
From fb095b7ea75ef366eaae5eb7b5322b9869a760ea Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Tue, 21 Jan 2025 11:52:58 -0500
Subject: [PATCH v46 1/2] Lock table first when setting index relation
 statistics.

Jian He reported [1] a missing lock relation bug in
pg_restore_relation_stats when restoring stats to an index.

This fix follows the steps for proper locking prior to an inplace update
of a relation as specified in aac2c9b4fde8, and mimics the locking
behavior of the analyze command, as well as correctly doing the ACL
checks against the underlying relation of an index rather than the index
itself.

There is no special case for partitioned indexes, so while we want to
the ACL checks against the underlying relation, we need to take out
the more restrictive ShareUpdateExclusiveLock on the partitioned index.

[1] https://www.postgresql.org/message-id/CACJufxGreTY7qsCV8%2BBkuv0p5SXGTScgh%3DD%2BDq6%3D%2B_%3DXTp7FWg%40mail.gmail.com
---
 src/backend/statistics/stat_utils.c        | 45 +++++++++++++++---
 src/test/regress/expected/stats_import.out | 53 ++++++++++++++++++++++
 src/test/regress/sql/stats_import.sql      | 36 +++++++++++++++
 3 files changed, 128 insertions(+), 6 deletions(-)

diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c
index 0d446f55b0..f87007e72c 100644
--- a/src/backend/statistics/stat_utils.c
+++ b/src/backend/statistics/stat_utils.c
@@ -17,13 +17,16 @@
 #include "postgres.h"
 
 #include "access/relation.h"
+#include "catalog/index.h"
 #include "catalog/pg_database.h"
 #include "funcapi.h"
 #include "miscadmin.h"
 #include "statistics/stat_utils.h"
+#include "storage/lmgr.h"
 #include "utils/acl.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
+#include "utils/lsyscache.h"
 #include "utils/rel.h"
 
 /*
@@ -126,18 +129,45 @@ stats_check_arg_pair(FunctionCallInfo fcinfo,
 void
 stats_lock_check_privileges(Oid reloid)
 {
-	Relation	rel = relation_open(reloid, ShareUpdateExclusiveLock);
-	const char	relkind = rel->rd_rel->relkind;
+	Relation	rel;
+	Oid			relation_oid = reloid;
+	Oid			index_oid = InvalidOid;
+	LOCKMODE	index_lockmode = AccessShareLock;
 
-	/* All of the types that can be used with ANALYZE, plus indexes */
-	switch (relkind)
+	/*
+	 * For indexes, we follow what do_analyze_rel() does so as to avoid any
+	 * deadlocks with analyze/vacuum, which is to take out a
+	 * ShareUpdateExclusive on table/matview first and then take an
+	 * AccessShareLock on the index itself. See check_inplace_rel_lock()
+	 * to see how this special case is implemented.
+	 *
+	 * Partitioned indexes do not have an exception in check_inplace_rel_lock(),
+	 * so we want to take a ShareUpdateExclusive lock there instead.
+	 */
+	switch(get_rel_relkind(reloid))
 	{
-		case RELKIND_RELATION:
 		case RELKIND_INDEX:
+			relation_oid = IndexGetRelation(reloid, false);
+			index_oid = reloid;
+			break;
+		case RELKIND_PARTITIONED_INDEX:
+			relation_oid = IndexGetRelation(reloid, false);
+			index_oid = reloid;
+			index_lockmode = ShareUpdateExclusiveLock;
+			break;
+		default:
+			break;
+	}
+
+	rel = relation_open(relation_oid, ShareUpdateExclusiveLock);
+
+	switch (rel->rd_rel->relkind)
+	{
+		/* All of the types that can be used with ANALYZE */
+		case RELKIND_RELATION:
 		case RELKIND_MATVIEW:
 		case RELKIND_FOREIGN_TABLE:
 		case RELKIND_PARTITIONED_TABLE:
-		case RELKIND_PARTITIONED_INDEX:
 			break;
 		default:
 			ereport(ERROR,
@@ -164,6 +194,9 @@ stats_lock_check_privileges(Oid reloid)
 						   NameStr(rel->rd_rel->relname));
 	}
 
+	if (OidIsValid(index_oid))
+		LockRelationOid(index_oid, index_lockmode);
+
 	relation_close(rel, NoLock);
 }
 
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index fb50da1cd8..6cd584da68 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -85,6 +85,26 @@ WHERE oid = 'stats_import.test'::regclass;
        17 |       400 |             4
 (1 row)
 
+CREATE INDEX test_i ON stats_import.test(id);
+-- regular indexes have special case locking rules
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test_i'::regclass,
+        relpages => 18::integer);
+ pg_set_relation_stats 
+-----------------------
+ 
+(1 row)
+
+SELECT
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.test_i'::regclass,
+        'relpages', 19::integer );
+ pg_restore_relation_stats 
+---------------------------
+ t
+(1 row)
+
 -- positional arguments
 SELECT
     pg_catalog.pg_set_relation_stats(
@@ -182,6 +202,7 @@ CREATE TABLE stats_import.part_child_1
   PARTITION OF stats_import.part_parent
   FOR VALUES FROM (0) TO (10)
   WITH (autovacuum_enabled = false);
+CREATE INDEX part_parent_i ON stats_import.part_parent(i);
 ANALYZE stats_import.part_parent;
 SELECT relpages
 FROM pg_class
@@ -202,6 +223,25 @@ SELECT
  
 (1 row)
 
+-- Partitioned indexes aren't analyzed but it is possible to set stats.
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.part_parent_i'::regclass,
+        relpages => 2::integer);
+ pg_set_relation_stats 
+-----------------------
+ 
+(1 row)
+
+SELECT
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.part_parent_i'::regclass,
+        'relpages', 2::integer);
+ pg_restore_relation_stats 
+---------------------------
+ t
+(1 row)
+
 -- nothing stops us from setting it to -1
 SELECT
     pg_catalog.pg_set_relation_stats(
@@ -1414,6 +1454,19 @@ SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import.complex_type,
 UNION ALL
 SELECT 4, 'four', NULL, int4range(0,100), NULL;
 CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
+-- Test for proper locking
+SELECT * FROM pg_catalog.pg_restore_relation_stats(
+    'relation', 'stats_import.is_odd'::regclass,
+    'version', '180000'::integer,
+    'relpages', '11'::integer,
+    'reltuples', '10000'::real,
+    'relallvisible', '0'::integer
+);
+ pg_restore_relation_stats 
+---------------------------
+ t
+(1 row)
+
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 CREATE TABLE stats_import.test_clone ( LIKE stats_import.test )
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index d3058bf8f6..23e85fc6ba 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -64,6 +64,19 @@ SELECT relpages, reltuples, relallvisible
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
+CREATE INDEX test_i ON stats_import.test(id);
+
+-- regular indexes have special case locking rules
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test_i'::regclass,
+        relpages => 18::integer);
+
+SELECT
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.test_i'::regclass,
+        'relpages', 19::integer );
+
 -- positional arguments
 SELECT
     pg_catalog.pg_set_relation_stats(
@@ -127,6 +140,8 @@ CREATE TABLE stats_import.part_child_1
   FOR VALUES FROM (0) TO (10)
   WITH (autovacuum_enabled = false);
 
+CREATE INDEX part_parent_i ON stats_import.part_parent(i);
+
 ANALYZE stats_import.part_parent;
 
 SELECT relpages
@@ -140,6 +155,17 @@ SELECT
         relation => 'stats_import.part_parent'::regclass,
         relpages => 2::integer);
 
+-- Partitioned indexes aren't analyzed but it is possible to set stats.
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.part_parent_i'::regclass,
+        relpages => 2::integer);
+
+SELECT
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.part_parent_i'::regclass,
+        'relpages', 2::integer);
+
 -- nothing stops us from setting it to -1
 SELECT
     pg_catalog.pg_set_relation_stats(
@@ -1062,6 +1088,16 @@ SELECT 4, 'four', NULL, int4range(0,100), NULL;
 
 CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
 
+
+-- Test for proper locking
+SELECT * FROM pg_catalog.pg_restore_relation_stats(
+    'relation', 'stats_import.is_odd'::regclass,
+    'version', '180000'::integer,
+    'relpages', '11'::integer,
+    'reltuples', '10000'::real,
+    'relallvisible', '0'::integer
+);
+
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 

base-commit: 9e020050b8fa8e184bc1d58e6a4bc1edfa76cb8c
-- 
2.48.1

v46-0002-Enable-dumping-of-table-index-stats-in-pg_dump.patchtext/x-patch; charset=US-ASCII; name=v46-0002-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From 983d67a266ce3f66df7e8e8579db38b13eaf9816 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH v46 2/2] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will generate a statement that
calls pg_set_relation_stats(), and it will generate a series of
statements that call pg_set_attribute_stats(), one per attribute that
has a corresponding row in pg_statistic. These statements will restore
the statistics of the current system onto the destination system.

Adds the command-line options -X / --statistics-only, which are mutually
exclusive to --schema-only and --data-only.

Statistics are not dumped when --schema-only is specified, except during
a binary upgrade.

As is the pattern with pg_dump options, statistics can be disabled using
--no-statistics.

Table statistics are dumped in the data section. This is true even if
dumping stats in a binary upgrade. Index statistics are dumped in the
post-data section.

Add --no-data option.  This option is useful for situations where
someone wishes to test query plans from a production database without
copying production data.

This also makes the corresponding change to the simulated pg_upgrade in
the TAP tests for pg_dump.

Add --no-schema option to pg_dump, etc.  Previously, users could use
--data-only when they wanted to suppress schema from a dump. However,
that no longer makes sense now that the data/schema binary has become
the data/schema/statistics trinary.
---
 src/bin/pg_dump/pg_backup.h           |  10 +-
 src/bin/pg_dump/pg_backup_archiver.c  |  97 +++++--
 src/bin/pg_dump/pg_backup_archiver.h  |   3 +-
 src/bin/pg_dump/pg_backup_directory.c |   2 +-
 src/bin/pg_dump/pg_dump.c             | 390 +++++++++++++++++++++++++-
 src/bin/pg_dump/pg_dump.h             |  11 +
 src/bin/pg_dump/pg_dump_sort.c        |  36 ++-
 src/bin/pg_dump/pg_dumpall.c          |   5 +
 src/bin/pg_dump/pg_restore.c          |  31 +-
 src/bin/pg_dump/t/001_basic.pl        |  18 ++
 src/bin/pg_dump/t/002_pg_dump.pl      | 111 +++++++-
 src/bin/pg_upgrade/dump.c             |   6 +-
 src/bin/pg_upgrade/option.c           |  12 +
 src/bin/pg_upgrade/pg_upgrade.h       |   1 +
 doc/src/sgml/ref/pg_dump.sgml         |  69 ++++-
 doc/src/sgml/ref/pg_dumpall.sgml      |  38 +++
 doc/src/sgml/ref/pg_restore.sgml      |  47 +++-
 doc/src/sgml/ref/pgupgrade.sgml       |  18 ++
 src/tools/pgindent/typedefs.list      |   1 +
 19 files changed, 841 insertions(+), 65 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index f0f19bb0b2..3fa1474fad 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -110,9 +110,12 @@ typedef struct _restoreOptions
 	int			column_inserts;
 	int			if_exists;
 	int			no_comments;	/* Skip comments */
+	int			no_data;			/* Skip data */
 	int			no_publications;	/* Skip publication entries */
+	int			no_schema;			/* Skip schema generation */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
@@ -160,6 +163,7 @@ typedef struct _restoreOptions
 	/* flags derived from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } RestoreOptions;
 
 typedef struct _dumpOptions
@@ -179,8 +183,11 @@ typedef struct _dumpOptions
 	int			column_inserts;
 	int			if_exists;
 	int			no_comments;
-	int			no_security_labels;
+	int			no_data;
 	int			no_publications;
+	int			no_schema;
+	int			no_security_labels;
+	int			no_statistics;
 	int			no_subscriptions;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
@@ -208,6 +215,7 @@ typedef struct _dumpOptions
 	/* flags derived from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } DumpOptions;
 
 /*
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 707a3fc844..cfa1349b8a 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -46,6 +46,12 @@
 #define TEXT_DUMP_HEADER "--\n-- PostgreSQL database dump\n--\n\n"
 #define TEXT_DUMPALL_HEADER "--\n-- PostgreSQL database cluster dump\n--\n\n"
 
+typedef enum entryType
+{
+	default_entry,
+	data_entry,
+	statistics_entry
+}			entryType;
 
 static ArchiveHandle *_allocAH(const char *FileSpec, const ArchiveFormat fmt,
 							   const pg_compress_specification compression_spec,
@@ -53,7 +59,7 @@ static ArchiveHandle *_allocAH(const char *FileSpec, const ArchiveFormat fmt,
 							   SetupWorkerPtrType setupWorkerPtr,
 							   DataDirSyncMethod sync_method);
 static void _getObjectDescription(PQExpBuffer buf, const TocEntry *te);
-static void _printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData);
+static void _printTocEntry(ArchiveHandle *AH, TocEntry *te, entryType entry_type);
 static char *sanitize_line(const char *str, bool want_hyphen);
 static void _doSetFixedOutputState(ArchiveHandle *AH);
 static void _doSetSessionAuth(ArchiveHandle *AH, const char *user);
@@ -149,6 +155,7 @@ InitDumpOptions(DumpOptions *opts)
 	opts->dumpSections = DUMP_UNSECTIONED;
 	opts->dumpSchema = true;
 	opts->dumpData = true;
+	opts->dumpStatistics = true;
 }
 
 /*
@@ -169,9 +176,10 @@ dumpOptionsFromRestoreOptions(RestoreOptions *ropt)
 	dopt->outputClean = ropt->dropSchema;
 	dopt->dumpData = ropt->dumpData;
 	dopt->dumpSchema = ropt->dumpSchema;
+	dopt->dumpSections = ropt->dumpSections;
+	dopt->dumpStatistics = ropt->dumpStatistics;
 	dopt->if_exists = ropt->if_exists;
 	dopt->column_inserts = ropt->column_inserts;
-	dopt->dumpSections = ropt->dumpSections;
 	dopt->aclsSkip = ropt->aclsSkip;
 	dopt->outputSuperuser = ropt->superuser;
 	dopt->outputCreateDB = ropt->createDB;
@@ -186,6 +194,9 @@ dumpOptionsFromRestoreOptions(RestoreOptions *ropt)
 	dopt->no_publications = ropt->no_publications;
 	dopt->no_security_labels = ropt->no_security_labels;
 	dopt->no_subscriptions = ropt->no_subscriptions;
+	dopt->no_data = ropt->no_data;
+	dopt->no_schema = ropt->no_schema;
+	dopt->no_statistics = ropt->no_statistics;
 	dopt->lockWaitTimeout = ropt->lockWaitTimeout;
 	dopt->include_everything = ropt->include_everything;
 	dopt->enable_row_security = ropt->enable_row_security;
@@ -418,30 +429,30 @@ RestoreArchive(Archive *AHX)
 	}
 
 	/*
-	 * Work out if we have an implied data-only restore. This can happen if
-	 * the dump was data only or if the user has used a toc list to exclude
-	 * all of the schema data. All we do is look for schema entries - if none
-	 * are found then we unset the dumpSchema flag.
+	 * Work out if we have an schema-less restore. This can happen if the dump
+	 * was data-only or statistics-only or no-schema or if the user has used a
+	 * toc list to exclude all of the schema data. All we do is look for
+	 * schema entries - if none are found then we unset the dumpSchema flag.
 	 *
 	 * We could scan for wanted TABLE entries, but that is not the same as
 	 * data-only. At this stage, it seems unnecessary (6-Mar-2001).
 	 */
 	if (ropt->dumpSchema)
 	{
-		int			impliedDataOnly = 1;
+		bool		no_schema_found = true;
 
 		for (te = AH->toc->next; te != AH->toc; te = te->next)
 		{
 			if ((te->reqs & REQ_SCHEMA) != 0)
-			{					/* It's schema, and it's wanted */
-				impliedDataOnly = 0;
+			{
+				no_schema_found = false;
 				break;
 			}
 		}
-		if (impliedDataOnly)
+		if (no_schema_found)
 		{
 			ropt->dumpSchema = false;
-			pg_log_info("implied data-only restore");
+			pg_log_info("implied no-schema restore");
 		}
 	}
 
@@ -739,7 +750,7 @@ RestoreArchive(Archive *AHX)
 
 		for (te = AH->toc->next; te != AH->toc; te = te->next)
 		{
-			if ((te->reqs & (REQ_SCHEMA | REQ_DATA)) == 0)
+			if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) == 0)
 				continue;		/* ignore if not to be dumped at all */
 
 			switch (_tocEntryRestorePass(te))
@@ -760,7 +771,7 @@ RestoreArchive(Archive *AHX)
 		{
 			for (te = AH->toc->next; te != AH->toc; te = te->next)
 			{
-				if ((te->reqs & (REQ_SCHEMA | REQ_DATA)) != 0 &&
+				if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) != 0 &&
 					_tocEntryRestorePass(te) == RESTORE_PASS_ACL)
 					(void) restore_toc_entry(AH, te, false);
 			}
@@ -770,7 +781,7 @@ RestoreArchive(Archive *AHX)
 		{
 			for (te = AH->toc->next; te != AH->toc; te = te->next)
 			{
-				if ((te->reqs & (REQ_SCHEMA | REQ_DATA)) != 0 &&
+				if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) != 0 &&
 					_tocEntryRestorePass(te) == RESTORE_PASS_POST_ACL)
 					(void) restore_toc_entry(AH, te, false);
 			}
@@ -869,7 +880,7 @@ restore_toc_entry(ArchiveHandle *AH, TocEntry *te, bool is_parallel)
 			pg_log_info("creating %s \"%s\"",
 						te->desc, te->tag);
 
-		_printTocEntry(AH, te, false);
+		_printTocEntry(AH, te, default_entry);
 		defnDumped = true;
 
 		if (strcmp(te->desc, "TABLE") == 0)
@@ -938,7 +949,7 @@ restore_toc_entry(ArchiveHandle *AH, TocEntry *te, bool is_parallel)
 			 */
 			if (AH->PrintTocDataPtr != NULL)
 			{
-				_printTocEntry(AH, te, true);
+				_printTocEntry(AH, te, data_entry);
 
 				if (strcmp(te->desc, "BLOBS") == 0 ||
 					strcmp(te->desc, "BLOB COMMENTS") == 0)
@@ -1036,15 +1047,21 @@ restore_toc_entry(ArchiveHandle *AH, TocEntry *te, bool is_parallel)
 		{
 			/* If we haven't already dumped the defn part, do so now */
 			pg_log_info("executing %s %s", te->desc, te->tag);
-			_printTocEntry(AH, te, false);
+			_printTocEntry(AH, te, default_entry);
 		}
 	}
 
+	/*
+	 * If it has a statistics component that we want, then process that
+	 */
+	if ((reqs & REQ_STATS) != 0)
+		_printTocEntry(AH, te, statistics_entry);
+
 	/*
 	 * If we emitted anything for this TOC entry, that counts as one action
 	 * against the transaction-size limit.  Commit if it's time to.
 	 */
-	if ((reqs & (REQ_SCHEMA | REQ_DATA)) != 0 && ropt->txn_size > 0)
+	if ((reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) != 0 && ropt->txn_size > 0)
 	{
 		if (++AH->txnCount >= ropt->txn_size)
 		{
@@ -1084,6 +1101,7 @@ NewRestoreOptions(void)
 	opts->compression_spec.level = 0;
 	opts->dumpSchema = true;
 	opts->dumpData = true;
+	opts->dumpStatistics = true;
 
 	return opts;
 }
@@ -1329,7 +1347,7 @@ PrintTOCSummary(Archive *AHX)
 		te->reqs = _tocEntryRequired(te, curSection, AH);
 		/* Now, should we print it? */
 		if (ropt->verbose ||
-			(te->reqs & (REQ_SCHEMA | REQ_DATA)) != 0)
+			(te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) != 0)
 		{
 			char	   *sanitized_name;
 			char	   *sanitized_schema;
@@ -2582,7 +2600,7 @@ WriteToc(ArchiveHandle *AH)
 	tocCount = 0;
 	for (te = AH->toc->next; te != AH->toc; te = te->next)
 	{
-		if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_SPECIAL)) != 0)
+		if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS | REQ_SPECIAL)) != 0)
 			tocCount++;
 	}
 
@@ -2592,7 +2610,7 @@ WriteToc(ArchiveHandle *AH)
 
 	for (te = AH->toc->next; te != AH->toc; te = te->next)
 	{
-		if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_SPECIAL)) == 0)
+		if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS | REQ_SPECIAL)) == 0)
 			continue;
 
 		WriteInt(AH, te->dumpId);
@@ -2918,6 +2936,14 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 		strcmp(te->desc, "SEARCHPATH") == 0)
 		return REQ_SPECIAL;
 
+	if (strcmp(te->desc, "STATISTICS DATA") == 0)
+	{
+		if (!ropt->dumpStatistics)
+			return 0;
+		else
+			res = REQ_STATS;
+	}
+
 	/*
 	 * DATABASE and DATABASE PROPERTIES also have a special rule: they are
 	 * restored in createDB mode, and not restored otherwise, independently of
@@ -2962,6 +2988,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's statistics and we don't want statistics, maybe ignore it */
+	if (!ropt->dumpStatistics && strcmp(te->desc, "STATISTICS DATA") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2991,6 +3021,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS DATA") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
@@ -3107,6 +3138,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 		}
 	}
 
+
 	/*
 	 * Determine whether the TOC entry contains schema and/or data components,
 	 * and mask off inapplicable REQ bits.  If it had a dataDumper, assume
@@ -3172,12 +3204,12 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 				strncmp(te->tag, "LARGE OBJECT", 12) == 0) ||
 			   (strcmp(te->desc, "SECURITY LABEL") == 0 &&
 				strncmp(te->tag, "LARGE OBJECT", 12) == 0))))
-			res = res & REQ_SCHEMA;
+			res = res & (REQ_SCHEMA | REQ_STATS);
 	}
 
 	/* Mask it if we don't want schema */
 	if (!ropt->dumpSchema)
-		res = res & REQ_DATA;
+		res = res & (REQ_DATA | REQ_STATS);
 
 	return res;
 }
@@ -3729,7 +3761,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te)
  * will remain at default, until the matching ACL TOC entry is restored.
  */
 static void
-_printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData)
+_printTocEntry(ArchiveHandle *AH, TocEntry *te, entryType entry_type)
 {
 	RestoreOptions *ropt = AH->public.ropt;
 
@@ -3753,10 +3785,17 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData)
 		char	   *sanitized_schema;
 		char	   *sanitized_owner;
 
-		if (isData)
-			pfx = "Data for ";
-		else
-			pfx = "";
+		switch (entry_type)
+		{
+			case data_entry:
+				pfx = "Data for ";
+				break;
+			case statistics_entry:
+				pfx = "Statistics for ";
+				break;
+			default:
+				pfx = "";
+		}
 
 		ahprintf(AH, "--\n");
 		if (AH->public.verbose)
@@ -4324,7 +4363,7 @@ restore_toc_entries_parallel(ArchiveHandle *AH, ParallelState *pstate,
 		if (next_work_item != NULL)
 		{
 			/* If not to be restored, don't waste time launching a worker */
-			if ((next_work_item->reqs & (REQ_SCHEMA | REQ_DATA)) == 0)
+			if ((next_work_item->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) == 0)
 			{
 				pg_log_info("skipping item %d %s %s",
 							next_work_item->dumpId,
diff --git a/src/bin/pg_dump/pg_backup_archiver.h b/src/bin/pg_dump/pg_backup_archiver.h
index ce5ed1dd39..a2064f471e 100644
--- a/src/bin/pg_dump/pg_backup_archiver.h
+++ b/src/bin/pg_dump/pg_backup_archiver.h
@@ -209,7 +209,8 @@ typedef enum
 
 #define REQ_SCHEMA	0x01		/* want schema */
 #define REQ_DATA	0x02		/* want data */
-#define REQ_SPECIAL	0x04		/* for special TOC entries */
+#define REQ_STATS	0x04
+#define REQ_SPECIAL	0x08		/* for special TOC entries */
 
 struct _archiveHandle
 {
diff --git a/src/bin/pg_dump/pg_backup_directory.c b/src/bin/pg_dump/pg_backup_directory.c
index 240a1d4106..b2a841bb0f 100644
--- a/src/bin/pg_dump/pg_backup_directory.c
+++ b/src/bin/pg_dump/pg_backup_directory.c
@@ -780,7 +780,7 @@ _PrepParallelRestore(ArchiveHandle *AH)
 			continue;
 
 		/* We may ignore items not due to be restored */
-		if ((te->reqs & REQ_DATA) == 0)
+		if ((te->reqs & (REQ_DATA | REQ_STATS)) == 0)
 			continue;
 
 		/*
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 02e1fdf8f7..c92a31902a 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -431,6 +431,7 @@ main(int argc, char **argv)
 	DataDirSyncMethod sync_method = DATA_DIR_SYNC_METHOD_FSYNC;
 	bool		data_only = false;
 	bool		schema_only = false;
+	bool		statistics_only = false;
 
 	static DumpOptions dopt;
 
@@ -467,6 +468,7 @@ main(int argc, char **argv)
 		{"encoding", required_argument, NULL, 'E'},
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
+		{"statistics-only", no_argument, NULL, 'X'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -493,8 +495,11 @@ main(int argc, char **argv)
 		{"strict-names", no_argument, &strict_names, 1},
 		{"use-set-session-authorization", no_argument, &dopt.use_setsessauth, 1},
 		{"no-comments", no_argument, &dopt.no_comments, 1},
+		{"no-data", no_argument, &dopt.no_data, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
+		{"no-schema", no_argument, &dopt.no_schema, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -540,7 +545,7 @@ main(int argc, char **argv)
 
 	InitDumpOptions(&dopt);
 
-	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:RsS:t:T:U:vwWxZ:",
+	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:RsS:t:T:U:vwWxXZ:",
 							long_options, &optindex)) != -1)
 	{
 		switch (c)
@@ -614,6 +619,10 @@ main(int argc, char **argv)
 				dopt.cparams.pgport = pg_strdup(optarg);
 				break;
 
+			case 'X':			/* Dump statistics only */
+				statistics_only = true;
+				break;
+
 			case 'R':
 				/* no-op, still accepted for backwards compatibility */
 				break;
@@ -785,6 +794,17 @@ main(int argc, char **argv)
 
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (schema_only && statistics_only)
+		pg_fatal("options -s/--schema-only and -X/--statistics-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
+
+	if (data_only && dopt.no_data)
+		pg_fatal("options -a/--data-only and --no-data cannot be used together");
+	if (schema_only && dopt.no_schema)
+		pg_fatal("options -s/--schema-only and --no-schema cannot be used together");
+	if (statistics_only && dopt.no_statistics)
+		pg_fatal("options -X/--statistics-only and --no-statistics cannot be used together");
 
 	if (schema_only && foreign_servers_include_patterns.head != NULL)
 		pg_fatal("options -s/--schema-only and --include-foreign-data cannot be used together");
@@ -799,8 +819,9 @@ main(int argc, char **argv)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
 	/* set derivative flags */
-	dopt.dumpSchema = (!data_only);
-	dopt.dumpData = (!schema_only);
+	dopt.dumpData = data_only || (!schema_only && !statistics_only && !dopt.no_data);
+	dopt.dumpSchema = schema_only || (!data_only && !statistics_only && !dopt.no_schema);
+	dopt.dumpStatistics = statistics_only || (!data_only && !schema_only && !dopt.no_statistics);
 
 	/*
 	 * --inserts are already implied above if --column-inserts or
@@ -1100,6 +1121,7 @@ main(int argc, char **argv)
 	ropt->dropSchema = dopt.outputClean;
 	ropt->dumpData = dopt.dumpData;
 	ropt->dumpSchema = dopt.dumpSchema;
+	ropt->dumpStatistics = dopt.dumpStatistics;
 	ropt->if_exists = dopt.if_exists;
 	ropt->column_inserts = dopt.column_inserts;
 	ropt->dumpSections = dopt.dumpSections;
@@ -1178,7 +1200,7 @@ help(const char *progname)
 	printf(_("  -?, --help                   show this help, then exit\n"));
 
 	printf(_("\nOptions controlling the output content:\n"));
-	printf(_("  -a, --data-only              dump only the data, not the schema\n"));
+	printf(_("  -a, --data-only              dump only the data, not the schema or statistics\n"));
 	printf(_("  -b, --large-objects          include large objects in dump\n"));
 	printf(_("  --blobs                      (same as --large-objects, deprecated)\n"));
 	printf(_("  -B, --no-large-objects       exclude large objects in dump\n"));
@@ -1191,11 +1213,12 @@ help(const char *progname)
 	printf(_("  -N, --exclude-schema=PATTERN do NOT dump the specified schema(s)\n"));
 	printf(_("  -O, --no-owner               skip restoration of object ownership in\n"
 			 "                               plain-text format\n"));
-	printf(_("  -s, --schema-only            dump only the schema, no data\n"));
+	printf(_("  -s, --schema-only            dump only the schema, no data or statistics\n"));
 	printf(_("  -S, --superuser=NAME         superuser user name to use in plain-text format\n"));
 	printf(_("  -t, --table=PATTERN          dump only the specified table(s)\n"));
 	printf(_("  -T, --exclude-table=PATTERN  do NOT dump the specified table(s)\n"));
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
+	printf(_("  -X, --statistics-only        dump only the statistics, not schema or data\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
@@ -1220,8 +1243,11 @@ help(const char *progname)
 	printf(_("  --inserts                    dump data as INSERT commands, rather than COPY\n"));
 	printf(_("  --load-via-partition-root    load partitions via the root table\n"));
 	printf(_("  --no-comments                do not dump comment commands\n"));
+	printf(_("  --no-data                    do not dump data\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
+	printf(_("  --no-schema                  do not dump schema\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -6778,6 +6804,43 @@ getFuncs(Archive *fout)
 	destroyPQExpBuffer(query);
 }
 
+/*
+ * getRelationStatistics
+ *    register the statistics object as a dependent of the relation.
+ *
+ */
+static RelStatsInfo *
+getRelationStatistics(Archive *fout, DumpableObject *rel, char relkind)
+{
+	if ((relkind == RELKIND_RELATION) ||
+		(relkind == RELKIND_PARTITIONED_TABLE) ||
+		(relkind == RELKIND_INDEX) ||
+		(relkind == RELKIND_PARTITIONED_INDEX) ||
+		(relkind == RELKIND_MATVIEW))
+	{
+		RelStatsInfo	   *info = pg_malloc0(sizeof(RelStatsInfo));
+		DumpableObject	   *dobj = &info->dobj;
+
+		dobj->objType = DO_REL_STATS;
+		dobj->catId.tableoid = 0;
+		dobj->catId.oid = 0;
+		AssignDumpId(dobj);
+		dobj->dependencies = (DumpId *) pg_malloc(sizeof(DumpId));
+		dobj->dependencies[0] = rel->dumpId;
+		dobj->nDeps = 1;
+		dobj->allocDeps = 1;
+		dobj->components |= DUMP_COMPONENT_STATISTICS;
+		dobj->dump = rel->dump;
+		dobj->name = pg_strdup(rel->name);
+		dobj->namespace = rel->namespace;
+		info->relkind = relkind;
+		info->postponed_def = false;
+
+		return info;
+	}
+	return NULL;
+}
+
 /*
  * getTables
  *	  read all the tables (no indexes) in the system catalogs,
@@ -7203,6 +7266,8 @@ getTables(Archive *fout, int *numTables)
 				}
 			}
 		}
+		if (tblinfo[i].interesting || dopt->dumpStatistics)
+			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relkind);
 	}
 
 	if (query->len != 0)
@@ -7649,11 +7714,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 		for (int c = 0; c < numinds; c++, j++)
 		{
 			char		contype;
+			char		indexkind;
+			RelStatsInfo   *relstats;
 
 			indxinfo[j].dobj.objType = DO_INDEX;
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -7676,7 +7744,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			{
 				NULL, NULL
 			};
+
+			if (indxinfo[j].parentidx == 0)
+				indexkind = RELKIND_INDEX;
+			else
+				indexkind = RELKIND_PARTITIONED_INDEX;
+
 			contype = *(PQgetvalue(res, j, i_contype));
+			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, indexkind);
 
 			if (contype == 'p' || contype == 'u' || contype == 'x')
 			{
@@ -7710,6 +7785,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				constrinfo->separate = true;
 
 				indxinfo[j].indexconstraint = constrinfo->dobj.dumpId;
+				if (relstats != NULL)
+					addObjectDependency(&relstats->dobj, constrinfo->dobj.dumpId);
 			}
 			else
 			{
@@ -10297,6 +10374,296 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * Tabular description of the parameters to pg_restore_relation_stats()
+ * param_name, param_type
+ */
+static const char *rel_stats_arginfo[][2] = {
+	{"relation", "regclass"},
+	{"version", "integer"},
+	{"relpages", "integer"},
+	{"reltuples", "real"},
+	{"relallvisible", "integer"},
+};
+
+/*
+ * Tabular description of the parameters to pg_restore_attribute_stats()
+ * param_name, param_type
+ */
+static const char *att_stats_arginfo[][2] = {
+	{"relation", "regclass"},
+	{"attname", "name"},
+	{"inherited", "boolean"},
+	{"version", "integer"},
+	{"null_frac", "float4"},
+	{"avg_width", "integer"},
+	{"n_distinct", "float4"},
+	{"most_common_vals", "text"},
+	{"most_common_freqs", "float4[]"},
+	{"histogram_bounds", "text"},
+	{"correlation", "float4"},
+	{"most_common_elems", "text"},
+	{"most_common_elem_freqs", "float4[]"},
+	{"elem_count_histogram", "float4[]"},
+	{"range_length_histogram", "text"},
+	{"range_empty_frac", "float4"},
+	{"range_bounds_histogram", "text"},
+};
+
+/*
+ * getRelStatsExportQuery --
+ *
+ * Generate a query that will fetch all relation (e.g. pg_class)
+ * stats for a given relation.
+ */
+static void
+getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "current_setting('server_version_num') AS version, "
+						 "c.relpages, c.reltuples, c.relallvisible "
+						 "FROM pg_class c "
+						 "JOIN pg_namespace n "
+						 "ON n.oid = c.relnamespace "
+						 "WHERE n.nspname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND c.relname = ");
+	appendStringLiteralAH(query, relname, fout);
+}
+
+/*
+ * getAttStatsExportQuery --
+ *
+ * Generate a query that will fetch all attribute (e.g. pg_statistic)
+ * stats for a given relation.
+ */
+static void
+getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "s.attname,"
+						 "s.inherited,"
+						 "current_setting('server_version_num') AS version, "
+						 "s.null_frac,"
+						 "s.avg_width,"
+						 "s.n_distinct,"
+						 "s.most_common_vals,"
+						 "s.most_common_freqs,"
+						 "s.histogram_bounds,"
+						 "s.correlation,"
+						 "s.most_common_elems,"
+						 "s.most_common_elem_freqs,"
+						 "s.elem_count_histogram,");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 "s.range_length_histogram,"
+							 "s.range_empty_frac,"
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(query,
+							 "NULL AS range_length_histogram,"
+							 "NULL AS range_empty_frac,"
+							 "NULL AS range_bounds_histogram ");
+
+	appendPQExpBufferStr(query,
+						 "FROM pg_stats s "
+						 "JOIN pg_namespace n "
+						 "ON n.nspname = s.schemaname "
+						 "JOIN pg_class c "
+						 "ON c.relname = s.tablename "
+						 "AND c.relnamespace = n.oid "
+						 "WHERE s.schemaname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND s.tablename = ");
+	appendStringLiteralAH(query, relname, fout);
+	appendPQExpBufferStr(query, " ORDER BY s.attname, s.inherited");
+}
+
+
+/*
+ * appendNamedArgument --
+ *
+ * Convenience routine for constructing parameters of the form:
+ * 'paraname', 'value'::type
+ */
+static void
+appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
+					const char *argval, const char *argtype)
+{
+	appendPQExpBufferStr(out, "\t");
+
+	appendStringLiteralAH(out, argname, fout);
+	appendPQExpBufferStr(out, ", ");
+
+	appendStringLiteralAH(out, argval, fout);
+	appendPQExpBuffer(out, "::%s", argtype);
+}
+
+/*
+ * appendRelStatsImport --
+ *
+ * Append a formatted pg_restore_relation_stats statement.
+ */
+static void
+appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	const char *sep = "";
+
+	if (PQntuples(res) == 0)
+		return;
+
+	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
+
+	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
+	{
+		const char *argname = rel_stats_arginfo[argno][0];
+		const char *argtype = rel_stats_arginfo[argno][1];
+		int			fieldno = PQfnumber(res, argname);
+
+		if (fieldno < 0)
+			pg_fatal("relation stats export query missing field '%s'",
+					 argname);
+
+		if (PQgetisnull(res, 0, fieldno))
+			continue;
+
+		appendPQExpBufferStr(out, sep);
+		appendNamedArgument(out, fout, argname, PQgetvalue(res, 0, fieldno), argtype);
+
+		sep = ",\n";
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+/*
+ * appendAttStatsImport --
+ *
+ * Append a series of formatted pg_restore_attribute_stats statements.
+ */
+static void
+appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	{
+		const char *sep = "";
+
+		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+		for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
+		{
+			const char *argname = att_stats_arginfo[argno][0];
+			const char *argtype = att_stats_arginfo[argno][1];
+			int			fieldno = PQfnumber(res, argname);
+
+			if (fieldno < 0)
+				pg_fatal("attribute stats export query missing field '%s'",
+						 argname);
+
+			if (PQgetisnull(res, rownum, fieldno))
+				continue;
+
+			appendPQExpBufferStr(out, sep);
+			appendNamedArgument(out, fout, argname, PQgetvalue(res, rownum, fieldno), argtype);
+			sep = ",\n";
+		}
+		appendPQExpBufferStr(out, "\n);\n");
+	}
+}
+
+/*
+ * Decide which section to use based on the relkind of the parent object.
+ *
+ * NB: materialized views may be postponed from SECTION_PRE_DATA to
+ * SECTION_POST_DATA to resolve some kinds of dependency problems. If so, the
+ * matview stats will also be postponed to SECTION_POST_DATA. See
+ * repairMatViewBoundaryMultiLoop().
+ */
+static teSection
+statisticsDumpSection(const RelStatsInfo *rsinfo)
+{
+	switch (rsinfo->relkind)
+	{
+		case RELKIND_RELATION:
+		case RELKIND_PARTITIONED_TABLE:
+		case RELKIND_MATVIEW:
+			return SECTION_DATA;
+		case RELKIND_INDEX:
+		case RELKIND_PARTITIONED_INDEX:
+			return SECTION_POST_DATA;
+		default:
+			pg_fatal("cannot dump statistics for relation kind '%c'",
+					 rsinfo->relkind);
+	}
+
+	return 0;				/* keep compiler quiet */
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	PGresult	   *res;
+	PQExpBuffer		query;
+	PQExpBuffer		out;
+	PQExpBuffer		tag;
+	DumpableObject *dobj = (DumpableObject *) &rsinfo->dobj;
+	DumpId		   *deps = NULL;
+	int				ndeps = 0;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
+	/* dependent on the relation definition, if doing schema */
+	if (fout->dopt->dumpSchema)
+	{
+		deps = dobj->dependencies;
+		ndeps = dobj->nDeps;
+	}
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", "STATISTICS DATA", fmtId(dobj->name));
+
+	query = createPQExpBuffer();
+	out = createPQExpBuffer();
+
+	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendRelStatsImport(out, fout, res);
+	PQclear(res);
+
+	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendAttStatsImport(out, fout, res);
+	PQclear(res);
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATISTICS DATA",
+							  .section = rsinfo->postponed_def ?
+								SECTION_POST_DATA :  statisticsDumpSection(rsinfo),
+							  .createStmt = out->data,
+							  .deps = deps,
+							  .nDeps = ndeps));
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(out);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -10745,6 +11112,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_SUBSCRIPTION_REL:
 			dumpSubscriptionTable(fout, (const SubRelInfo *) dobj);
 			break;
+		case DO_REL_STATS:
+			dumpRelationStats(fout, (const RelStatsInfo *) dobj);
+			break;
 		case DO_PRE_DATA_BOUNDARY:
 		case DO_POST_DATA_BOUNDARY:
 			/* never dumped, nothing to do */
@@ -18971,6 +19341,16 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 				/* must come after the pre-data boundary */
 				addObjectDependency(dobj, preDataBound->dumpId);
 				break;
+			case DO_REL_STATS:
+				/* stats section varies by parent object type, DATA or POST */
+				if (statisticsDumpSection((RelStatsInfo *) dobj) == SECTION_DATA)
+				{
+					addObjectDependency(dobj, preDataBound->dumpId);
+					addObjectDependency(postDataBound, dobj->dumpId);
+				}
+				else
+					addObjectDependency(dobj, postDataBound->dumpId);
+				break;
 		}
 	}
 }
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 7139c88a69..410c162a85 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -83,10 +83,13 @@ typedef enum
 	DO_PUBLICATION,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
+	DO_REL_STATS,
 	DO_SUBSCRIPTION,
 	DO_SUBSCRIPTION_REL,		/* see note for SubRelInfo */
 } DumpableObjectType;
 
+#define NUM_DUMPABLE_OBJECT_TYPES (DO_SUBSCRIPTION_REL + 1)
+
 /*
  * DumpComponents is a bitmask of the potentially dumpable components of
  * a database object: its core definition, plus optional attributes such
@@ -110,6 +113,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
@@ -430,6 +434,13 @@ typedef struct _indexAttachInfo
 	IndxInfo   *partitionIdx;	/* link to index on partition */
 } IndexAttachInfo;
 
+typedef struct _relStatsInfo
+{
+	DumpableObject	dobj;
+	char			relkind;		/* 'r', 'm', 'i', etc */
+	bool			postponed_def;  /* stats must be postponed into post-data */
+} RelStatsInfo;
+
 typedef struct _statsExtInfo
 {
 	DumpableObject dobj;
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index dc9a28924b..201eeedde7 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -81,6 +81,7 @@ enum dbObjectTypePriorities
 	PRIO_TABLE_DATA,
 	PRIO_SEQUENCE_SET,
 	PRIO_LARGE_OBJECT_DATA,
+	PRIO_STATISTICS_DATA_DATA,
 	PRIO_POST_DATA_BOUNDARY,	/* boundary! */
 	PRIO_CONSTRAINT,
 	PRIO_INDEX,
@@ -148,11 +149,12 @@ static const int dbObjectTypePriority[] =
 	[DO_PUBLICATION] = PRIO_PUBLICATION,
 	[DO_PUBLICATION_REL] = PRIO_PUBLICATION_REL,
 	[DO_PUBLICATION_TABLE_IN_SCHEMA] = PRIO_PUBLICATION_TABLE_IN_SCHEMA,
+	[DO_REL_STATS] = PRIO_STATISTICS_DATA_DATA,
 	[DO_SUBSCRIPTION] = PRIO_SUBSCRIPTION,
 	[DO_SUBSCRIPTION_REL] = PRIO_SUBSCRIPTION_REL,
 };
 
-StaticAssertDecl(lengthof(dbObjectTypePriority) == (DO_SUBSCRIPTION_REL + 1),
+StaticAssertDecl(lengthof(dbObjectTypePriority) == NUM_DUMPABLE_OBJECT_TYPES,
 				 "array length mismatch");
 
 static DumpId preDataBoundId;
@@ -801,11 +803,21 @@ repairMatViewBoundaryMultiLoop(DumpableObject *boundaryobj,
 {
 	/* remove boundary's dependency on object after it in loop */
 	removeObjectDependency(boundaryobj, nextobj->dumpId);
-	/* if that object is a matview, mark it as postponed into post-data */
+	/*
+	 * If that object is a matview or matview status, mark it as postponed into
+	 * post-data.
+	 */
 	if (nextobj->objType == DO_TABLE)
 	{
 		TableInfo  *nextinfo = (TableInfo *) nextobj;
 
+		if (nextinfo->relkind == RELKIND_MATVIEW)
+			nextinfo->postponed_def = true;
+	}
+	else if (nextobj->objType == DO_REL_STATS)
+	{
+		RelStatsInfo *nextinfo = (RelStatsInfo *) nextobj;
+
 		if (nextinfo->relkind == RELKIND_MATVIEW)
 			nextinfo->postponed_def = true;
 	}
@@ -1018,6 +1030,21 @@ repairDependencyLoop(DumpableObject **loop,
 					{
 						DumpableObject *nextobj;
 
+						nextobj = (j < nLoop - 1) ? loop[j + 1] : loop[0];
+						repairMatViewBoundaryMultiLoop(loop[j], nextobj);
+						return;
+					}
+				}
+			}
+			else if (loop[i]->objType == DO_REL_STATS &&
+					 ((RelStatsInfo *) loop[i])->relkind == RELKIND_MATVIEW)
+			{
+				for (j = 0; j < nLoop; j++)
+				{
+					if (loop[j]->objType == DO_POST_DATA_BOUNDARY)
+					{
+						DumpableObject *nextobj;
+
 						nextobj = (j < nLoop - 1) ? loop[j + 1] : loop[0];
 						repairMatViewBoundaryMultiLoop(loop[j], nextobj);
 						return;
@@ -1500,6 +1527,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "POST-DATA BOUNDARY  (ID %d)",
 					 obj->dumpId);
 			return;
+		case DO_REL_STATS:
+			snprintf(buf, bufsize,
+					 "RELATION STATISTICS FOR %s  (ID %d OID %u)",
+					 obj->name, obj->dumpId, obj->catId.oid);
+			return;
 	}
 	/* shouldn't get here */
 	snprintf(buf, bufsize,
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 396f79781c..7effb70490 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -103,6 +103,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -172,6 +173,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -451,6 +453,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -666,6 +670,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index c602272d7d..02af89bae1 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -63,6 +63,9 @@ main(int argc, char **argv)
 	int			numWorkers = 1;
 	Archive    *AH;
 	char	   *inputFileSpec;
+	bool		data_only = false;
+	bool		schema_only = false;
+	bool		statistics_only = false;
 	static int	disable_triggers = 0;
 	static int	enable_row_security = 0;
 	static int	if_exists = 0;
@@ -71,12 +74,13 @@ main(int argc, char **argv)
 	static int	outputNoTablespaces = 0;
 	static int	use_setsessauth = 0;
 	static int	no_comments = 0;
+	static int	no_data = 0;
 	static int	no_publications = 0;
+	static int	no_schema = 0;
 	static int	no_security_labels = 0;
+	static int  no_statistics = 0;
 	static int	no_subscriptions = 0;
 	static int	strict_names = 0;
-	bool		data_only = false;
-	bool		schema_only = false;
 
 	struct option cmdopts[] = {
 		{"clean", 0, NULL, 'c'},
@@ -108,6 +112,7 @@ main(int argc, char **argv)
 		{"username", 1, NULL, 'U'},
 		{"verbose", 0, NULL, 'v'},
 		{"single-transaction", 0, NULL, '1'},
+		{"statistics-only", no_argument, NULL, 'X'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -124,9 +129,12 @@ main(int argc, char **argv)
 		{"transaction-size", required_argument, NULL, 5},
 		{"use-set-session-authorization", no_argument, &use_setsessauth, 1},
 		{"no-comments", no_argument, &no_comments, 1},
+		{"no-data", no_argument, &no_data, 1},
 		{"no-publications", no_argument, &no_publications, 1},
+		{"no-schema", no_argument, &no_schema, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"filter", required_argument, NULL, 4},
 
 		{NULL, 0, NULL, 0}
@@ -271,6 +279,10 @@ main(int argc, char **argv)
 				opts->aclsSkip = 1;
 				break;
 
+			case 'X':			/* Restore statistics only */
+				statistics_only = true;
+				break;
+
 			case '1':			/* Restore data in a single transaction */
 				opts->single_txn = true;
 				opts->exit_on_error = true;
@@ -343,6 +355,10 @@ main(int argc, char **argv)
 
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
+	if (schema_only && statistics_only)
+		pg_fatal("options -s/--schema-only and -X/--statistics-only cannot be used together");
 
 	if (data_only && opts->dropSchema)
 		pg_fatal("options -c/--clean and -a/--data-only cannot be used together");
@@ -362,8 +378,9 @@ main(int argc, char **argv)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
 	/* set derivative flags */
-	opts->dumpSchema = (!data_only);
-	opts->dumpData = (!schema_only);
+	opts->dumpData = data_only || (!no_data && !schema_only && !statistics_only);
+	opts->dumpSchema = schema_only || (!no_schema && !data_only && !statistics_only);
+	opts->dumpStatistics = statistics_only || (!no_statistics && !data_only && !schema_only);
 
 	opts->disable_triggers = disable_triggers;
 	opts->enable_row_security = enable_row_security;
@@ -375,6 +392,8 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
+	opts->no_data = no_data;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
@@ -482,6 +501,7 @@ usage(const char *progname)
 	printf(_("  -t, --table=NAME             restore named relation (table, view, etc.)\n"));
 	printf(_("  -T, --trigger=NAME           restore named trigger\n"));
 	printf(_("  -x, --no-privileges          skip restoration of access privileges (grant/revoke)\n"));
+	printf(_("  -X, --statistics-only        restore only the statistics, not schema or data\n"));
 	printf(_("  -1, --single-transaction     restore as a single transaction\n"));
 	printf(_("  --disable-triggers           disable triggers during data-only restore\n"));
 	printf(_("  --enable-row-security        enable row security\n"));
@@ -489,10 +509,13 @@ usage(const char *progname)
 			 "                               in FILENAME\n"));
 	printf(_("  --if-exists                  use IF EXISTS when dropping objects\n"));
 	printf(_("  --no-comments                do not restore comment commands\n"));
+	printf(_("  --no-data                    do not restore data\n"));
 	printf(_("  --no-data-for-failed-tables  do not restore data of tables that could not be\n"
 			 "                               created\n"));
 	printf(_("  --no-publications            do not restore publications\n"));
+	printf(_("  --no-schema                  do not restore schema\n"));
 	printf(_("  --no-security-labels         do not restore security labels\n"));
+	printf(_("  --no-statistics              do not restore statistics\n"));
 	printf(_("  --no-subscriptions           do not restore subscriptions\n"));
 	printf(_("  --no-table-access-method     do not restore table access methods\n"));
 	printf(_("  --no-tablespaces             do not restore tablespace assignments\n"));
diff --git a/src/bin/pg_dump/t/001_basic.pl b/src/bin/pg_dump/t/001_basic.pl
index 214240f1ae..f29da06ed2 100644
--- a/src/bin/pg_dump/t/001_basic.pl
+++ b/src/bin/pg_dump/t/001_basic.pl
@@ -50,12 +50,30 @@ command_fails_like(
 	'pg_dump: options -s/--schema-only and -a/--data-only cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '-s', '-X' ],
+	qr/\Qpg_dump: error: options -s\/--schema-only and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: error: options -s/--schema-only and -X/--statistics-only cannot be used together'
+);
+
+command_fails_like(
+	[ 'pg_dump', '-a', '-X' ],
+	qr/\Qpg_dump: error: options -a\/--data-only and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: error: options -a/--data-only and -X/--statistics-only cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-s', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: options -s\/--schema-only and --include-foreign-data cannot be used together\E/,
 	'pg_dump: options -s/--schema-only and --include-foreign-data cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '--statistics-only', '--no-statistics' ],
+	qr/\Qpg_dump: error: options -X\/--statistics-only and --no-statistics cannot be used together\E/,
+	'pg_dump: options -X\/--statistics-only and --no-statistics cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-j2', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: option --include-foreign-data is not supported with parallel backup\E/,
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 805ba9f49f..d5f0f92dfa 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -65,7 +65,7 @@ my %pgdump_runs = (
 			'--format' => 'custom',
 			'--file' => "$tempdir/binary_upgrade.dump",
 			'--no-password',
-			'--schema-only',
+			'--no-data',
 			'--binary-upgrade',
 			'--dbname' => 'postgres',    # alternative way to specify database
 		],
@@ -710,6 +710,39 @@ my %pgdump_runs = (
 			'--no-large-objects',
 			'postgres',
 		],
+	},
+	no_statistics => {
+		dump_cmd => [
+			'pg_dump', '--no-sync',
+			"--file=$tempdir/no_statistics.sql",
+			'--no-statistics',
+			'postgres',
+		],
+	},
+	no_data_no_schema => {
+		dump_cmd => [
+			'pg_dump', '--no-sync',
+			"--file=$tempdir/no_data_no_schema.sql",
+			'--no-data',
+			'--no-schema',
+			'postgres',
+		],
+	},
+	statistics_only => {
+		dump_cmd => [
+			'pg_dump', '--no-sync',
+			"--file=$tempdir/statistics_only.sql",
+			'--statistics-only',
+			'postgres',
+		],
+	},
+	no_schema => {
+		dump_cmd => [
+			'pg_dump', '--no-sync',
+			"--file=$tempdir/no_schema.sql",
+			'--no-schema',
+			'postgres',
+		],
 	},);
 
 ###############################################################
@@ -776,6 +809,7 @@ my %full_runs = (
 	no_large_objects => 1,
 	no_owner => 1,
 	no_privs => 1,
+	no_statistics => 1,
 	no_table_access_method => 1,
 	pg_dumpall_dbprivs => 1,
 	pg_dumpall_exclude => 1,
@@ -977,6 +1011,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1390,6 +1425,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1411,6 +1447,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1432,6 +1469,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1598,6 +1636,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1751,6 +1790,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			only_dump_test_table => 1,
 			section_data => 1,
 		},
@@ -1778,6 +1818,7 @@ my %tests = (
 			data_only => 1,
 			exclude_test_table => 1,
 			exclude_test_table_data => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1798,7 +1839,10 @@ my %tests = (
 			\QCOPY dump_test.fk_reference_test_table (col1) FROM stdin;\E
 			\n(?:\d\n){5}\\\.\n
 			/xms,
-		like => { data_only => 1, },
+		like => {
+			data_only => 1,
+			no_schema => 1,
+		},
 	},
 
 	'COPY test_second_table' => {
@@ -1814,6 +1858,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1836,6 +1881,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1859,6 +1905,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1881,6 +1928,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1903,6 +1951,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -3299,6 +3348,7 @@ my %tests = (
 		like => {
 			%full_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 			only_dump_test_schema => 1,
 			test_schema_plus_large_objects => 1,
@@ -3469,6 +3519,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			only_dump_measurement => 1,
 			section_data => 1,
 			only_dump_test_schema => 1,
@@ -4351,6 +4402,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 			binary_upgrade => 1,
@@ -4651,6 +4703,61 @@ my %tests = (
 		},
 	},
 
+	#
+	# TABLE and MATVIEW stats will end up in SECTION_DATA.
+	# INDEX stats (expression columns only) will end up in SECTION_POST_DATA.
+	#
+	'statistics_import' => {
+		create_sql => '
+			CREATE TABLE dump_test.has_stats
+			AS SELECT g.g AS x, g.g / 2 AS y FROM generate_series(1,100) AS g(g);
+			CREATE MATERIALIZED VIEW dump_test.has_stats_mv AS SELECT * FROM dump_test.has_stats;
+			CREATE INDEX dup_test_post_data_ix ON dump_test.has_stats((x - 1));
+			ANALYZE dump_test.has_stats, dump_test.has_stats_mv;',
+		regexp => qr/pg_catalog.pg_restore_attribute_stats/,
+		like => {
+			%full_runs,
+			%dump_test_schema_runs,
+			no_data_no_schema => 1,
+			no_schema => 1,
+			section_data => 1,
+			section_post_data => 1,
+			statistics_only => 1,
+			},
+		unlike => {
+			exclude_dump_test_schema => 1,
+			no_statistics => 1,
+			only_dump_measurement => 1,
+			schema_only => 1,
+			},
+	},
+
+	#
+	# While attribute stats (aka pg_statistic stats) only appear for tables
+	# that have been analyzed, all tables will have relation stats because
+	# those come from pg_class.
+	#
+	'relstats_on_unanalyzed_tables' => {
+		regexp => qr/pg_catalog.pg_restore_relation_stats/,
+
+		like => {
+			%full_runs,
+			%dump_test_schema_runs,
+			no_data_no_schema => 1,
+			no_schema => 1,
+			only_dump_test_table => 1,
+			role => 1,
+			role_parallel => 1,
+			section_data => 1,
+			section_post_data => 1,
+			statistics_only => 1,
+			},
+		unlike => {
+			no_statistics => 1,
+			schema_only => 1,
+			},
+	},
+
 	# CREATE TABLE with partitioned table and various AMs.  One
 	# partition uses the same default as the parent, and a second
 	# uses its own AM.
diff --git a/src/bin/pg_upgrade/dump.c b/src/bin/pg_upgrade/dump.c
index 8ce0fa3020..a29cd2cca9 100644
--- a/src/bin/pg_upgrade/dump.c
+++ b/src/bin/pg_upgrade/dump.c
@@ -21,10 +21,11 @@ generate_old_dump(void)
 
 	/* run new pg_dumpall binary for globals */
 	exec_prog(UTILITY_LOG_FILE, NULL, true, true,
-			  "\"%s/pg_dumpall\" %s --globals-only --quote-all-identifiers "
+			  "\"%s/pg_dumpall\" %s %s --globals-only --quote-all-identifiers "
 			  "--binary-upgrade %s --no-sync -f \"%s/%s\"",
 			  new_cluster.bindir, cluster_conn_opts(&old_cluster),
 			  log_opts.verbose ? "--verbose" : "",
+			  user_opts.do_statistics ? "" : "--no-statistics",
 			  log_opts.dumpdir,
 			  GLOBALS_DUMP_FILE);
 	check_ok();
@@ -52,10 +53,11 @@ generate_old_dump(void)
 		snprintf(log_file_name, sizeof(log_file_name), DB_DUMP_LOG_FILE_MASK, old_db->db_oid);
 
 		parallel_exec_prog(log_file_name, NULL,
-						   "\"%s/pg_dump\" %s --schema-only --quote-all-identifiers "
+						   "\"%s/pg_dump\" %s --no-data %s --quote-all-identifiers "
 						   "--binary-upgrade --format=custom %s --no-sync --file=\"%s/%s\" %s",
 						   new_cluster.bindir, cluster_conn_opts(&old_cluster),
 						   log_opts.verbose ? "--verbose" : "",
+						   user_opts.do_statistics ? "" : "--no-statistics",
 						   log_opts.dumpdir,
 						   sql_file_name, escaped_connstr.data);
 
diff --git a/src/bin/pg_upgrade/option.c b/src/bin/pg_upgrade/option.c
index 108eb7a1ba..3b6c7ec994 100644
--- a/src/bin/pg_upgrade/option.c
+++ b/src/bin/pg_upgrade/option.c
@@ -60,6 +60,8 @@ parseCommandLine(int argc, char *argv[])
 		{"copy", no_argument, NULL, 2},
 		{"copy-file-range", no_argument, NULL, 3},
 		{"sync-method", required_argument, NULL, 4},
+		{"with-statistics", no_argument, NULL, 5},
+		{"no-statistics", no_argument, NULL, 6},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -70,6 +72,7 @@ parseCommandLine(int argc, char *argv[])
 
 	user_opts.do_sync = true;
 	user_opts.transfer_mode = TRANSFER_MODE_COPY;
+	user_opts.do_statistics = true;
 
 	os_info.progname = get_progname(argv[0]);
 
@@ -212,6 +215,13 @@ parseCommandLine(int argc, char *argv[])
 				user_opts.sync_method = pg_strdup(optarg);
 				break;
 
+			case 5:
+				user_opts.do_statistics = true;
+				break;
+			case 6:
+				user_opts.do_statistics = false;
+				break;
+
 			default:
 				fprintf(stderr, _("Try \"%s --help\" for more information.\n"),
 						os_info.progname);
@@ -306,7 +316,9 @@ usage(void)
 	printf(_("  --clone                       clone instead of copying files to new cluster\n"));
 	printf(_("  --copy                        copy files to new cluster (default)\n"));
 	printf(_("  --copy-file-range             copy files to new cluster with copy_file_range\n"));
+	printf(_("  --no-statistics               do not import statistics from old cluster\n"));
 	printf(_("  --sync-method=METHOD          set method for syncing files to disk\n"));
+	printf(_("  --with-statistics             import statistics from old cluster (default)\n"));
 	printf(_("  -?, --help                    show this help, then exit\n"));
 	printf(_("\n"
 			 "Before running pg_upgrade you must:\n"
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index 0cdd675e4f..3fe111fbde 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -327,6 +327,7 @@ typedef struct
 	int			jobs;			/* number of processes/threads to use */
 	char	   *socketdir;		/* directory to use for Unix sockets */
 	char	   *sync_method;
+	bool		do_statistics;	/* carry over statistics from old cluster */
 } UserOpts;
 
 typedef struct
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index 24fcc76d72..ecf7d3d632 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -123,7 +123,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Dump only the data, not the schema (data definitions).
+        Dump only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are dumped.
        </para>
 
@@ -141,13 +141,11 @@ PostgreSQL documentation
       <listitem>
        <para>
         Include large objects in the dump.  This is the default behavior
-        except when <option>--schema</option>, <option>--table</option>, or
-        <option>--schema-only</option> is specified.  The <option>-b</option>
-        switch is therefore only useful to add large objects to dumps
-        where a specific schema or table has been requested.  Note that
-        large objects are considered data and therefore will be included when
-        <option>--data-only</option> is used, but not
-        when <option>--schema-only</option> is.
+        except when <option>--schema</option>, <option>--table</option>,
+        <option>--schema-only</option>, or <option>--statistics-only</option>, or
+        <option>--no-data</option> is specified.  The <option>-b</option>
+        switch is therefore only useful to add large objects to dumps where a
+        specific schema or table has been requested.
        </para>
       </listitem>
      </varlistentry>
@@ -516,10 +514,11 @@ PostgreSQL documentation
       <term><option>--schema-only</option></term>
       <listitem>
        <para>
-        Dump only the object definitions (schema), not data.
+        Dump only the object definitions (schema), not data or statistics.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive to <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
         <option>--section=pre-data --section=post-data</option>.
@@ -652,6 +651,18 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Dump only the statistics, not the schema (data definitions) or data.
+        Statistics for tables, materialized views, and indexes are dumped.
+       </para>
+
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-Z <replaceable class="parameter">level</replaceable></option></term>
       <term><option>-Z <replaceable class="parameter">method</replaceable></option>[:<replaceable>detail</replaceable>]</term>
@@ -824,16 +835,17 @@ PostgreSQL documentation
       <term><option>--exclude-table-data=<replaceable class="parameter">pattern</replaceable></option></term>
       <listitem>
        <para>
-        Do not dump data for any tables matching <replaceable
+        Do not dump data or statistics for any tables matching <replaceable
         class="parameter">pattern</replaceable>. The pattern is
         interpreted according to the same rules as for <option>-t</option>.
         <option>--exclude-table-data</option> can be given more than once to
-        exclude tables matching any of several patterns. This option is
-        useful when you need the definition of a particular table even
-        though you do not need the data in it.
+        exclude tables matching any of several patterns. This option is useful
+        when you need the definition of a particular table even though you do
+        not need the data in it.
        </para>
        <para>
-        To exclude data for all tables in the database, see <option>--schema-only</option>.
+        To exclude data for all tables in the database, see <option>--schema-only</option>
+        or <option>--statistics-only</option>.
        </para>
       </listitem>
      </varlistentry>
@@ -1080,6 +1092,15 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-data</option></term>
+      <listitem>
+       <para>
+        Do not dump data.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-publications</option></term>
       <listitem>
@@ -1098,6 +1119,24 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-schema</option></term>
+      <listitem>
+       <para>
+        Do not dump schema (data definitions).
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_dumpall.sgml b/doc/src/sgml/ref/pg_dumpall.sgml
index 39d93c2c0e..3834756973 100644
--- a/doc/src/sgml/ref/pg_dumpall.sgml
+++ b/doc/src/sgml/ref/pg_dumpall.sgml
@@ -265,6 +265,17 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Dump only the statistics, not the schema (data definitions) or data.
+        Statistics for tables, materialized views, and indexes are dumped.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--binary-upgrade</option></term>
       <listitem>
@@ -422,6 +433,15 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-data</option></term>
+      <listitem>
+       <para>
+        Do not dump data.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-publications</option></term>
       <listitem>
@@ -447,6 +467,15 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-schema</option></term>
+      <listitem>
+       <para>
+        Do not dump schema (data definitions).
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-security-labels</option></term>
       <listitem>
@@ -456,6 +485,15 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index b8b27e1719..3c381db1aa 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -94,7 +94,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Restore only the data, not the schema (data definitions).
+        Restore only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are restored,
         if present in the archive.
        </para>
@@ -483,10 +483,11 @@ PostgreSQL documentation
         to the extent that schema entries are present in the archive.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive of <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
-        <option>--section=pre-data --section=post-data</option>.
+        <option>--section=pre-data --section=post-data --no-statistics</option>.
        </para>
        <para>
         (Do not confuse this with the <option>--schema</option> option, which
@@ -599,6 +600,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Restore only the statistics, not schema (data definitions) or data.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-1</option></term>
       <term><option>--single-transaction</option></term>
@@ -681,6 +692,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-data</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore data, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-data-for-failed-tables</option></term>
       <listitem>
@@ -713,6 +734,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-schema</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore schema (data definitions), even if
+        the archive contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-security-labels</option></term>
       <listitem>
@@ -723,6 +754,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore statistics, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pgupgrade.sgml b/doc/src/sgml/ref/pgupgrade.sgml
index 4777381dac..64a1ebd613 100644
--- a/doc/src/sgml/ref/pgupgrade.sgml
+++ b/doc/src/sgml/ref/pgupgrade.sgml
@@ -145,6 +145,24 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--with-statistics</option></term>
+      <listitem>
+       <para>
+        Restore statistics from the old cluster into the new cluster.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not restore statistics from the old cluster into the new cluster.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-o</option> <replaceable class="parameter">options</replaceable></term>
       <term><option>--old-options</option> <replaceable class="parameter">options</replaceable></term>
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9a3bee93de..f3351342a2 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2400,6 +2400,7 @@ RelMapFile
 RelMapping
 RelOptInfo
 RelOptKind
+RelStatsInfo
 RelToCheck
 RelToCluster
 RelabelType
-- 
2.48.1

#303Michael Paquier
michael@paquier.xyz
In reply to: Jeff Davis (#301)
Re: Statistics Import and Export

On Wed, Feb 05, 2025 at 08:45:06PM -0800, Jeff Davis wrote:

v45-0001 addresses this by locking both the partitioned index, as well
as its table, in ShareUpdateExclusive mode. That satisfies the in-place
update requirement to take a ShareUpdateExclusiveLock on the
partitioned index, while otherwise being the same as normal indexes
(and therefore unlikely to cause a problem if ANALYZE sets stats on
partitioned indexes in the future).

That means:
* For indexes: ShareUpdateExclusiveLock on table and AccessShareLock
on index
* For partitioned indexes: ShareUpdateExclusiveLock on table and
ShareUpdateExclusiveLock on index
* Otherwise, ShareupdateExclusiveLock on the relation

which makes sense to me. The v45-0001 patch itself could use some
cleanup, but I can take care of that at commit time if we agree on the
locking scheme.

Fine by me.

The regression tests of v45 and v46 are a bit fuzzy regarding the
tests around locking for partitioned tables. For example, with v46
applied on top of HEAD, if I manipulate the internals of the patch in
stats_check_arg_pair() so as we don't take a lock on the parent table,
then a make check is still happy and passes even if the internals are
clearly broken.

I would recommend to work a bit more the tests by updating the stats
of a relation with the SQL functions in a transaction and add some
queries on pg_locks for locktype = 'relation' that are able to check
the locks we are taking when running these operations (return pairs of
relation::regclass and mode, for example). Doing that for
non-partitioned relations is also something I would do, so as the
locking schema we are using is clearly tracked and that future
manipulations of the area would help one in tracking problems. Bonus
points: scans of pg_locks are cheap tests.
--
Michael

#304Corey Huinker
corey.huinker@gmail.com
In reply to: Michael Paquier (#303)
2 attachment(s)
Re: Statistics Import and Export

I would recommend to work a bit more the tests by updating the stats
of a relation with the SQL functions in a transaction and add some
queries on pg_locks for locktype = 'relation' that are able to check
the locks we are taking when running these operations (return pairs of
relation::regclass and mode, for example). Doing that for
non-partitioned relations is also something I would do, so as the
locking schema we are using is clearly tracked and that future
manipulations of the area would help one in tracking problems. Bonus
points: scans of pg_locks are cheap tests.
--
Michael

0001 - I've added pg_locks tests for a regular index and a partitioned
index.

0002 - I've done some documentation rewording, mostly wording changes where
behaviors surrounding data-only dumps are actually meant for any dump that
has all schema excluded.

Attachments:

v47-0001-Lock-table-first-when-setting-index-relation-sta.patchtext/x-patch; charset=US-ASCII; name=v47-0001-Lock-table-first-when-setting-index-relation-sta.patchDownload
From 2de404f8187466061469abfaf3a773290cce6af8 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Tue, 21 Jan 2025 11:52:58 -0500
Subject: [PATCH v47 1/2] Lock table first when setting index relation
 statistics.

Jian He reported [1] a missing lock relation bug in
pg_restore_relation_stats when restoring stats to an index.

This fix follows the steps for proper locking prior to an inplace update
of a relation as specified in aac2c9b4fde8, and mimics the locking
behavior of the analyze command, as well as correctly doing the ACL
checks against the underlying relation of an index rather than the index
itself.

There is no special case for partitioned indexes, so while we want to
the ACL checks against the underlying relation, we need to take out
the more restrictive ShareUpdateExclusiveLock on the partitioned index.

[1] https://www.postgresql.org/message-id/CACJufxGreTY7qsCV8%2BBkuv0p5SXGTScgh%3DD%2BDq6%3D%2B_%3DXTp7FWg%40mail.gmail.com
---
 src/backend/statistics/stat_utils.c        |  45 +++++++--
 src/test/regress/expected/stats_import.out | 106 +++++++++++++++++++++
 src/test/regress/sql/stats_import.sql      |  72 ++++++++++++++
 3 files changed, 217 insertions(+), 6 deletions(-)

diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c
index 0d446f55b01..f87007e72c2 100644
--- a/src/backend/statistics/stat_utils.c
+++ b/src/backend/statistics/stat_utils.c
@@ -17,13 +17,16 @@
 #include "postgres.h"
 
 #include "access/relation.h"
+#include "catalog/index.h"
 #include "catalog/pg_database.h"
 #include "funcapi.h"
 #include "miscadmin.h"
 #include "statistics/stat_utils.h"
+#include "storage/lmgr.h"
 #include "utils/acl.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
+#include "utils/lsyscache.h"
 #include "utils/rel.h"
 
 /*
@@ -126,18 +129,45 @@ stats_check_arg_pair(FunctionCallInfo fcinfo,
 void
 stats_lock_check_privileges(Oid reloid)
 {
-	Relation	rel = relation_open(reloid, ShareUpdateExclusiveLock);
-	const char	relkind = rel->rd_rel->relkind;
+	Relation	rel;
+	Oid			relation_oid = reloid;
+	Oid			index_oid = InvalidOid;
+	LOCKMODE	index_lockmode = AccessShareLock;
 
-	/* All of the types that can be used with ANALYZE, plus indexes */
-	switch (relkind)
+	/*
+	 * For indexes, we follow what do_analyze_rel() does so as to avoid any
+	 * deadlocks with analyze/vacuum, which is to take out a
+	 * ShareUpdateExclusive on table/matview first and then take an
+	 * AccessShareLock on the index itself. See check_inplace_rel_lock()
+	 * to see how this special case is implemented.
+	 *
+	 * Partitioned indexes do not have an exception in check_inplace_rel_lock(),
+	 * so we want to take a ShareUpdateExclusive lock there instead.
+	 */
+	switch(get_rel_relkind(reloid))
 	{
-		case RELKIND_RELATION:
 		case RELKIND_INDEX:
+			relation_oid = IndexGetRelation(reloid, false);
+			index_oid = reloid;
+			break;
+		case RELKIND_PARTITIONED_INDEX:
+			relation_oid = IndexGetRelation(reloid, false);
+			index_oid = reloid;
+			index_lockmode = ShareUpdateExclusiveLock;
+			break;
+		default:
+			break;
+	}
+
+	rel = relation_open(relation_oid, ShareUpdateExclusiveLock);
+
+	switch (rel->rd_rel->relkind)
+	{
+		/* All of the types that can be used with ANALYZE */
+		case RELKIND_RELATION:
 		case RELKIND_MATVIEW:
 		case RELKIND_FOREIGN_TABLE:
 		case RELKIND_PARTITIONED_TABLE:
-		case RELKIND_PARTITIONED_INDEX:
 			break;
 		default:
 			ereport(ERROR,
@@ -164,6 +194,9 @@ stats_lock_check_privileges(Oid reloid)
 						   NameStr(rel->rd_rel->relname));
 	}
 
+	if (OidIsValid(index_oid))
+		LockRelationOid(index_oid, index_lockmode);
+
 	relation_close(rel, NoLock);
 }
 
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index fb50da1cd83..a479300a68d 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -85,6 +85,46 @@ WHERE oid = 'stats_import.test'::regclass;
        17 |       400 |             4
 (1 row)
 
+CREATE INDEX test_i ON stats_import.test(id);
+BEGIN;
+-- regular indexes have special case locking rules
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test_i'::regclass,
+        relpages => 18::integer);
+ pg_set_relation_stats 
+-----------------------
+ 
+(1 row)
+
+SELECT mode
+FROM pg_locks
+WHERE relation = 'stats_import.test'::regclass
+AND granted;
+           mode           
+--------------------------
+ ShareUpdateExclusiveLock
+(1 row)
+
+SELECT mode
+FROM pg_locks
+WHERE relation = 'stats_import.test_i'::regclass
+AND granted;
+      mode       
+-----------------
+ AccessShareLock
+(1 row)
+
+COMMIT;
+SELECT
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.test_i'::regclass,
+        'relpages', 19::integer );
+ pg_restore_relation_stats 
+---------------------------
+ t
+(1 row)
+
 -- positional arguments
 SELECT
     pg_catalog.pg_set_relation_stats(
@@ -182,6 +222,7 @@ CREATE TABLE stats_import.part_child_1
   PARTITION OF stats_import.part_parent
   FOR VALUES FROM (0) TO (10)
   WITH (autovacuum_enabled = false);
+CREATE INDEX part_parent_i ON stats_import.part_parent(i);
 ANALYZE stats_import.part_parent;
 SELECT relpages
 FROM pg_class
@@ -193,6 +234,15 @@ WHERE oid = 'stats_import.part_parent'::regclass;
 
 -- although partitioned tables have no storage, setting relpages to a
 -- positive value is still allowed
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.part_parent_i'::regclass,
+        relpages => 2::integer);
+ pg_set_relation_stats 
+-----------------------
+ 
+(1 row)
+
 SELECT
     pg_catalog.pg_set_relation_stats(
         relation => 'stats_import.part_parent'::regclass,
@@ -202,6 +252,49 @@ SELECT
  
 (1 row)
 
+--
+-- Partitioned indexes aren't analyzed but it is possible to set stats.
+-- Partitioned indexes also have less relaxed locking rules than regular
+-- indexes.
+--
+BEGIN;
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.part_parent_i'::regclass,
+        relpages => 2::integer);
+ pg_set_relation_stats 
+-----------------------
+ 
+(1 row)
+
+SELECT mode
+FROM pg_locks
+WHERE relation = 'stats_import.part_parent'::regclass
+AND granted;
+           mode           
+--------------------------
+ ShareUpdateExclusiveLock
+(1 row)
+
+SELECT mode
+FROM pg_locks
+WHERE relation = 'stats_import.part_parent_i'::regclass
+AND granted;
+           mode           
+--------------------------
+ ShareUpdateExclusiveLock
+(1 row)
+
+COMMIT;
+SELECT
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.part_parent_i'::regclass,
+        'relpages', 2::integer);
+ pg_restore_relation_stats 
+---------------------------
+ t
+(1 row)
+
 -- nothing stops us from setting it to -1
 SELECT
     pg_catalog.pg_set_relation_stats(
@@ -1414,6 +1507,19 @@ SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import.complex_type,
 UNION ALL
 SELECT 4, 'four', NULL, int4range(0,100), NULL;
 CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
+-- Test for proper locking
+SELECT * FROM pg_catalog.pg_restore_relation_stats(
+    'relation', 'stats_import.is_odd'::regclass,
+    'version', '180000'::integer,
+    'relpages', '11'::integer,
+    'reltuples', '10000'::real,
+    'relallvisible', '0'::integer
+);
+ pg_restore_relation_stats 
+---------------------------
+ t
+(1 row)
+
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 CREATE TABLE stats_import.test_clone ( LIKE stats_import.test )
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index d3058bf8f6b..f5b8bd5fc58 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -64,6 +64,32 @@ SELECT relpages, reltuples, relallvisible
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
+CREATE INDEX test_i ON stats_import.test(id);
+
+BEGIN;
+-- regular indexes have special case locking rules
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.test_i'::regclass,
+        relpages => 18::integer);
+
+SELECT mode
+FROM pg_locks
+WHERE relation = 'stats_import.test'::regclass
+AND granted;
+
+SELECT mode
+FROM pg_locks
+WHERE relation = 'stats_import.test_i'::regclass
+AND granted;
+
+COMMIT;
+
+SELECT
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.test_i'::regclass,
+        'relpages', 19::integer );
+
 -- positional arguments
 SELECT
     pg_catalog.pg_set_relation_stats(
@@ -127,6 +153,8 @@ CREATE TABLE stats_import.part_child_1
   FOR VALUES FROM (0) TO (10)
   WITH (autovacuum_enabled = false);
 
+CREATE INDEX part_parent_i ON stats_import.part_parent(i);
+
 ANALYZE stats_import.part_parent;
 
 SELECT relpages
@@ -135,11 +163,45 @@ WHERE oid = 'stats_import.part_parent'::regclass;
 
 -- although partitioned tables have no storage, setting relpages to a
 -- positive value is still allowed
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.part_parent_i'::regclass,
+        relpages => 2::integer);
+
 SELECT
     pg_catalog.pg_set_relation_stats(
         relation => 'stats_import.part_parent'::regclass,
         relpages => 2::integer);
 
+--
+-- Partitioned indexes aren't analyzed but it is possible to set stats.
+-- Partitioned indexes also have less relaxed locking rules than regular
+-- indexes.
+--
+BEGIN;
+
+SELECT
+    pg_catalog.pg_set_relation_stats(
+        relation => 'stats_import.part_parent_i'::regclass,
+        relpages => 2::integer);
+
+SELECT mode
+FROM pg_locks
+WHERE relation = 'stats_import.part_parent'::regclass
+AND granted;
+
+SELECT mode
+FROM pg_locks
+WHERE relation = 'stats_import.part_parent_i'::regclass
+AND granted;
+
+COMMIT;
+
+SELECT
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.part_parent_i'::regclass,
+        'relpages', 2::integer);
+
 -- nothing stops us from setting it to -1
 SELECT
     pg_catalog.pg_set_relation_stats(
@@ -1062,6 +1124,16 @@ SELECT 4, 'four', NULL, int4range(0,100), NULL;
 
 CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
 
+
+-- Test for proper locking
+SELECT * FROM pg_catalog.pg_restore_relation_stats(
+    'relation', 'stats_import.is_odd'::regclass,
+    'version', '180000'::integer,
+    'relpages', '11'::integer,
+    'reltuples', '10000'::real,
+    'relallvisible', '0'::integer
+);
+
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 

base-commit: 3d17d7d7fb7a4603b48acb275b5a416f110db464
-- 
2.48.1

v47-0002-Enable-dumping-of-table-index-stats-in-pg_dump.patchtext/x-patch; charset=US-ASCII; name=v47-0002-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From 6e971f1528bdb95a5de86af63ac68e8367501c87 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH v47 2/2] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will generate a statement that
calls pg_set_relation_stats(), and it will generate a series of
statements that call pg_set_attribute_stats(), one per attribute that
has a corresponding row in pg_statistic. These statements will restore
the statistics of the current system onto the destination system.

Adds the command-line options -X / --statistics-only, which are mutually
exclusive to --schema-only and --data-only.

Statistics are not dumped when --schema-only is specified, except during
a binary upgrade.

As is the pattern with pg_dump options, statistics can be disabled using
--no-statistics.

Table statistics are dumped in the data section. This is true even if
dumping stats in a binary upgrade. Index statistics are dumped in the
post-data section.

Add --no-data option.  This option is useful for situations where
someone wishes to test query plans from a production database without
copying production data.

This also makes the corresponding change to the simulated pg_upgrade in
the TAP tests for pg_dump.

Add --no-schema option to pg_dump, etc.  Previously, users could use
--data-only when they wanted to suppress schema from a dump. However,
that no longer makes sense now that the data/schema binary has become
the data/schema/statistics trinary.
---
 src/bin/pg_dump/pg_backup.h           |  10 +-
 src/bin/pg_dump/pg_backup_archiver.c  |  97 +++++--
 src/bin/pg_dump/pg_backup_archiver.h  |   3 +-
 src/bin/pg_dump/pg_backup_directory.c |   2 +-
 src/bin/pg_dump/pg_dump.c             | 390 +++++++++++++++++++++++++-
 src/bin/pg_dump/pg_dump.h             |  11 +
 src/bin/pg_dump/pg_dump_sort.c        |  36 ++-
 src/bin/pg_dump/pg_dumpall.c          |   5 +
 src/bin/pg_dump/pg_restore.c          |  31 +-
 src/bin/pg_dump/t/001_basic.pl        |  18 ++
 src/bin/pg_dump/t/002_pg_dump.pl      | 111 +++++++-
 src/bin/pg_upgrade/dump.c             |   6 +-
 src/bin/pg_upgrade/option.c           |  12 +
 src/bin/pg_upgrade/pg_upgrade.h       |   1 +
 doc/src/sgml/ref/pg_dump.sgml         |  80 ++++--
 doc/src/sgml/ref/pg_dumpall.sgml      |  42 ++-
 doc/src/sgml/ref/pg_restore.sgml      |  49 +++-
 doc/src/sgml/ref/pgupgrade.sgml       |  18 ++
 src/tools/pgindent/typedefs.list      |   1 +
 19 files changed, 851 insertions(+), 72 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index f0f19bb0b29..3fa1474fad7 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -110,9 +110,12 @@ typedef struct _restoreOptions
 	int			column_inserts;
 	int			if_exists;
 	int			no_comments;	/* Skip comments */
+	int			no_data;			/* Skip data */
 	int			no_publications;	/* Skip publication entries */
+	int			no_schema;			/* Skip schema generation */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
@@ -160,6 +163,7 @@ typedef struct _restoreOptions
 	/* flags derived from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } RestoreOptions;
 
 typedef struct _dumpOptions
@@ -179,8 +183,11 @@ typedef struct _dumpOptions
 	int			column_inserts;
 	int			if_exists;
 	int			no_comments;
-	int			no_security_labels;
+	int			no_data;
 	int			no_publications;
+	int			no_schema;
+	int			no_security_labels;
+	int			no_statistics;
 	int			no_subscriptions;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
@@ -208,6 +215,7 @@ typedef struct _dumpOptions
 	/* flags derived from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } DumpOptions;
 
 /*
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 707a3fc844c..cfa1349b8a9 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -46,6 +46,12 @@
 #define TEXT_DUMP_HEADER "--\n-- PostgreSQL database dump\n--\n\n"
 #define TEXT_DUMPALL_HEADER "--\n-- PostgreSQL database cluster dump\n--\n\n"
 
+typedef enum entryType
+{
+	default_entry,
+	data_entry,
+	statistics_entry
+}			entryType;
 
 static ArchiveHandle *_allocAH(const char *FileSpec, const ArchiveFormat fmt,
 							   const pg_compress_specification compression_spec,
@@ -53,7 +59,7 @@ static ArchiveHandle *_allocAH(const char *FileSpec, const ArchiveFormat fmt,
 							   SetupWorkerPtrType setupWorkerPtr,
 							   DataDirSyncMethod sync_method);
 static void _getObjectDescription(PQExpBuffer buf, const TocEntry *te);
-static void _printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData);
+static void _printTocEntry(ArchiveHandle *AH, TocEntry *te, entryType entry_type);
 static char *sanitize_line(const char *str, bool want_hyphen);
 static void _doSetFixedOutputState(ArchiveHandle *AH);
 static void _doSetSessionAuth(ArchiveHandle *AH, const char *user);
@@ -149,6 +155,7 @@ InitDumpOptions(DumpOptions *opts)
 	opts->dumpSections = DUMP_UNSECTIONED;
 	opts->dumpSchema = true;
 	opts->dumpData = true;
+	opts->dumpStatistics = true;
 }
 
 /*
@@ -169,9 +176,10 @@ dumpOptionsFromRestoreOptions(RestoreOptions *ropt)
 	dopt->outputClean = ropt->dropSchema;
 	dopt->dumpData = ropt->dumpData;
 	dopt->dumpSchema = ropt->dumpSchema;
+	dopt->dumpSections = ropt->dumpSections;
+	dopt->dumpStatistics = ropt->dumpStatistics;
 	dopt->if_exists = ropt->if_exists;
 	dopt->column_inserts = ropt->column_inserts;
-	dopt->dumpSections = ropt->dumpSections;
 	dopt->aclsSkip = ropt->aclsSkip;
 	dopt->outputSuperuser = ropt->superuser;
 	dopt->outputCreateDB = ropt->createDB;
@@ -186,6 +194,9 @@ dumpOptionsFromRestoreOptions(RestoreOptions *ropt)
 	dopt->no_publications = ropt->no_publications;
 	dopt->no_security_labels = ropt->no_security_labels;
 	dopt->no_subscriptions = ropt->no_subscriptions;
+	dopt->no_data = ropt->no_data;
+	dopt->no_schema = ropt->no_schema;
+	dopt->no_statistics = ropt->no_statistics;
 	dopt->lockWaitTimeout = ropt->lockWaitTimeout;
 	dopt->include_everything = ropt->include_everything;
 	dopt->enable_row_security = ropt->enable_row_security;
@@ -418,30 +429,30 @@ RestoreArchive(Archive *AHX)
 	}
 
 	/*
-	 * Work out if we have an implied data-only restore. This can happen if
-	 * the dump was data only or if the user has used a toc list to exclude
-	 * all of the schema data. All we do is look for schema entries - if none
-	 * are found then we unset the dumpSchema flag.
+	 * Work out if we have an schema-less restore. This can happen if the dump
+	 * was data-only or statistics-only or no-schema or if the user has used a
+	 * toc list to exclude all of the schema data. All we do is look for
+	 * schema entries - if none are found then we unset the dumpSchema flag.
 	 *
 	 * We could scan for wanted TABLE entries, but that is not the same as
 	 * data-only. At this stage, it seems unnecessary (6-Mar-2001).
 	 */
 	if (ropt->dumpSchema)
 	{
-		int			impliedDataOnly = 1;
+		bool		no_schema_found = true;
 
 		for (te = AH->toc->next; te != AH->toc; te = te->next)
 		{
 			if ((te->reqs & REQ_SCHEMA) != 0)
-			{					/* It's schema, and it's wanted */
-				impliedDataOnly = 0;
+			{
+				no_schema_found = false;
 				break;
 			}
 		}
-		if (impliedDataOnly)
+		if (no_schema_found)
 		{
 			ropt->dumpSchema = false;
-			pg_log_info("implied data-only restore");
+			pg_log_info("implied no-schema restore");
 		}
 	}
 
@@ -739,7 +750,7 @@ RestoreArchive(Archive *AHX)
 
 		for (te = AH->toc->next; te != AH->toc; te = te->next)
 		{
-			if ((te->reqs & (REQ_SCHEMA | REQ_DATA)) == 0)
+			if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) == 0)
 				continue;		/* ignore if not to be dumped at all */
 
 			switch (_tocEntryRestorePass(te))
@@ -760,7 +771,7 @@ RestoreArchive(Archive *AHX)
 		{
 			for (te = AH->toc->next; te != AH->toc; te = te->next)
 			{
-				if ((te->reqs & (REQ_SCHEMA | REQ_DATA)) != 0 &&
+				if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) != 0 &&
 					_tocEntryRestorePass(te) == RESTORE_PASS_ACL)
 					(void) restore_toc_entry(AH, te, false);
 			}
@@ -770,7 +781,7 @@ RestoreArchive(Archive *AHX)
 		{
 			for (te = AH->toc->next; te != AH->toc; te = te->next)
 			{
-				if ((te->reqs & (REQ_SCHEMA | REQ_DATA)) != 0 &&
+				if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) != 0 &&
 					_tocEntryRestorePass(te) == RESTORE_PASS_POST_ACL)
 					(void) restore_toc_entry(AH, te, false);
 			}
@@ -869,7 +880,7 @@ restore_toc_entry(ArchiveHandle *AH, TocEntry *te, bool is_parallel)
 			pg_log_info("creating %s \"%s\"",
 						te->desc, te->tag);
 
-		_printTocEntry(AH, te, false);
+		_printTocEntry(AH, te, default_entry);
 		defnDumped = true;
 
 		if (strcmp(te->desc, "TABLE") == 0)
@@ -938,7 +949,7 @@ restore_toc_entry(ArchiveHandle *AH, TocEntry *te, bool is_parallel)
 			 */
 			if (AH->PrintTocDataPtr != NULL)
 			{
-				_printTocEntry(AH, te, true);
+				_printTocEntry(AH, te, data_entry);
 
 				if (strcmp(te->desc, "BLOBS") == 0 ||
 					strcmp(te->desc, "BLOB COMMENTS") == 0)
@@ -1036,15 +1047,21 @@ restore_toc_entry(ArchiveHandle *AH, TocEntry *te, bool is_parallel)
 		{
 			/* If we haven't already dumped the defn part, do so now */
 			pg_log_info("executing %s %s", te->desc, te->tag);
-			_printTocEntry(AH, te, false);
+			_printTocEntry(AH, te, default_entry);
 		}
 	}
 
+	/*
+	 * If it has a statistics component that we want, then process that
+	 */
+	if ((reqs & REQ_STATS) != 0)
+		_printTocEntry(AH, te, statistics_entry);
+
 	/*
 	 * If we emitted anything for this TOC entry, that counts as one action
 	 * against the transaction-size limit.  Commit if it's time to.
 	 */
-	if ((reqs & (REQ_SCHEMA | REQ_DATA)) != 0 && ropt->txn_size > 0)
+	if ((reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) != 0 && ropt->txn_size > 0)
 	{
 		if (++AH->txnCount >= ropt->txn_size)
 		{
@@ -1084,6 +1101,7 @@ NewRestoreOptions(void)
 	opts->compression_spec.level = 0;
 	opts->dumpSchema = true;
 	opts->dumpData = true;
+	opts->dumpStatistics = true;
 
 	return opts;
 }
@@ -1329,7 +1347,7 @@ PrintTOCSummary(Archive *AHX)
 		te->reqs = _tocEntryRequired(te, curSection, AH);
 		/* Now, should we print it? */
 		if (ropt->verbose ||
-			(te->reqs & (REQ_SCHEMA | REQ_DATA)) != 0)
+			(te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) != 0)
 		{
 			char	   *sanitized_name;
 			char	   *sanitized_schema;
@@ -2582,7 +2600,7 @@ WriteToc(ArchiveHandle *AH)
 	tocCount = 0;
 	for (te = AH->toc->next; te != AH->toc; te = te->next)
 	{
-		if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_SPECIAL)) != 0)
+		if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS | REQ_SPECIAL)) != 0)
 			tocCount++;
 	}
 
@@ -2592,7 +2610,7 @@ WriteToc(ArchiveHandle *AH)
 
 	for (te = AH->toc->next; te != AH->toc; te = te->next)
 	{
-		if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_SPECIAL)) == 0)
+		if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS | REQ_SPECIAL)) == 0)
 			continue;
 
 		WriteInt(AH, te->dumpId);
@@ -2918,6 +2936,14 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 		strcmp(te->desc, "SEARCHPATH") == 0)
 		return REQ_SPECIAL;
 
+	if (strcmp(te->desc, "STATISTICS DATA") == 0)
+	{
+		if (!ropt->dumpStatistics)
+			return 0;
+		else
+			res = REQ_STATS;
+	}
+
 	/*
 	 * DATABASE and DATABASE PROPERTIES also have a special rule: they are
 	 * restored in createDB mode, and not restored otherwise, independently of
@@ -2962,6 +2988,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's statistics and we don't want statistics, maybe ignore it */
+	if (!ropt->dumpStatistics && strcmp(te->desc, "STATISTICS DATA") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2991,6 +3021,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS DATA") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
@@ -3107,6 +3138,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 		}
 	}
 
+
 	/*
 	 * Determine whether the TOC entry contains schema and/or data components,
 	 * and mask off inapplicable REQ bits.  If it had a dataDumper, assume
@@ -3172,12 +3204,12 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 				strncmp(te->tag, "LARGE OBJECT", 12) == 0) ||
 			   (strcmp(te->desc, "SECURITY LABEL") == 0 &&
 				strncmp(te->tag, "LARGE OBJECT", 12) == 0))))
-			res = res & REQ_SCHEMA;
+			res = res & (REQ_SCHEMA | REQ_STATS);
 	}
 
 	/* Mask it if we don't want schema */
 	if (!ropt->dumpSchema)
-		res = res & REQ_DATA;
+		res = res & (REQ_DATA | REQ_STATS);
 
 	return res;
 }
@@ -3729,7 +3761,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te)
  * will remain at default, until the matching ACL TOC entry is restored.
  */
 static void
-_printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData)
+_printTocEntry(ArchiveHandle *AH, TocEntry *te, entryType entry_type)
 {
 	RestoreOptions *ropt = AH->public.ropt;
 
@@ -3753,10 +3785,17 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData)
 		char	   *sanitized_schema;
 		char	   *sanitized_owner;
 
-		if (isData)
-			pfx = "Data for ";
-		else
-			pfx = "";
+		switch (entry_type)
+		{
+			case data_entry:
+				pfx = "Data for ";
+				break;
+			case statistics_entry:
+				pfx = "Statistics for ";
+				break;
+			default:
+				pfx = "";
+		}
 
 		ahprintf(AH, "--\n");
 		if (AH->public.verbose)
@@ -4324,7 +4363,7 @@ restore_toc_entries_parallel(ArchiveHandle *AH, ParallelState *pstate,
 		if (next_work_item != NULL)
 		{
 			/* If not to be restored, don't waste time launching a worker */
-			if ((next_work_item->reqs & (REQ_SCHEMA | REQ_DATA)) == 0)
+			if ((next_work_item->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) == 0)
 			{
 				pg_log_info("skipping item %d %s %s",
 							next_work_item->dumpId,
diff --git a/src/bin/pg_dump/pg_backup_archiver.h b/src/bin/pg_dump/pg_backup_archiver.h
index ce5ed1dd395..a2064f471ed 100644
--- a/src/bin/pg_dump/pg_backup_archiver.h
+++ b/src/bin/pg_dump/pg_backup_archiver.h
@@ -209,7 +209,8 @@ typedef enum
 
 #define REQ_SCHEMA	0x01		/* want schema */
 #define REQ_DATA	0x02		/* want data */
-#define REQ_SPECIAL	0x04		/* for special TOC entries */
+#define REQ_STATS	0x04
+#define REQ_SPECIAL	0x08		/* for special TOC entries */
 
 struct _archiveHandle
 {
diff --git a/src/bin/pg_dump/pg_backup_directory.c b/src/bin/pg_dump/pg_backup_directory.c
index 240a1d41062..b2a841bb0ff 100644
--- a/src/bin/pg_dump/pg_backup_directory.c
+++ b/src/bin/pg_dump/pg_backup_directory.c
@@ -780,7 +780,7 @@ _PrepParallelRestore(ArchiveHandle *AH)
 			continue;
 
 		/* We may ignore items not due to be restored */
-		if ((te->reqs & REQ_DATA) == 0)
+		if ((te->reqs & (REQ_DATA | REQ_STATS)) == 0)
 			continue;
 
 		/*
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 520e1338c28..2a2397e20c6 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -431,6 +431,7 @@ main(int argc, char **argv)
 	DataDirSyncMethod sync_method = DATA_DIR_SYNC_METHOD_FSYNC;
 	bool		data_only = false;
 	bool		schema_only = false;
+	bool		statistics_only = false;
 
 	static DumpOptions dopt;
 
@@ -467,6 +468,7 @@ main(int argc, char **argv)
 		{"encoding", required_argument, NULL, 'E'},
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
+		{"statistics-only", no_argument, NULL, 'X'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -493,8 +495,11 @@ main(int argc, char **argv)
 		{"strict-names", no_argument, &strict_names, 1},
 		{"use-set-session-authorization", no_argument, &dopt.use_setsessauth, 1},
 		{"no-comments", no_argument, &dopt.no_comments, 1},
+		{"no-data", no_argument, &dopt.no_data, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
+		{"no-schema", no_argument, &dopt.no_schema, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -540,7 +545,7 @@ main(int argc, char **argv)
 
 	InitDumpOptions(&dopt);
 
-	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:RsS:t:T:U:vwWxZ:",
+	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:RsS:t:T:U:vwWxXZ:",
 							long_options, &optindex)) != -1)
 	{
 		switch (c)
@@ -614,6 +619,10 @@ main(int argc, char **argv)
 				dopt.cparams.pgport = pg_strdup(optarg);
 				break;
 
+			case 'X':			/* Dump statistics only */
+				statistics_only = true;
+				break;
+
 			case 'R':
 				/* no-op, still accepted for backwards compatibility */
 				break;
@@ -785,6 +794,17 @@ main(int argc, char **argv)
 
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (schema_only && statistics_only)
+		pg_fatal("options -s/--schema-only and -X/--statistics-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
+
+	if (data_only && dopt.no_data)
+		pg_fatal("options -a/--data-only and --no-data cannot be used together");
+	if (schema_only && dopt.no_schema)
+		pg_fatal("options -s/--schema-only and --no-schema cannot be used together");
+	if (statistics_only && dopt.no_statistics)
+		pg_fatal("options -X/--statistics-only and --no-statistics cannot be used together");
 
 	if (schema_only && foreign_servers_include_patterns.head != NULL)
 		pg_fatal("options -s/--schema-only and --include-foreign-data cannot be used together");
@@ -799,8 +819,9 @@ main(int argc, char **argv)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
 	/* set derivative flags */
-	dopt.dumpSchema = (!data_only);
-	dopt.dumpData = (!schema_only);
+	dopt.dumpData = data_only || (!schema_only && !statistics_only && !dopt.no_data);
+	dopt.dumpSchema = schema_only || (!data_only && !statistics_only && !dopt.no_schema);
+	dopt.dumpStatistics = statistics_only || (!data_only && !schema_only && !dopt.no_statistics);
 
 	/*
 	 * --inserts are already implied above if --column-inserts or
@@ -1100,6 +1121,7 @@ main(int argc, char **argv)
 	ropt->dropSchema = dopt.outputClean;
 	ropt->dumpData = dopt.dumpData;
 	ropt->dumpSchema = dopt.dumpSchema;
+	ropt->dumpStatistics = dopt.dumpStatistics;
 	ropt->if_exists = dopt.if_exists;
 	ropt->column_inserts = dopt.column_inserts;
 	ropt->dumpSections = dopt.dumpSections;
@@ -1178,7 +1200,7 @@ help(const char *progname)
 	printf(_("  -?, --help                   show this help, then exit\n"));
 
 	printf(_("\nOptions controlling the output content:\n"));
-	printf(_("  -a, --data-only              dump only the data, not the schema\n"));
+	printf(_("  -a, --data-only              dump only the data, not the schema or statistics\n"));
 	printf(_("  -b, --large-objects          include large objects in dump\n"));
 	printf(_("  --blobs                      (same as --large-objects, deprecated)\n"));
 	printf(_("  -B, --no-large-objects       exclude large objects in dump\n"));
@@ -1191,11 +1213,12 @@ help(const char *progname)
 	printf(_("  -N, --exclude-schema=PATTERN do NOT dump the specified schema(s)\n"));
 	printf(_("  -O, --no-owner               skip restoration of object ownership in\n"
 			 "                               plain-text format\n"));
-	printf(_("  -s, --schema-only            dump only the schema, no data\n"));
+	printf(_("  -s, --schema-only            dump only the schema, no data or statistics\n"));
 	printf(_("  -S, --superuser=NAME         superuser user name to use in plain-text format\n"));
 	printf(_("  -t, --table=PATTERN          dump only the specified table(s)\n"));
 	printf(_("  -T, --exclude-table=PATTERN  do NOT dump the specified table(s)\n"));
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
+	printf(_("  -X, --statistics-only        dump only the statistics, not schema or data\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
@@ -1220,8 +1243,11 @@ help(const char *progname)
 	printf(_("  --inserts                    dump data as INSERT commands, rather than COPY\n"));
 	printf(_("  --load-via-partition-root    load partitions via the root table\n"));
 	printf(_("  --no-comments                do not dump comment commands\n"));
+	printf(_("  --no-data                    do not dump data\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
+	printf(_("  --no-schema                  do not dump schema\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -6778,6 +6804,43 @@ getFuncs(Archive *fout)
 	destroyPQExpBuffer(query);
 }
 
+/*
+ * getRelationStatistics
+ *    register the statistics object as a dependent of the relation.
+ *
+ */
+static RelStatsInfo *
+getRelationStatistics(Archive *fout, DumpableObject *rel, char relkind)
+{
+	if ((relkind == RELKIND_RELATION) ||
+		(relkind == RELKIND_PARTITIONED_TABLE) ||
+		(relkind == RELKIND_INDEX) ||
+		(relkind == RELKIND_PARTITIONED_INDEX) ||
+		(relkind == RELKIND_MATVIEW))
+	{
+		RelStatsInfo	   *info = pg_malloc0(sizeof(RelStatsInfo));
+		DumpableObject	   *dobj = &info->dobj;
+
+		dobj->objType = DO_REL_STATS;
+		dobj->catId.tableoid = 0;
+		dobj->catId.oid = 0;
+		AssignDumpId(dobj);
+		dobj->dependencies = (DumpId *) pg_malloc(sizeof(DumpId));
+		dobj->dependencies[0] = rel->dumpId;
+		dobj->nDeps = 1;
+		dobj->allocDeps = 1;
+		dobj->components |= DUMP_COMPONENT_STATISTICS;
+		dobj->dump = rel->dump;
+		dobj->name = pg_strdup(rel->name);
+		dobj->namespace = rel->namespace;
+		info->relkind = relkind;
+		info->postponed_def = false;
+
+		return info;
+	}
+	return NULL;
+}
+
 /*
  * getTables
  *	  read all the tables (no indexes) in the system catalogs,
@@ -7203,6 +7266,8 @@ getTables(Archive *fout, int *numTables)
 				}
 			}
 		}
+		if (tblinfo[i].interesting || dopt->dumpStatistics)
+			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relkind);
 	}
 
 	if (query->len != 0)
@@ -7649,11 +7714,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 		for (int c = 0; c < numinds; c++, j++)
 		{
 			char		contype;
+			char		indexkind;
+			RelStatsInfo   *relstats;
 
 			indxinfo[j].dobj.objType = DO_INDEX;
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -7676,7 +7744,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			{
 				NULL, NULL
 			};
+
+			if (indxinfo[j].parentidx == 0)
+				indexkind = RELKIND_INDEX;
+			else
+				indexkind = RELKIND_PARTITIONED_INDEX;
+
 			contype = *(PQgetvalue(res, j, i_contype));
+			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, indexkind);
 
 			if (contype == 'p' || contype == 'u' || contype == 'x')
 			{
@@ -7710,6 +7785,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				constrinfo->separate = true;
 
 				indxinfo[j].indexconstraint = constrinfo->dobj.dumpId;
+				if (relstats != NULL)
+					addObjectDependency(&relstats->dobj, constrinfo->dobj.dumpId);
 			}
 			else
 			{
@@ -10297,6 +10374,296 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * Tabular description of the parameters to pg_restore_relation_stats()
+ * param_name, param_type
+ */
+static const char *rel_stats_arginfo[][2] = {
+	{"relation", "regclass"},
+	{"version", "integer"},
+	{"relpages", "integer"},
+	{"reltuples", "real"},
+	{"relallvisible", "integer"},
+};
+
+/*
+ * Tabular description of the parameters to pg_restore_attribute_stats()
+ * param_name, param_type
+ */
+static const char *att_stats_arginfo[][2] = {
+	{"relation", "regclass"},
+	{"attname", "name"},
+	{"inherited", "boolean"},
+	{"version", "integer"},
+	{"null_frac", "float4"},
+	{"avg_width", "integer"},
+	{"n_distinct", "float4"},
+	{"most_common_vals", "text"},
+	{"most_common_freqs", "float4[]"},
+	{"histogram_bounds", "text"},
+	{"correlation", "float4"},
+	{"most_common_elems", "text"},
+	{"most_common_elem_freqs", "float4[]"},
+	{"elem_count_histogram", "float4[]"},
+	{"range_length_histogram", "text"},
+	{"range_empty_frac", "float4"},
+	{"range_bounds_histogram", "text"},
+};
+
+/*
+ * getRelStatsExportQuery --
+ *
+ * Generate a query that will fetch all relation (e.g. pg_class)
+ * stats for a given relation.
+ */
+static void
+getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "current_setting('server_version_num') AS version, "
+						 "c.relpages, c.reltuples, c.relallvisible "
+						 "FROM pg_class c "
+						 "JOIN pg_namespace n "
+						 "ON n.oid = c.relnamespace "
+						 "WHERE n.nspname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND c.relname = ");
+	appendStringLiteralAH(query, relname, fout);
+}
+
+/*
+ * getAttStatsExportQuery --
+ *
+ * Generate a query that will fetch all attribute (e.g. pg_statistic)
+ * stats for a given relation.
+ */
+static void
+getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "s.attname,"
+						 "s.inherited,"
+						 "current_setting('server_version_num') AS version, "
+						 "s.null_frac,"
+						 "s.avg_width,"
+						 "s.n_distinct,"
+						 "s.most_common_vals,"
+						 "s.most_common_freqs,"
+						 "s.histogram_bounds,"
+						 "s.correlation,"
+						 "s.most_common_elems,"
+						 "s.most_common_elem_freqs,"
+						 "s.elem_count_histogram,");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 "s.range_length_histogram,"
+							 "s.range_empty_frac,"
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(query,
+							 "NULL AS range_length_histogram,"
+							 "NULL AS range_empty_frac,"
+							 "NULL AS range_bounds_histogram ");
+
+	appendPQExpBufferStr(query,
+						 "FROM pg_stats s "
+						 "JOIN pg_namespace n "
+						 "ON n.nspname = s.schemaname "
+						 "JOIN pg_class c "
+						 "ON c.relname = s.tablename "
+						 "AND c.relnamespace = n.oid "
+						 "WHERE s.schemaname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND s.tablename = ");
+	appendStringLiteralAH(query, relname, fout);
+	appendPQExpBufferStr(query, " ORDER BY s.attname, s.inherited");
+}
+
+
+/*
+ * appendNamedArgument --
+ *
+ * Convenience routine for constructing parameters of the form:
+ * 'paraname', 'value'::type
+ */
+static void
+appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
+					const char *argval, const char *argtype)
+{
+	appendPQExpBufferStr(out, "\t");
+
+	appendStringLiteralAH(out, argname, fout);
+	appendPQExpBufferStr(out, ", ");
+
+	appendStringLiteralAH(out, argval, fout);
+	appendPQExpBuffer(out, "::%s", argtype);
+}
+
+/*
+ * appendRelStatsImport --
+ *
+ * Append a formatted pg_restore_relation_stats statement.
+ */
+static void
+appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	const char *sep = "";
+
+	if (PQntuples(res) == 0)
+		return;
+
+	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
+
+	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
+	{
+		const char *argname = rel_stats_arginfo[argno][0];
+		const char *argtype = rel_stats_arginfo[argno][1];
+		int			fieldno = PQfnumber(res, argname);
+
+		if (fieldno < 0)
+			pg_fatal("relation stats export query missing field '%s'",
+					 argname);
+
+		if (PQgetisnull(res, 0, fieldno))
+			continue;
+
+		appendPQExpBufferStr(out, sep);
+		appendNamedArgument(out, fout, argname, PQgetvalue(res, 0, fieldno), argtype);
+
+		sep = ",\n";
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+/*
+ * appendAttStatsImport --
+ *
+ * Append a series of formatted pg_restore_attribute_stats statements.
+ */
+static void
+appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	{
+		const char *sep = "";
+
+		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+		for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
+		{
+			const char *argname = att_stats_arginfo[argno][0];
+			const char *argtype = att_stats_arginfo[argno][1];
+			int			fieldno = PQfnumber(res, argname);
+
+			if (fieldno < 0)
+				pg_fatal("attribute stats export query missing field '%s'",
+						 argname);
+
+			if (PQgetisnull(res, rownum, fieldno))
+				continue;
+
+			appendPQExpBufferStr(out, sep);
+			appendNamedArgument(out, fout, argname, PQgetvalue(res, rownum, fieldno), argtype);
+			sep = ",\n";
+		}
+		appendPQExpBufferStr(out, "\n);\n");
+	}
+}
+
+/*
+ * Decide which section to use based on the relkind of the parent object.
+ *
+ * NB: materialized views may be postponed from SECTION_PRE_DATA to
+ * SECTION_POST_DATA to resolve some kinds of dependency problems. If so, the
+ * matview stats will also be postponed to SECTION_POST_DATA. See
+ * repairMatViewBoundaryMultiLoop().
+ */
+static teSection
+statisticsDumpSection(const RelStatsInfo *rsinfo)
+{
+	switch (rsinfo->relkind)
+	{
+		case RELKIND_RELATION:
+		case RELKIND_PARTITIONED_TABLE:
+		case RELKIND_MATVIEW:
+			return SECTION_DATA;
+		case RELKIND_INDEX:
+		case RELKIND_PARTITIONED_INDEX:
+			return SECTION_POST_DATA;
+		default:
+			pg_fatal("cannot dump statistics for relation kind '%c'",
+					 rsinfo->relkind);
+	}
+
+	return 0;				/* keep compiler quiet */
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	PGresult	   *res;
+	PQExpBuffer		query;
+	PQExpBuffer		out;
+	PQExpBuffer		tag;
+	DumpableObject *dobj = (DumpableObject *) &rsinfo->dobj;
+	DumpId		   *deps = NULL;
+	int				ndeps = 0;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
+	/* dependent on the relation definition, if doing schema */
+	if (fout->dopt->dumpSchema)
+	{
+		deps = dobj->dependencies;
+		ndeps = dobj->nDeps;
+	}
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", "STATISTICS DATA", fmtId(dobj->name));
+
+	query = createPQExpBuffer();
+	out = createPQExpBuffer();
+
+	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendRelStatsImport(out, fout, res);
+	PQclear(res);
+
+	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendAttStatsImport(out, fout, res);
+	PQclear(res);
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATISTICS DATA",
+							  .section = rsinfo->postponed_def ?
+								SECTION_POST_DATA :  statisticsDumpSection(rsinfo),
+							  .createStmt = out->data,
+							  .deps = deps,
+							  .nDeps = ndeps));
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(out);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -10745,6 +11112,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_SUBSCRIPTION_REL:
 			dumpSubscriptionTable(fout, (const SubRelInfo *) dobj);
 			break;
+		case DO_REL_STATS:
+			dumpRelationStats(fout, (const RelStatsInfo *) dobj);
+			break;
 		case DO_PRE_DATA_BOUNDARY:
 		case DO_POST_DATA_BOUNDARY:
 			/* never dumped, nothing to do */
@@ -18974,6 +19344,16 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 				/* must come after the pre-data boundary */
 				addObjectDependency(dobj, preDataBound->dumpId);
 				break;
+			case DO_REL_STATS:
+				/* stats section varies by parent object type, DATA or POST */
+				if (statisticsDumpSection((RelStatsInfo *) dobj) == SECTION_DATA)
+				{
+					addObjectDependency(dobj, preDataBound->dumpId);
+					addObjectDependency(postDataBound, dobj->dumpId);
+				}
+				else
+					addObjectDependency(dobj, postDataBound->dumpId);
+				break;
 		}
 	}
 }
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 7139c88a69a..410c162a85d 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -83,10 +83,13 @@ typedef enum
 	DO_PUBLICATION,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
+	DO_REL_STATS,
 	DO_SUBSCRIPTION,
 	DO_SUBSCRIPTION_REL,		/* see note for SubRelInfo */
 } DumpableObjectType;
 
+#define NUM_DUMPABLE_OBJECT_TYPES (DO_SUBSCRIPTION_REL + 1)
+
 /*
  * DumpComponents is a bitmask of the potentially dumpable components of
  * a database object: its core definition, plus optional attributes such
@@ -110,6 +113,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
@@ -430,6 +434,13 @@ typedef struct _indexAttachInfo
 	IndxInfo   *partitionIdx;	/* link to index on partition */
 } IndexAttachInfo;
 
+typedef struct _relStatsInfo
+{
+	DumpableObject	dobj;
+	char			relkind;		/* 'r', 'm', 'i', etc */
+	bool			postponed_def;  /* stats must be postponed into post-data */
+} RelStatsInfo;
+
 typedef struct _statsExtInfo
 {
 	DumpableObject dobj;
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index dc9a28924bd..201eeedde73 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -81,6 +81,7 @@ enum dbObjectTypePriorities
 	PRIO_TABLE_DATA,
 	PRIO_SEQUENCE_SET,
 	PRIO_LARGE_OBJECT_DATA,
+	PRIO_STATISTICS_DATA_DATA,
 	PRIO_POST_DATA_BOUNDARY,	/* boundary! */
 	PRIO_CONSTRAINT,
 	PRIO_INDEX,
@@ -148,11 +149,12 @@ static const int dbObjectTypePriority[] =
 	[DO_PUBLICATION] = PRIO_PUBLICATION,
 	[DO_PUBLICATION_REL] = PRIO_PUBLICATION_REL,
 	[DO_PUBLICATION_TABLE_IN_SCHEMA] = PRIO_PUBLICATION_TABLE_IN_SCHEMA,
+	[DO_REL_STATS] = PRIO_STATISTICS_DATA_DATA,
 	[DO_SUBSCRIPTION] = PRIO_SUBSCRIPTION,
 	[DO_SUBSCRIPTION_REL] = PRIO_SUBSCRIPTION_REL,
 };
 
-StaticAssertDecl(lengthof(dbObjectTypePriority) == (DO_SUBSCRIPTION_REL + 1),
+StaticAssertDecl(lengthof(dbObjectTypePriority) == NUM_DUMPABLE_OBJECT_TYPES,
 				 "array length mismatch");
 
 static DumpId preDataBoundId;
@@ -801,11 +803,21 @@ repairMatViewBoundaryMultiLoop(DumpableObject *boundaryobj,
 {
 	/* remove boundary's dependency on object after it in loop */
 	removeObjectDependency(boundaryobj, nextobj->dumpId);
-	/* if that object is a matview, mark it as postponed into post-data */
+	/*
+	 * If that object is a matview or matview status, mark it as postponed into
+	 * post-data.
+	 */
 	if (nextobj->objType == DO_TABLE)
 	{
 		TableInfo  *nextinfo = (TableInfo *) nextobj;
 
+		if (nextinfo->relkind == RELKIND_MATVIEW)
+			nextinfo->postponed_def = true;
+	}
+	else if (nextobj->objType == DO_REL_STATS)
+	{
+		RelStatsInfo *nextinfo = (RelStatsInfo *) nextobj;
+
 		if (nextinfo->relkind == RELKIND_MATVIEW)
 			nextinfo->postponed_def = true;
 	}
@@ -1018,6 +1030,21 @@ repairDependencyLoop(DumpableObject **loop,
 					{
 						DumpableObject *nextobj;
 
+						nextobj = (j < nLoop - 1) ? loop[j + 1] : loop[0];
+						repairMatViewBoundaryMultiLoop(loop[j], nextobj);
+						return;
+					}
+				}
+			}
+			else if (loop[i]->objType == DO_REL_STATS &&
+					 ((RelStatsInfo *) loop[i])->relkind == RELKIND_MATVIEW)
+			{
+				for (j = 0; j < nLoop; j++)
+				{
+					if (loop[j]->objType == DO_POST_DATA_BOUNDARY)
+					{
+						DumpableObject *nextobj;
+
 						nextobj = (j < nLoop - 1) ? loop[j + 1] : loop[0];
 						repairMatViewBoundaryMultiLoop(loop[j], nextobj);
 						return;
@@ -1500,6 +1527,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "POST-DATA BOUNDARY  (ID %d)",
 					 obj->dumpId);
 			return;
+		case DO_REL_STATS:
+			snprintf(buf, bufsize,
+					 "RELATION STATISTICS FOR %s  (ID %d OID %u)",
+					 obj->name, obj->dumpId, obj->catId.oid);
+			return;
 	}
 	/* shouldn't get here */
 	snprintf(buf, bufsize,
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 396f79781c5..7effb704905 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -103,6 +103,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -172,6 +173,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -451,6 +453,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -666,6 +670,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index c602272d7db..02af89bae15 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -63,6 +63,9 @@ main(int argc, char **argv)
 	int			numWorkers = 1;
 	Archive    *AH;
 	char	   *inputFileSpec;
+	bool		data_only = false;
+	bool		schema_only = false;
+	bool		statistics_only = false;
 	static int	disable_triggers = 0;
 	static int	enable_row_security = 0;
 	static int	if_exists = 0;
@@ -71,12 +74,13 @@ main(int argc, char **argv)
 	static int	outputNoTablespaces = 0;
 	static int	use_setsessauth = 0;
 	static int	no_comments = 0;
+	static int	no_data = 0;
 	static int	no_publications = 0;
+	static int	no_schema = 0;
 	static int	no_security_labels = 0;
+	static int  no_statistics = 0;
 	static int	no_subscriptions = 0;
 	static int	strict_names = 0;
-	bool		data_only = false;
-	bool		schema_only = false;
 
 	struct option cmdopts[] = {
 		{"clean", 0, NULL, 'c'},
@@ -108,6 +112,7 @@ main(int argc, char **argv)
 		{"username", 1, NULL, 'U'},
 		{"verbose", 0, NULL, 'v'},
 		{"single-transaction", 0, NULL, '1'},
+		{"statistics-only", no_argument, NULL, 'X'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -124,9 +129,12 @@ main(int argc, char **argv)
 		{"transaction-size", required_argument, NULL, 5},
 		{"use-set-session-authorization", no_argument, &use_setsessauth, 1},
 		{"no-comments", no_argument, &no_comments, 1},
+		{"no-data", no_argument, &no_data, 1},
 		{"no-publications", no_argument, &no_publications, 1},
+		{"no-schema", no_argument, &no_schema, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"filter", required_argument, NULL, 4},
 
 		{NULL, 0, NULL, 0}
@@ -271,6 +279,10 @@ main(int argc, char **argv)
 				opts->aclsSkip = 1;
 				break;
 
+			case 'X':			/* Restore statistics only */
+				statistics_only = true;
+				break;
+
 			case '1':			/* Restore data in a single transaction */
 				opts->single_txn = true;
 				opts->exit_on_error = true;
@@ -343,6 +355,10 @@ main(int argc, char **argv)
 
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
+	if (schema_only && statistics_only)
+		pg_fatal("options -s/--schema-only and -X/--statistics-only cannot be used together");
 
 	if (data_only && opts->dropSchema)
 		pg_fatal("options -c/--clean and -a/--data-only cannot be used together");
@@ -362,8 +378,9 @@ main(int argc, char **argv)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
 	/* set derivative flags */
-	opts->dumpSchema = (!data_only);
-	opts->dumpData = (!schema_only);
+	opts->dumpData = data_only || (!no_data && !schema_only && !statistics_only);
+	opts->dumpSchema = schema_only || (!no_schema && !data_only && !statistics_only);
+	opts->dumpStatistics = statistics_only || (!no_statistics && !data_only && !schema_only);
 
 	opts->disable_triggers = disable_triggers;
 	opts->enable_row_security = enable_row_security;
@@ -375,6 +392,8 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
+	opts->no_data = no_data;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
@@ -482,6 +501,7 @@ usage(const char *progname)
 	printf(_("  -t, --table=NAME             restore named relation (table, view, etc.)\n"));
 	printf(_("  -T, --trigger=NAME           restore named trigger\n"));
 	printf(_("  -x, --no-privileges          skip restoration of access privileges (grant/revoke)\n"));
+	printf(_("  -X, --statistics-only        restore only the statistics, not schema or data\n"));
 	printf(_("  -1, --single-transaction     restore as a single transaction\n"));
 	printf(_("  --disable-triggers           disable triggers during data-only restore\n"));
 	printf(_("  --enable-row-security        enable row security\n"));
@@ -489,10 +509,13 @@ usage(const char *progname)
 			 "                               in FILENAME\n"));
 	printf(_("  --if-exists                  use IF EXISTS when dropping objects\n"));
 	printf(_("  --no-comments                do not restore comment commands\n"));
+	printf(_("  --no-data                    do not restore data\n"));
 	printf(_("  --no-data-for-failed-tables  do not restore data of tables that could not be\n"
 			 "                               created\n"));
 	printf(_("  --no-publications            do not restore publications\n"));
+	printf(_("  --no-schema                  do not restore schema\n"));
 	printf(_("  --no-security-labels         do not restore security labels\n"));
+	printf(_("  --no-statistics              do not restore statistics\n"));
 	printf(_("  --no-subscriptions           do not restore subscriptions\n"));
 	printf(_("  --no-table-access-method     do not restore table access methods\n"));
 	printf(_("  --no-tablespaces             do not restore tablespace assignments\n"));
diff --git a/src/bin/pg_dump/t/001_basic.pl b/src/bin/pg_dump/t/001_basic.pl
index 214240f1ae5..f29da06ed28 100644
--- a/src/bin/pg_dump/t/001_basic.pl
+++ b/src/bin/pg_dump/t/001_basic.pl
@@ -50,12 +50,30 @@ command_fails_like(
 	'pg_dump: options -s/--schema-only and -a/--data-only cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '-s', '-X' ],
+	qr/\Qpg_dump: error: options -s\/--schema-only and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: error: options -s/--schema-only and -X/--statistics-only cannot be used together'
+);
+
+command_fails_like(
+	[ 'pg_dump', '-a', '-X' ],
+	qr/\Qpg_dump: error: options -a\/--data-only and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: error: options -a/--data-only and -X/--statistics-only cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-s', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: options -s\/--schema-only and --include-foreign-data cannot be used together\E/,
 	'pg_dump: options -s/--schema-only and --include-foreign-data cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '--statistics-only', '--no-statistics' ],
+	qr/\Qpg_dump: error: options -X\/--statistics-only and --no-statistics cannot be used together\E/,
+	'pg_dump: options -X\/--statistics-only and --no-statistics cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-j2', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: option --include-foreign-data is not supported with parallel backup\E/,
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index bc5d9222a20..e1545e797a7 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -65,7 +65,7 @@ my %pgdump_runs = (
 			'--format' => 'custom',
 			'--file' => "$tempdir/binary_upgrade.dump",
 			'--no-password',
-			'--schema-only',
+			'--no-data',
 			'--binary-upgrade',
 			'--dbname' => 'postgres',    # alternative way to specify database
 		],
@@ -710,6 +710,39 @@ my %pgdump_runs = (
 			'--no-large-objects',
 			'postgres',
 		],
+	},
+	no_statistics => {
+		dump_cmd => [
+			'pg_dump', '--no-sync',
+			"--file=$tempdir/no_statistics.sql",
+			'--no-statistics',
+			'postgres',
+		],
+	},
+	no_data_no_schema => {
+		dump_cmd => [
+			'pg_dump', '--no-sync',
+			"--file=$tempdir/no_data_no_schema.sql",
+			'--no-data',
+			'--no-schema',
+			'postgres',
+		],
+	},
+	statistics_only => {
+		dump_cmd => [
+			'pg_dump', '--no-sync',
+			"--file=$tempdir/statistics_only.sql",
+			'--statistics-only',
+			'postgres',
+		],
+	},
+	no_schema => {
+		dump_cmd => [
+			'pg_dump', '--no-sync',
+			"--file=$tempdir/no_schema.sql",
+			'--no-schema',
+			'postgres',
+		],
 	},);
 
 ###############################################################
@@ -776,6 +809,7 @@ my %full_runs = (
 	no_large_objects => 1,
 	no_owner => 1,
 	no_privs => 1,
+	no_statistics => 1,
 	no_table_access_method => 1,
 	pg_dumpall_dbprivs => 1,
 	pg_dumpall_exclude => 1,
@@ -977,6 +1011,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1390,6 +1425,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1411,6 +1447,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1432,6 +1469,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1598,6 +1636,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1751,6 +1790,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			only_dump_test_table => 1,
 			section_data => 1,
 		},
@@ -1778,6 +1818,7 @@ my %tests = (
 			data_only => 1,
 			exclude_test_table => 1,
 			exclude_test_table_data => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1798,7 +1839,10 @@ my %tests = (
 			\QCOPY dump_test.fk_reference_test_table (col1) FROM stdin;\E
 			\n(?:\d\n){5}\\\.\n
 			/xms,
-		like => { data_only => 1, },
+		like => {
+			data_only => 1,
+			no_schema => 1,
+		},
 	},
 
 	'COPY test_second_table' => {
@@ -1814,6 +1858,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1836,6 +1881,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1859,6 +1905,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1881,6 +1928,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1903,6 +1951,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -3299,6 +3348,7 @@ my %tests = (
 		like => {
 			%full_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 			only_dump_test_schema => 1,
 			test_schema_plus_large_objects => 1,
@@ -3469,6 +3519,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			only_dump_measurement => 1,
 			section_data => 1,
 			only_dump_test_schema => 1,
@@ -4353,6 +4404,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 			binary_upgrade => 1,
@@ -4653,6 +4705,61 @@ my %tests = (
 		},
 	},
 
+	#
+	# TABLE and MATVIEW stats will end up in SECTION_DATA.
+	# INDEX stats (expression columns only) will end up in SECTION_POST_DATA.
+	#
+	'statistics_import' => {
+		create_sql => '
+			CREATE TABLE dump_test.has_stats
+			AS SELECT g.g AS x, g.g / 2 AS y FROM generate_series(1,100) AS g(g);
+			CREATE MATERIALIZED VIEW dump_test.has_stats_mv AS SELECT * FROM dump_test.has_stats;
+			CREATE INDEX dup_test_post_data_ix ON dump_test.has_stats((x - 1));
+			ANALYZE dump_test.has_stats, dump_test.has_stats_mv;',
+		regexp => qr/pg_catalog.pg_restore_attribute_stats/,
+		like => {
+			%full_runs,
+			%dump_test_schema_runs,
+			no_data_no_schema => 1,
+			no_schema => 1,
+			section_data => 1,
+			section_post_data => 1,
+			statistics_only => 1,
+			},
+		unlike => {
+			exclude_dump_test_schema => 1,
+			no_statistics => 1,
+			only_dump_measurement => 1,
+			schema_only => 1,
+			},
+	},
+
+	#
+	# While attribute stats (aka pg_statistic stats) only appear for tables
+	# that have been analyzed, all tables will have relation stats because
+	# those come from pg_class.
+	#
+	'relstats_on_unanalyzed_tables' => {
+		regexp => qr/pg_catalog.pg_restore_relation_stats/,
+
+		like => {
+			%full_runs,
+			%dump_test_schema_runs,
+			no_data_no_schema => 1,
+			no_schema => 1,
+			only_dump_test_table => 1,
+			role => 1,
+			role_parallel => 1,
+			section_data => 1,
+			section_post_data => 1,
+			statistics_only => 1,
+			},
+		unlike => {
+			no_statistics => 1,
+			schema_only => 1,
+			},
+	},
+
 	# CREATE TABLE with partitioned table and various AMs.  One
 	# partition uses the same default as the parent, and a second
 	# uses its own AM.
diff --git a/src/bin/pg_upgrade/dump.c b/src/bin/pg_upgrade/dump.c
index 8ce0fa3020e..a29cd2cca98 100644
--- a/src/bin/pg_upgrade/dump.c
+++ b/src/bin/pg_upgrade/dump.c
@@ -21,10 +21,11 @@ generate_old_dump(void)
 
 	/* run new pg_dumpall binary for globals */
 	exec_prog(UTILITY_LOG_FILE, NULL, true, true,
-			  "\"%s/pg_dumpall\" %s --globals-only --quote-all-identifiers "
+			  "\"%s/pg_dumpall\" %s %s --globals-only --quote-all-identifiers "
 			  "--binary-upgrade %s --no-sync -f \"%s/%s\"",
 			  new_cluster.bindir, cluster_conn_opts(&old_cluster),
 			  log_opts.verbose ? "--verbose" : "",
+			  user_opts.do_statistics ? "" : "--no-statistics",
 			  log_opts.dumpdir,
 			  GLOBALS_DUMP_FILE);
 	check_ok();
@@ -52,10 +53,11 @@ generate_old_dump(void)
 		snprintf(log_file_name, sizeof(log_file_name), DB_DUMP_LOG_FILE_MASK, old_db->db_oid);
 
 		parallel_exec_prog(log_file_name, NULL,
-						   "\"%s/pg_dump\" %s --schema-only --quote-all-identifiers "
+						   "\"%s/pg_dump\" %s --no-data %s --quote-all-identifiers "
 						   "--binary-upgrade --format=custom %s --no-sync --file=\"%s/%s\" %s",
 						   new_cluster.bindir, cluster_conn_opts(&old_cluster),
 						   log_opts.verbose ? "--verbose" : "",
+						   user_opts.do_statistics ? "" : "--no-statistics",
 						   log_opts.dumpdir,
 						   sql_file_name, escaped_connstr.data);
 
diff --git a/src/bin/pg_upgrade/option.c b/src/bin/pg_upgrade/option.c
index 108eb7a1ba4..3b6c7ec994e 100644
--- a/src/bin/pg_upgrade/option.c
+++ b/src/bin/pg_upgrade/option.c
@@ -60,6 +60,8 @@ parseCommandLine(int argc, char *argv[])
 		{"copy", no_argument, NULL, 2},
 		{"copy-file-range", no_argument, NULL, 3},
 		{"sync-method", required_argument, NULL, 4},
+		{"with-statistics", no_argument, NULL, 5},
+		{"no-statistics", no_argument, NULL, 6},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -70,6 +72,7 @@ parseCommandLine(int argc, char *argv[])
 
 	user_opts.do_sync = true;
 	user_opts.transfer_mode = TRANSFER_MODE_COPY;
+	user_opts.do_statistics = true;
 
 	os_info.progname = get_progname(argv[0]);
 
@@ -212,6 +215,13 @@ parseCommandLine(int argc, char *argv[])
 				user_opts.sync_method = pg_strdup(optarg);
 				break;
 
+			case 5:
+				user_opts.do_statistics = true;
+				break;
+			case 6:
+				user_opts.do_statistics = false;
+				break;
+
 			default:
 				fprintf(stderr, _("Try \"%s --help\" for more information.\n"),
 						os_info.progname);
@@ -306,7 +316,9 @@ usage(void)
 	printf(_("  --clone                       clone instead of copying files to new cluster\n"));
 	printf(_("  --copy                        copy files to new cluster (default)\n"));
 	printf(_("  --copy-file-range             copy files to new cluster with copy_file_range\n"));
+	printf(_("  --no-statistics               do not import statistics from old cluster\n"));
 	printf(_("  --sync-method=METHOD          set method for syncing files to disk\n"));
+	printf(_("  --with-statistics             import statistics from old cluster (default)\n"));
 	printf(_("  -?, --help                    show this help, then exit\n"));
 	printf(_("\n"
 			 "Before running pg_upgrade you must:\n"
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index 0cdd675e4f1..3fe111fbde5 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -327,6 +327,7 @@ typedef struct
 	int			jobs;			/* number of processes/threads to use */
 	char	   *socketdir;		/* directory to use for Unix sockets */
 	char	   *sync_method;
+	bool		do_statistics;	/* carry over statistics from old cluster */
 } UserOpts;
 
 typedef struct
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index 24fcc76d72c..0d5e7d5a2d7 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -123,7 +123,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Dump only the data, not the schema (data definitions).
+        Dump only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are dumped.
        </para>
 
@@ -141,13 +141,11 @@ PostgreSQL documentation
       <listitem>
        <para>
         Include large objects in the dump.  This is the default behavior
-        except when <option>--schema</option>, <option>--table</option>, or
-        <option>--schema-only</option> is specified.  The <option>-b</option>
-        switch is therefore only useful to add large objects to dumps
-        where a specific schema or table has been requested.  Note that
-        large objects are considered data and therefore will be included when
-        <option>--data-only</option> is used, but not
-        when <option>--schema-only</option> is.
+        except when <option>--schema</option>, <option>--table</option>,
+        <option>--schema-only</option>, or <option>--statistics-only</option>, or
+        <option>--no-data</option> is specified.  The <option>-b</option>
+        switch is therefore only useful to add large objects to dumps where a
+        specific schema or table has been requested.
        </para>
       </listitem>
      </varlistentry>
@@ -516,10 +514,11 @@ PostgreSQL documentation
       <term><option>--schema-only</option></term>
       <listitem>
        <para>
-        Dump only the object definitions (schema), not data.
+        Dump only the object definitions (schema), not data or statistics.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive to <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
         <option>--section=pre-data --section=post-data</option>.
@@ -652,6 +651,18 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Dump only the statistics, not the schema (data definitions) or data.
+        Statistics for tables, materialized views, and indexes are dumped.
+       </para>
+
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-Z <replaceable class="parameter">level</replaceable></option></term>
       <term><option>-Z <replaceable class="parameter">method</replaceable></option>[:<replaceable>detail</replaceable>]</term>
@@ -741,7 +752,8 @@ PostgreSQL documentation
       <term><option>--disable-triggers</option></term>
       <listitem>
        <para>
-        This option is relevant only when creating a data-only dump.
+        This option is relevant only when creating a dump that includes data
+        but does not include schema.
         It instructs <application>pg_dump</application> to include commands
         to temporarily disable triggers on the target tables while
         the data is restored.  Use this if you have referential
@@ -824,16 +836,17 @@ PostgreSQL documentation
       <term><option>--exclude-table-data=<replaceable class="parameter">pattern</replaceable></option></term>
       <listitem>
        <para>
-        Do not dump data for any tables matching <replaceable
+        Do not dump data or statistics for any tables matching <replaceable
         class="parameter">pattern</replaceable>. The pattern is
         interpreted according to the same rules as for <option>-t</option>.
         <option>--exclude-table-data</option> can be given more than once to
-        exclude tables matching any of several patterns. This option is
-        useful when you need the definition of a particular table even
-        though you do not need the data in it.
+        exclude tables matching any of several patterns. This option is useful
+        when you need the definition of a particular table even though you do
+        not need the data in it.
        </para>
        <para>
-        To exclude data for all tables in the database, see <option>--schema-only</option>.
+        To exclude data for all tables in the database, see <option>--schema-only</option>
+        or <option>--statistics-only</option>.
        </para>
       </listitem>
      </varlistentry>
@@ -1080,6 +1093,15 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-data</option></term>
+      <listitem>
+       <para>
+        Do not dump data.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-publications</option></term>
       <listitem>
@@ -1098,6 +1120,24 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-schema</option></term>
+      <listitem>
+       <para>
+        Do not dump schema (data definitions).
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
@@ -1236,9 +1276,11 @@ PostgreSQL documentation
          </para>
          <para>
           The data section contains actual table data, large-object
-          contents, and sequence values.
+          contents, statitistics for tables and materialized views and
+          sequence values.
           Post-data items include definitions of indexes, triggers, rules,
-          and constraints other than validated check constraints.
+          statistics for indexes, and constraints other than validated check
+          constraints.
           Pre-data items include all other data definition items.
          </para>
        </listitem>
@@ -1581,7 +1623,7 @@ CREATE DATABASE foo WITH TEMPLATE template0;
   </para>
 
   <para>
-   When a data-only dump is chosen and the option <option>--disable-triggers</option>
+   When a dump without schema is chosen and the option <option>--disable-triggers</option>
    is used, <application>pg_dump</application> emits commands
    to disable triggers on user tables before inserting the data,
    and then commands to re-enable them after the data has been
diff --git a/doc/src/sgml/ref/pg_dumpall.sgml b/doc/src/sgml/ref/pg_dumpall.sgml
index 39d93c2c0e3..15fb40e7be9 100644
--- a/doc/src/sgml/ref/pg_dumpall.sgml
+++ b/doc/src/sgml/ref/pg_dumpall.sgml
@@ -81,7 +81,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Dump only the data, not the schema (data definitions).
+        Dump only the data, not the schema (data definitions) or statistics.
        </para>
       </listitem>
      </varlistentry>
@@ -265,6 +265,17 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Dump only the statistics, not the schema (data definitions) or data.
+        Statistics for tables, materialized views, and indexes are dumped.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--binary-upgrade</option></term>
       <listitem>
@@ -307,7 +318,7 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       <term><option>--disable-triggers</option></term>
       <listitem>
        <para>
-        This option is relevant only when creating a data-only dump.
+        This option is relevant only when creating a dump with data and without schema.
         It instructs <application>pg_dumpall</application> to include commands
         to temporarily disable triggers on the target tables while
         the data is restored.  Use this if you have referential
@@ -422,6 +433,15 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-data</option></term>
+      <listitem>
+       <para>
+        Do not dump data.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-publications</option></term>
       <listitem>
@@ -447,6 +467,15 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-schema</option></term>
+      <listitem>
+       <para>
+        Do not dump schema (data definitions).
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-security-labels</option></term>
       <listitem>
@@ -456,6 +485,15 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index b8b27e1719e..86e4264714d 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -94,7 +94,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Restore only the data, not the schema (data definitions).
+        Restore only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are restored,
         if present in the archive.
        </para>
@@ -483,10 +483,11 @@ PostgreSQL documentation
         to the extent that schema entries are present in the archive.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive of <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
-        <option>--section=pre-data --section=post-data</option>.
+        <option>--section=pre-data --section=post-data --no-statistics</option>.
        </para>
        <para>
         (Do not confuse this with the <option>--schema</option> option, which
@@ -599,6 +600,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Restore only the statistics, not schema (data definitions) or data.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-1</option></term>
       <term><option>--single-transaction</option></term>
@@ -617,7 +628,7 @@ PostgreSQL documentation
       <term><option>--disable-triggers</option></term>
       <listitem>
        <para>
-        This option is relevant only when performing a data-only restore.
+        This option is relevant only when performing a restore without schema.
         It instructs <application>pg_restore</application> to execute commands
         to temporarily disable triggers on the target tables while
         the data is restored.  Use this if you have referential
@@ -681,6 +692,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-data</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore data, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-data-for-failed-tables</option></term>
       <listitem>
@@ -713,6 +734,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-schema</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore schema (data definitions), even if
+        the archive contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-security-labels</option></term>
       <listitem>
@@ -723,6 +754,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore statistics, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pgupgrade.sgml b/doc/src/sgml/ref/pgupgrade.sgml
index 4777381dac2..64a1ebd613b 100644
--- a/doc/src/sgml/ref/pgupgrade.sgml
+++ b/doc/src/sgml/ref/pgupgrade.sgml
@@ -145,6 +145,24 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--with-statistics</option></term>
+      <listitem>
+       <para>
+        Restore statistics from the old cluster into the new cluster.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not restore statistics from the old cluster into the new cluster.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-o</option> <replaceable class="parameter">options</replaceable></term>
       <term><option>--old-options</option> <replaceable class="parameter">options</replaceable></term>
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9a3bee93dec..f3351342a24 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2400,6 +2400,7 @@ RelMapFile
 RelMapping
 RelOptInfo
 RelOptKind
+RelStatsInfo
 RelToCheck
 RelToCluster
 RelabelType
-- 
2.48.1

#305Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#304)
Re: Statistics Import and Export

On Sun, 2025-02-09 at 22:00 -0500, Corey Huinker wrote:

0001 - I've added pg_locks tests for a regular index and a
partitioned index.

Committed 0001, the fix for importing stats.

Regards,
Jeff Davis

#306Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#304)
Re: Statistics Import and Export

On Sun, 2025-02-09 at 22:00 -0500, Corey Huinker wrote:

0002 - I've done some documentation rewording, mostly wording changes
where behaviors surrounding data-only dumps are actually meant for
any dump that has all schema excluded.

Comments on v45-0002:

* Why is generate_old_dump() passing optionally passing --no-statistics
to pg_dumpall along with --globals-only? If --globals-only is
specified, no stats are dumped anyway, right?

* The tag is still wrong: it is "STATISTICS DATA mytable" when it
should just be "mytable".

* What's the logic behind the pg_dumpall options? The docs say
it should support the new pg_dump options, but they don't seem to work.

* The enum entryType casing is unconventional. How about a type name of
TocEntryType and values like STATS_TOC_ENTRY.

* The pg_dump test suite time has increased by ~50%. If some tests are
superfluous, please remove them.

Regards,
Jeff Davis

#307Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#306)
1 attachment(s)
Re: Statistics Import and Export

Comments on v45-0002:

Assuming you meant 47

* Why is generate_old_dump() passing optionally passing --no-statistics
to pg_dumpall along with --globals-only? If --globals-only is
specified, no stats are dumped anyway, right?

Removed.

* The tag is still wrong: it is "STATISTICS DATA mytable" when it
should just be "mytable".

Fixed.

* What's the logic behind the pg_dumpall options? The docs say
it should support the new pg_dump options, but they don't seem to work.

Fixed.

* The enum entryType casing is unconventional. How about a type name of
TocEntryType and values like STATS_TOC_ENTRY.

Conventionified.

* The pg_dump test suite time has increased by ~50%. If some tests are
superfluous, please remove them.

The nature of the TAP tests is that every new check N must be run against
every existing dump run M adding one N check gets you M scans, and adding a
dump type requires that it be scanned N times, and we increased both N and
M. If there were a way to only do stats-related tests against a subset of
the dumps, then yes, we could trim it down, but as it is I think it's a
limitation of the testing structure. But aside from creating a whole extra
XYZ_pg_dump.pl file, I don't think there's a way to do that.

The previous 0001 is now committed (thanks!) so only one remains.

Attachments:

v48-0001-Enable-dumping-of-table-index-stats-in-pg_dump.patchtext/x-patch; charset=US-ASCII; name=v48-0001-Enable-dumping-of-table-index-stats-in-pg_dump.patchDownload
From afb3d0fd81fb5395ff71ac7b05131babf5d61877 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH v48] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will generate a statement that
calls pg_set_relation_stats(), and it will generate a series of
statements that call pg_set_attribute_stats(), one per attribute that
has a corresponding row in pg_statistic. These statements will restore
the statistics of the current system onto the destination system.

Adds the command-line options -X / --statistics-only, which are mutually
exclusive to --schema-only and --data-only.

Statistics are not dumped when --schema-only is specified, except during
a binary upgrade.

As is the pattern with pg_dump options, statistics can be disabled using
--no-statistics.

Table statistics are dumped in the data section. This is true even if
dumping stats in a binary upgrade. Index statistics are dumped in the
post-data section.

Add --no-data option.  This option is useful for situations where
someone wishes to test query plans from a production database without
copying production data.

This also makes the corresponding change to the simulated pg_upgrade in
the TAP tests for pg_dump.

Add --no-schema option to pg_dump, etc.  Previously, users could use
--data-only when they wanted to suppress schema from a dump. However,
that no longer makes sense now that the data/schema binary has become
the data/schema/statistics trinary.
---
 src/bin/pg_dump/pg_backup.h           |  10 +-
 src/bin/pg_dump/pg_backup_archiver.c  |  97 +++++--
 src/bin/pg_dump/pg_backup_archiver.h  |   3 +-
 src/bin/pg_dump/pg_backup_directory.c |   2 +-
 src/bin/pg_dump/pg_dump.c             | 390 +++++++++++++++++++++++++-
 src/bin/pg_dump/pg_dump.h             |  11 +
 src/bin/pg_dump/pg_dump_sort.c        |  36 ++-
 src/bin/pg_dump/pg_dumpall.c          |  18 ++
 src/bin/pg_dump/pg_restore.c          |  31 +-
 src/bin/pg_dump/t/001_basic.pl        |  18 ++
 src/bin/pg_dump/t/002_pg_dump.pl      | 111 +++++++-
 src/bin/pg_upgrade/dump.c             |   7 +-
 src/bin/pg_upgrade/option.c           |  12 +
 src/bin/pg_upgrade/pg_upgrade.h       |   1 +
 doc/src/sgml/ref/pg_dump.sgml         |  80 ++++--
 doc/src/sgml/ref/pg_dumpall.sgml      |  42 ++-
 doc/src/sgml/ref/pg_restore.sgml      |  49 +++-
 doc/src/sgml/ref/pgupgrade.sgml       |  18 ++
 src/tools/pgindent/typedefs.list      |   1 +
 19 files changed, 864 insertions(+), 73 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index f0f19bb0b29..3fa1474fad7 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -110,9 +110,12 @@ typedef struct _restoreOptions
 	int			column_inserts;
 	int			if_exists;
 	int			no_comments;	/* Skip comments */
+	int			no_data;			/* Skip data */
 	int			no_publications;	/* Skip publication entries */
+	int			no_schema;			/* Skip schema generation */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
@@ -160,6 +163,7 @@ typedef struct _restoreOptions
 	/* flags derived from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } RestoreOptions;
 
 typedef struct _dumpOptions
@@ -179,8 +183,11 @@ typedef struct _dumpOptions
 	int			column_inserts;
 	int			if_exists;
 	int			no_comments;
-	int			no_security_labels;
+	int			no_data;
 	int			no_publications;
+	int			no_schema;
+	int			no_security_labels;
+	int			no_statistics;
 	int			no_subscriptions;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
@@ -208,6 +215,7 @@ typedef struct _dumpOptions
 	/* flags derived from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } DumpOptions;
 
 /*
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index b9d7ab98c3e..6c897311437 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -46,6 +46,12 @@
 #define TEXT_DUMP_HEADER "--\n-- PostgreSQL database dump\n--\n\n"
 #define TEXT_DUMPALL_HEADER "--\n-- PostgreSQL database cluster dump\n--\n\n"
 
+typedef enum TocEntryType
+{
+	DEFAULT_TOC_ENTRY,
+	DATA_TOC_ENTRY,
+	STATS_TOC_ENTRY
+}			TocEntryType;
 
 static ArchiveHandle *_allocAH(const char *FileSpec, const ArchiveFormat fmt,
 							   const pg_compress_specification compression_spec,
@@ -53,7 +59,7 @@ static ArchiveHandle *_allocAH(const char *FileSpec, const ArchiveFormat fmt,
 							   SetupWorkerPtrType setupWorkerPtr,
 							   DataDirSyncMethod sync_method);
 static void _getObjectDescription(PQExpBuffer buf, const TocEntry *te);
-static void _printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData);
+static void _printTocEntry(ArchiveHandle *AH, TocEntry *te, TocEntryType entry_type);
 static char *sanitize_line(const char *str, bool want_hyphen);
 static void _doSetFixedOutputState(ArchiveHandle *AH);
 static void _doSetSessionAuth(ArchiveHandle *AH, const char *user);
@@ -149,6 +155,7 @@ InitDumpOptions(DumpOptions *opts)
 	opts->dumpSections = DUMP_UNSECTIONED;
 	opts->dumpSchema = true;
 	opts->dumpData = true;
+	opts->dumpStatistics = true;
 }
 
 /*
@@ -169,9 +176,10 @@ dumpOptionsFromRestoreOptions(RestoreOptions *ropt)
 	dopt->outputClean = ropt->dropSchema;
 	dopt->dumpData = ropt->dumpData;
 	dopt->dumpSchema = ropt->dumpSchema;
+	dopt->dumpSections = ropt->dumpSections;
+	dopt->dumpStatistics = ropt->dumpStatistics;
 	dopt->if_exists = ropt->if_exists;
 	dopt->column_inserts = ropt->column_inserts;
-	dopt->dumpSections = ropt->dumpSections;
 	dopt->aclsSkip = ropt->aclsSkip;
 	dopt->outputSuperuser = ropt->superuser;
 	dopt->outputCreateDB = ropt->createDB;
@@ -186,6 +194,9 @@ dumpOptionsFromRestoreOptions(RestoreOptions *ropt)
 	dopt->no_publications = ropt->no_publications;
 	dopt->no_security_labels = ropt->no_security_labels;
 	dopt->no_subscriptions = ropt->no_subscriptions;
+	dopt->no_data = ropt->no_data;
+	dopt->no_schema = ropt->no_schema;
+	dopt->no_statistics = ropt->no_statistics;
 	dopt->lockWaitTimeout = ropt->lockWaitTimeout;
 	dopt->include_everything = ropt->include_everything;
 	dopt->enable_row_security = ropt->enable_row_security;
@@ -418,30 +429,30 @@ RestoreArchive(Archive *AHX)
 	}
 
 	/*
-	 * Work out if we have an implied data-only restore. This can happen if
-	 * the dump was data only or if the user has used a toc list to exclude
-	 * all of the schema data. All we do is look for schema entries - if none
-	 * are found then we unset the dumpSchema flag.
+	 * Work out if we have an schema-less restore. This can happen if the dump
+	 * was data-only or statistics-only or no-schema or if the user has used a
+	 * toc list to exclude all of the schema data. All we do is look for
+	 * schema entries - if none are found then we unset the dumpSchema flag.
 	 *
 	 * We could scan for wanted TABLE entries, but that is not the same as
 	 * data-only. At this stage, it seems unnecessary (6-Mar-2001).
 	 */
 	if (ropt->dumpSchema)
 	{
-		int			impliedDataOnly = 1;
+		bool		no_schema_found = true;
 
 		for (te = AH->toc->next; te != AH->toc; te = te->next)
 		{
 			if ((te->reqs & REQ_SCHEMA) != 0)
-			{					/* It's schema, and it's wanted */
-				impliedDataOnly = 0;
+			{
+				no_schema_found = false;
 				break;
 			}
 		}
-		if (impliedDataOnly)
+		if (no_schema_found)
 		{
 			ropt->dumpSchema = false;
-			pg_log_info("implied data-only restore");
+			pg_log_info("implied no-schema restore");
 		}
 	}
 
@@ -739,7 +750,7 @@ RestoreArchive(Archive *AHX)
 
 		for (te = AH->toc->next; te != AH->toc; te = te->next)
 		{
-			if ((te->reqs & (REQ_SCHEMA | REQ_DATA)) == 0)
+			if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) == 0)
 				continue;		/* ignore if not to be dumped at all */
 
 			switch (_tocEntryRestorePass(te))
@@ -760,7 +771,7 @@ RestoreArchive(Archive *AHX)
 		{
 			for (te = AH->toc->next; te != AH->toc; te = te->next)
 			{
-				if ((te->reqs & (REQ_SCHEMA | REQ_DATA)) != 0 &&
+				if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) != 0 &&
 					_tocEntryRestorePass(te) == RESTORE_PASS_ACL)
 					(void) restore_toc_entry(AH, te, false);
 			}
@@ -770,7 +781,7 @@ RestoreArchive(Archive *AHX)
 		{
 			for (te = AH->toc->next; te != AH->toc; te = te->next)
 			{
-				if ((te->reqs & (REQ_SCHEMA | REQ_DATA)) != 0 &&
+				if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) != 0 &&
 					_tocEntryRestorePass(te) == RESTORE_PASS_POST_ACL)
 					(void) restore_toc_entry(AH, te, false);
 			}
@@ -869,7 +880,7 @@ restore_toc_entry(ArchiveHandle *AH, TocEntry *te, bool is_parallel)
 			pg_log_info("creating %s \"%s\"",
 						te->desc, te->tag);
 
-		_printTocEntry(AH, te, false);
+		_printTocEntry(AH, te, DEFAULT_TOC_ENTRY);
 		defnDumped = true;
 
 		if (strcmp(te->desc, "TABLE") == 0)
@@ -938,7 +949,7 @@ restore_toc_entry(ArchiveHandle *AH, TocEntry *te, bool is_parallel)
 			 */
 			if (AH->PrintTocDataPtr != NULL)
 			{
-				_printTocEntry(AH, te, true);
+				_printTocEntry(AH, te, DATA_TOC_ENTRY);
 
 				if (strcmp(te->desc, "BLOBS") == 0 ||
 					strcmp(te->desc, "BLOB COMMENTS") == 0)
@@ -1036,15 +1047,21 @@ restore_toc_entry(ArchiveHandle *AH, TocEntry *te, bool is_parallel)
 		{
 			/* If we haven't already dumped the defn part, do so now */
 			pg_log_info("executing %s %s", te->desc, te->tag);
-			_printTocEntry(AH, te, false);
+			_printTocEntry(AH, te, DEFAULT_TOC_ENTRY);
 		}
 	}
 
+	/*
+	 * If it has a statistics component that we want, then process that
+	 */
+	if ((reqs & REQ_STATS) != 0)
+		_printTocEntry(AH, te, STATS_TOC_ENTRY);
+
 	/*
 	 * If we emitted anything for this TOC entry, that counts as one action
 	 * against the transaction-size limit.  Commit if it's time to.
 	 */
-	if ((reqs & (REQ_SCHEMA | REQ_DATA)) != 0 && ropt->txn_size > 0)
+	if ((reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) != 0 && ropt->txn_size > 0)
 	{
 		if (++AH->txnCount >= ropt->txn_size)
 		{
@@ -1084,6 +1101,7 @@ NewRestoreOptions(void)
 	opts->compression_spec.level = 0;
 	opts->dumpSchema = true;
 	opts->dumpData = true;
+	opts->dumpStatistics = true;
 
 	return opts;
 }
@@ -1329,7 +1347,7 @@ PrintTOCSummary(Archive *AHX)
 		te->reqs = _tocEntryRequired(te, curSection, AH);
 		/* Now, should we print it? */
 		if (ropt->verbose ||
-			(te->reqs & (REQ_SCHEMA | REQ_DATA)) != 0)
+			(te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) != 0)
 		{
 			char	   *sanitized_name;
 			char	   *sanitized_schema;
@@ -2582,7 +2600,7 @@ WriteToc(ArchiveHandle *AH)
 	tocCount = 0;
 	for (te = AH->toc->next; te != AH->toc; te = te->next)
 	{
-		if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_SPECIAL)) != 0)
+		if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS | REQ_SPECIAL)) != 0)
 			tocCount++;
 	}
 
@@ -2592,7 +2610,7 @@ WriteToc(ArchiveHandle *AH)
 
 	for (te = AH->toc->next; te != AH->toc; te = te->next)
 	{
-		if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_SPECIAL)) == 0)
+		if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS | REQ_SPECIAL)) == 0)
 			continue;
 
 		WriteInt(AH, te->dumpId);
@@ -2919,6 +2937,14 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 		strcmp(te->desc, "SEARCHPATH") == 0)
 		return REQ_SPECIAL;
 
+	if (strcmp(te->desc, "STATISTICS DATA") == 0)
+	{
+		if (!ropt->dumpStatistics)
+			return 0;
+		else
+			res = REQ_STATS;
+	}
+
 	/*
 	 * DATABASE and DATABASE PROPERTIES also have a special rule: they are
 	 * restored in createDB mode, and not restored otherwise, independently of
@@ -2963,6 +2989,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's statistics and we don't want statistics, maybe ignore it */
+	if (!ropt->dumpStatistics && strcmp(te->desc, "STATISTICS DATA") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2992,6 +3022,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS DATA") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
@@ -3108,6 +3139,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 		}
 	}
 
+
 	/*
 	 * Determine whether the TOC entry contains schema and/or data components,
 	 * and mask off inapplicable REQ bits.  If it had a dataDumper, assume
@@ -3173,12 +3205,12 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 				strncmp(te->tag, "LARGE OBJECT", 12) == 0) ||
 			   (strcmp(te->desc, "SECURITY LABEL") == 0 &&
 				strncmp(te->tag, "LARGE OBJECT", 12) == 0))))
-			res = res & REQ_SCHEMA;
+			res = res & (REQ_SCHEMA | REQ_STATS);
 	}
 
 	/* Mask it if we don't want schema */
 	if (!ropt->dumpSchema)
-		res = res & REQ_DATA;
+		res = res & (REQ_DATA | REQ_STATS);
 
 	return res;
 }
@@ -3730,7 +3762,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te)
  * will remain at default, until the matching ACL TOC entry is restored.
  */
 static void
-_printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData)
+_printTocEntry(ArchiveHandle *AH, TocEntry *te, TocEntryType entry_type)
 {
 	RestoreOptions *ropt = AH->public.ropt;
 
@@ -3754,10 +3786,17 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData)
 		char	   *sanitized_schema;
 		char	   *sanitized_owner;
 
-		if (isData)
-			pfx = "Data for ";
-		else
-			pfx = "";
+		switch (entry_type)
+		{
+			case DATA_TOC_ENTRY:
+				pfx = "Data for ";
+				break;
+			case STATS_TOC_ENTRY:
+				pfx = "Statistics for ";
+				break;
+			default:
+				pfx = "";
+		}
 
 		ahprintf(AH, "--\n");
 		if (AH->public.verbose)
@@ -4325,7 +4364,7 @@ restore_toc_entries_parallel(ArchiveHandle *AH, ParallelState *pstate,
 		if (next_work_item != NULL)
 		{
 			/* If not to be restored, don't waste time launching a worker */
-			if ((next_work_item->reqs & (REQ_SCHEMA | REQ_DATA)) == 0)
+			if ((next_work_item->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) == 0)
 			{
 				pg_log_info("skipping item %d %s %s",
 							next_work_item->dumpId,
diff --git a/src/bin/pg_dump/pg_backup_archiver.h b/src/bin/pg_dump/pg_backup_archiver.h
index ce5ed1dd395..a2064f471ed 100644
--- a/src/bin/pg_dump/pg_backup_archiver.h
+++ b/src/bin/pg_dump/pg_backup_archiver.h
@@ -209,7 +209,8 @@ typedef enum
 
 #define REQ_SCHEMA	0x01		/* want schema */
 #define REQ_DATA	0x02		/* want data */
-#define REQ_SPECIAL	0x04		/* for special TOC entries */
+#define REQ_STATS	0x04
+#define REQ_SPECIAL	0x08		/* for special TOC entries */
 
 struct _archiveHandle
 {
diff --git a/src/bin/pg_dump/pg_backup_directory.c b/src/bin/pg_dump/pg_backup_directory.c
index 240a1d41062..b2a841bb0ff 100644
--- a/src/bin/pg_dump/pg_backup_directory.c
+++ b/src/bin/pg_dump/pg_backup_directory.c
@@ -780,7 +780,7 @@ _PrepParallelRestore(ArchiveHandle *AH)
 			continue;
 
 		/* We may ignore items not due to be restored */
-		if ((te->reqs & REQ_DATA) == 0)
+		if ((te->reqs & (REQ_DATA | REQ_STATS)) == 0)
 			continue;
 
 		/*
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index ca15b40939c..11048fc98ba 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -431,6 +431,7 @@ main(int argc, char **argv)
 	DataDirSyncMethod sync_method = DATA_DIR_SYNC_METHOD_FSYNC;
 	bool		data_only = false;
 	bool		schema_only = false;
+	bool		statistics_only = false;
 
 	static DumpOptions dopt;
 
@@ -467,6 +468,7 @@ main(int argc, char **argv)
 		{"encoding", required_argument, NULL, 'E'},
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
+		{"statistics-only", no_argument, NULL, 'X'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -493,8 +495,11 @@ main(int argc, char **argv)
 		{"strict-names", no_argument, &strict_names, 1},
 		{"use-set-session-authorization", no_argument, &dopt.use_setsessauth, 1},
 		{"no-comments", no_argument, &dopt.no_comments, 1},
+		{"no-data", no_argument, &dopt.no_data, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
+		{"no-schema", no_argument, &dopt.no_schema, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -540,7 +545,7 @@ main(int argc, char **argv)
 
 	InitDumpOptions(&dopt);
 
-	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:RsS:t:T:U:vwWxZ:",
+	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:RsS:t:T:U:vwWxXZ:",
 							long_options, &optindex)) != -1)
 	{
 		switch (c)
@@ -614,6 +619,10 @@ main(int argc, char **argv)
 				dopt.cparams.pgport = pg_strdup(optarg);
 				break;
 
+			case 'X':			/* Dump statistics only */
+				statistics_only = true;
+				break;
+
 			case 'R':
 				/* no-op, still accepted for backwards compatibility */
 				break;
@@ -785,6 +794,17 @@ main(int argc, char **argv)
 
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (schema_only && statistics_only)
+		pg_fatal("options -s/--schema-only and -X/--statistics-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
+
+	if (data_only && dopt.no_data)
+		pg_fatal("options -a/--data-only and --no-data cannot be used together");
+	if (schema_only && dopt.no_schema)
+		pg_fatal("options -s/--schema-only and --no-schema cannot be used together");
+	if (statistics_only && dopt.no_statistics)
+		pg_fatal("options -X/--statistics-only and --no-statistics cannot be used together");
 
 	if (schema_only && foreign_servers_include_patterns.head != NULL)
 		pg_fatal("options -s/--schema-only and --include-foreign-data cannot be used together");
@@ -799,8 +819,9 @@ main(int argc, char **argv)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
 	/* set derivative flags */
-	dopt.dumpSchema = (!data_only);
-	dopt.dumpData = (!schema_only);
+	dopt.dumpData = data_only || (!schema_only && !statistics_only && !dopt.no_data);
+	dopt.dumpSchema = schema_only || (!data_only && !statistics_only && !dopt.no_schema);
+	dopt.dumpStatistics = statistics_only || (!data_only && !schema_only && !dopt.no_statistics);
 
 	/*
 	 * --inserts are already implied above if --column-inserts or
@@ -1100,6 +1121,7 @@ main(int argc, char **argv)
 	ropt->dropSchema = dopt.outputClean;
 	ropt->dumpData = dopt.dumpData;
 	ropt->dumpSchema = dopt.dumpSchema;
+	ropt->dumpStatistics = dopt.dumpStatistics;
 	ropt->if_exists = dopt.if_exists;
 	ropt->column_inserts = dopt.column_inserts;
 	ropt->dumpSections = dopt.dumpSections;
@@ -1178,7 +1200,7 @@ help(const char *progname)
 	printf(_("  -?, --help                   show this help, then exit\n"));
 
 	printf(_("\nOptions controlling the output content:\n"));
-	printf(_("  -a, --data-only              dump only the data, not the schema\n"));
+	printf(_("  -a, --data-only              dump only the data, not the schema or statistics\n"));
 	printf(_("  -b, --large-objects          include large objects in dump\n"));
 	printf(_("  --blobs                      (same as --large-objects, deprecated)\n"));
 	printf(_("  -B, --no-large-objects       exclude large objects in dump\n"));
@@ -1191,11 +1213,12 @@ help(const char *progname)
 	printf(_("  -N, --exclude-schema=PATTERN do NOT dump the specified schema(s)\n"));
 	printf(_("  -O, --no-owner               skip restoration of object ownership in\n"
 			 "                               plain-text format\n"));
-	printf(_("  -s, --schema-only            dump only the schema, no data\n"));
+	printf(_("  -s, --schema-only            dump only the schema, no data or statistics\n"));
 	printf(_("  -S, --superuser=NAME         superuser user name to use in plain-text format\n"));
 	printf(_("  -t, --table=PATTERN          dump only the specified table(s)\n"));
 	printf(_("  -T, --exclude-table=PATTERN  do NOT dump the specified table(s)\n"));
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
+	printf(_("  -X, --statistics-only        dump only the statistics, not schema or data\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
@@ -1220,8 +1243,11 @@ help(const char *progname)
 	printf(_("  --inserts                    dump data as INSERT commands, rather than COPY\n"));
 	printf(_("  --load-via-partition-root    load partitions via the root table\n"));
 	printf(_("  --no-comments                do not dump comment commands\n"));
+	printf(_("  --no-data                    do not dump data\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
+	printf(_("  --no-schema                  do not dump schema\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -6779,6 +6805,43 @@ getFuncs(Archive *fout)
 	destroyPQExpBuffer(query);
 }
 
+/*
+ * getRelationStatistics
+ *    register the statistics object as a dependent of the relation.
+ *
+ */
+static RelStatsInfo *
+getRelationStatistics(Archive *fout, DumpableObject *rel, char relkind)
+{
+	if ((relkind == RELKIND_RELATION) ||
+		(relkind == RELKIND_PARTITIONED_TABLE) ||
+		(relkind == RELKIND_INDEX) ||
+		(relkind == RELKIND_PARTITIONED_INDEX) ||
+		(relkind == RELKIND_MATVIEW))
+	{
+		RelStatsInfo	   *info = pg_malloc0(sizeof(RelStatsInfo));
+		DumpableObject	   *dobj = &info->dobj;
+
+		dobj->objType = DO_REL_STATS;
+		dobj->catId.tableoid = 0;
+		dobj->catId.oid = 0;
+		AssignDumpId(dobj);
+		dobj->dependencies = (DumpId *) pg_malloc(sizeof(DumpId));
+		dobj->dependencies[0] = rel->dumpId;
+		dobj->nDeps = 1;
+		dobj->allocDeps = 1;
+		dobj->components |= DUMP_COMPONENT_STATISTICS;
+		dobj->dump = rel->dump;
+		dobj->name = pg_strdup(rel->name);
+		dobj->namespace = rel->namespace;
+		info->relkind = relkind;
+		info->postponed_def = false;
+
+		return info;
+	}
+	return NULL;
+}
+
 /*
  * getTables
  *	  read all the tables (no indexes) in the system catalogs,
@@ -7204,6 +7267,8 @@ getTables(Archive *fout, int *numTables)
 				}
 			}
 		}
+		if (tblinfo[i].interesting || dopt->dumpStatistics)
+			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relkind);
 	}
 
 	if (query->len != 0)
@@ -7650,11 +7715,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 		for (int c = 0; c < numinds; c++, j++)
 		{
 			char		contype;
+			char		indexkind;
+			RelStatsInfo   *relstats;
 
 			indxinfo[j].dobj.objType = DO_INDEX;
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -7677,7 +7745,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			{
 				NULL, NULL
 			};
+
+			if (indxinfo[j].parentidx == 0)
+				indexkind = RELKIND_INDEX;
+			else
+				indexkind = RELKIND_PARTITIONED_INDEX;
+
 			contype = *(PQgetvalue(res, j, i_contype));
+			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, indexkind);
 
 			if (contype == 'p' || contype == 'u' || contype == 'x')
 			{
@@ -7711,6 +7786,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				constrinfo->separate = true;
 
 				indxinfo[j].indexconstraint = constrinfo->dobj.dumpId;
+				if (relstats != NULL)
+					addObjectDependency(&relstats->dobj, constrinfo->dobj.dumpId);
 			}
 			else
 			{
@@ -10298,6 +10375,296 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * Tabular description of the parameters to pg_restore_relation_stats()
+ * param_name, param_type
+ */
+static const char *rel_stats_arginfo[][2] = {
+	{"relation", "regclass"},
+	{"version", "integer"},
+	{"relpages", "integer"},
+	{"reltuples", "real"},
+	{"relallvisible", "integer"},
+};
+
+/*
+ * Tabular description of the parameters to pg_restore_attribute_stats()
+ * param_name, param_type
+ */
+static const char *att_stats_arginfo[][2] = {
+	{"relation", "regclass"},
+	{"attname", "name"},
+	{"inherited", "boolean"},
+	{"version", "integer"},
+	{"null_frac", "float4"},
+	{"avg_width", "integer"},
+	{"n_distinct", "float4"},
+	{"most_common_vals", "text"},
+	{"most_common_freqs", "float4[]"},
+	{"histogram_bounds", "text"},
+	{"correlation", "float4"},
+	{"most_common_elems", "text"},
+	{"most_common_elem_freqs", "float4[]"},
+	{"elem_count_histogram", "float4[]"},
+	{"range_length_histogram", "text"},
+	{"range_empty_frac", "float4"},
+	{"range_bounds_histogram", "text"},
+};
+
+/*
+ * getRelStatsExportQuery --
+ *
+ * Generate a query that will fetch all relation (e.g. pg_class)
+ * stats for a given relation.
+ */
+static void
+getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "current_setting('server_version_num') AS version, "
+						 "c.relpages, c.reltuples, c.relallvisible "
+						 "FROM pg_class c "
+						 "JOIN pg_namespace n "
+						 "ON n.oid = c.relnamespace "
+						 "WHERE n.nspname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND c.relname = ");
+	appendStringLiteralAH(query, relname, fout);
+}
+
+/*
+ * getAttStatsExportQuery --
+ *
+ * Generate a query that will fetch all attribute (e.g. pg_statistic)
+ * stats for a given relation.
+ */
+static void
+getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "s.attname,"
+						 "s.inherited,"
+						 "current_setting('server_version_num') AS version, "
+						 "s.null_frac,"
+						 "s.avg_width,"
+						 "s.n_distinct,"
+						 "s.most_common_vals,"
+						 "s.most_common_freqs,"
+						 "s.histogram_bounds,"
+						 "s.correlation,"
+						 "s.most_common_elems,"
+						 "s.most_common_elem_freqs,"
+						 "s.elem_count_histogram,");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 "s.range_length_histogram,"
+							 "s.range_empty_frac,"
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(query,
+							 "NULL AS range_length_histogram,"
+							 "NULL AS range_empty_frac,"
+							 "NULL AS range_bounds_histogram ");
+
+	appendPQExpBufferStr(query,
+						 "FROM pg_stats s "
+						 "JOIN pg_namespace n "
+						 "ON n.nspname = s.schemaname "
+						 "JOIN pg_class c "
+						 "ON c.relname = s.tablename "
+						 "AND c.relnamespace = n.oid "
+						 "WHERE s.schemaname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND s.tablename = ");
+	appendStringLiteralAH(query, relname, fout);
+	appendPQExpBufferStr(query, " ORDER BY s.attname, s.inherited");
+}
+
+
+/*
+ * appendNamedArgument --
+ *
+ * Convenience routine for constructing parameters of the form:
+ * 'paraname', 'value'::type
+ */
+static void
+appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
+					const char *argval, const char *argtype)
+{
+	appendPQExpBufferStr(out, "\t");
+
+	appendStringLiteralAH(out, argname, fout);
+	appendPQExpBufferStr(out, ", ");
+
+	appendStringLiteralAH(out, argval, fout);
+	appendPQExpBuffer(out, "::%s", argtype);
+}
+
+/*
+ * appendRelStatsImport --
+ *
+ * Append a formatted pg_restore_relation_stats statement.
+ */
+static void
+appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	const char *sep = "";
+
+	if (PQntuples(res) == 0)
+		return;
+
+	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
+
+	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
+	{
+		const char *argname = rel_stats_arginfo[argno][0];
+		const char *argtype = rel_stats_arginfo[argno][1];
+		int			fieldno = PQfnumber(res, argname);
+
+		if (fieldno < 0)
+			pg_fatal("relation stats export query missing field '%s'",
+					 argname);
+
+		if (PQgetisnull(res, 0, fieldno))
+			continue;
+
+		appendPQExpBufferStr(out, sep);
+		appendNamedArgument(out, fout, argname, PQgetvalue(res, 0, fieldno), argtype);
+
+		sep = ",\n";
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+/*
+ * appendAttStatsImport --
+ *
+ * Append a series of formatted pg_restore_attribute_stats statements.
+ */
+static void
+appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	{
+		const char *sep = "";
+
+		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+		for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
+		{
+			const char *argname = att_stats_arginfo[argno][0];
+			const char *argtype = att_stats_arginfo[argno][1];
+			int			fieldno = PQfnumber(res, argname);
+
+			if (fieldno < 0)
+				pg_fatal("attribute stats export query missing field '%s'",
+						 argname);
+
+			if (PQgetisnull(res, rownum, fieldno))
+				continue;
+
+			appendPQExpBufferStr(out, sep);
+			appendNamedArgument(out, fout, argname, PQgetvalue(res, rownum, fieldno), argtype);
+			sep = ",\n";
+		}
+		appendPQExpBufferStr(out, "\n);\n");
+	}
+}
+
+/*
+ * Decide which section to use based on the relkind of the parent object.
+ *
+ * NB: materialized views may be postponed from SECTION_PRE_DATA to
+ * SECTION_POST_DATA to resolve some kinds of dependency problems. If so, the
+ * matview stats will also be postponed to SECTION_POST_DATA. See
+ * repairMatViewBoundaryMultiLoop().
+ */
+static teSection
+statisticsDumpSection(const RelStatsInfo *rsinfo)
+{
+	switch (rsinfo->relkind)
+	{
+		case RELKIND_RELATION:
+		case RELKIND_PARTITIONED_TABLE:
+		case RELKIND_MATVIEW:
+			return SECTION_DATA;
+		case RELKIND_INDEX:
+		case RELKIND_PARTITIONED_INDEX:
+			return SECTION_POST_DATA;
+		default:
+			pg_fatal("cannot dump statistics for relation kind '%c'",
+					 rsinfo->relkind);
+	}
+
+	return 0;				/* keep compiler quiet */
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	PGresult	   *res;
+	PQExpBuffer		query;
+	PQExpBuffer		out;
+	PQExpBuffer		tag;
+	DumpableObject *dobj = (DumpableObject *) &rsinfo->dobj;
+	DumpId		   *deps = NULL;
+	int				ndeps = 0;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
+	/* dependent on the relation definition, if doing schema */
+	if (fout->dopt->dumpSchema)
+	{
+		deps = dobj->dependencies;
+		ndeps = dobj->nDeps;
+	}
+
+	tag = createPQExpBuffer();
+	appendPQExpBufferStr(tag, fmtId(dobj->name));
+
+	query = createPQExpBuffer();
+	out = createPQExpBuffer();
+
+	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendRelStatsImport(out, fout, res);
+	PQclear(res);
+
+	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendAttStatsImport(out, fout, res);
+	PQclear(res);
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATISTICS DATA",
+							  .section = rsinfo->postponed_def ?
+								SECTION_POST_DATA :  statisticsDumpSection(rsinfo),
+							  .createStmt = out->data,
+							  .deps = deps,
+							  .nDeps = ndeps));
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(out);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -10746,6 +11113,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_SUBSCRIPTION_REL:
 			dumpSubscriptionTable(fout, (const SubRelInfo *) dobj);
 			break;
+		case DO_REL_STATS:
+			dumpRelationStats(fout, (const RelStatsInfo *) dobj);
+			break;
 		case DO_PRE_DATA_BOUNDARY:
 		case DO_POST_DATA_BOUNDARY:
 			/* never dumped, nothing to do */
@@ -18975,6 +19345,16 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 				/* must come after the pre-data boundary */
 				addObjectDependency(dobj, preDataBound->dumpId);
 				break;
+			case DO_REL_STATS:
+				/* stats section varies by parent object type, DATA or POST */
+				if (statisticsDumpSection((RelStatsInfo *) dobj) == SECTION_DATA)
+				{
+					addObjectDependency(dobj, preDataBound->dumpId);
+					addObjectDependency(postDataBound, dobj->dumpId);
+				}
+				else
+					addObjectDependency(dobj, postDataBound->dumpId);
+				break;
 		}
 	}
 }
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 7139c88a69a..410c162a85d 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -83,10 +83,13 @@ typedef enum
 	DO_PUBLICATION,
 	DO_PUBLICATION_REL,
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
+	DO_REL_STATS,
 	DO_SUBSCRIPTION,
 	DO_SUBSCRIPTION_REL,		/* see note for SubRelInfo */
 } DumpableObjectType;
 
+#define NUM_DUMPABLE_OBJECT_TYPES (DO_SUBSCRIPTION_REL + 1)
+
 /*
  * DumpComponents is a bitmask of the potentially dumpable components of
  * a database object: its core definition, plus optional attributes such
@@ -110,6 +113,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
@@ -430,6 +434,13 @@ typedef struct _indexAttachInfo
 	IndxInfo   *partitionIdx;	/* link to index on partition */
 } IndexAttachInfo;
 
+typedef struct _relStatsInfo
+{
+	DumpableObject	dobj;
+	char			relkind;		/* 'r', 'm', 'i', etc */
+	bool			postponed_def;  /* stats must be postponed into post-data */
+} RelStatsInfo;
+
 typedef struct _statsExtInfo
 {
 	DumpableObject dobj;
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index dc9a28924bd..201eeedde73 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -81,6 +81,7 @@ enum dbObjectTypePriorities
 	PRIO_TABLE_DATA,
 	PRIO_SEQUENCE_SET,
 	PRIO_LARGE_OBJECT_DATA,
+	PRIO_STATISTICS_DATA_DATA,
 	PRIO_POST_DATA_BOUNDARY,	/* boundary! */
 	PRIO_CONSTRAINT,
 	PRIO_INDEX,
@@ -148,11 +149,12 @@ static const int dbObjectTypePriority[] =
 	[DO_PUBLICATION] = PRIO_PUBLICATION,
 	[DO_PUBLICATION_REL] = PRIO_PUBLICATION_REL,
 	[DO_PUBLICATION_TABLE_IN_SCHEMA] = PRIO_PUBLICATION_TABLE_IN_SCHEMA,
+	[DO_REL_STATS] = PRIO_STATISTICS_DATA_DATA,
 	[DO_SUBSCRIPTION] = PRIO_SUBSCRIPTION,
 	[DO_SUBSCRIPTION_REL] = PRIO_SUBSCRIPTION_REL,
 };
 
-StaticAssertDecl(lengthof(dbObjectTypePriority) == (DO_SUBSCRIPTION_REL + 1),
+StaticAssertDecl(lengthof(dbObjectTypePriority) == NUM_DUMPABLE_OBJECT_TYPES,
 				 "array length mismatch");
 
 static DumpId preDataBoundId;
@@ -801,11 +803,21 @@ repairMatViewBoundaryMultiLoop(DumpableObject *boundaryobj,
 {
 	/* remove boundary's dependency on object after it in loop */
 	removeObjectDependency(boundaryobj, nextobj->dumpId);
-	/* if that object is a matview, mark it as postponed into post-data */
+	/*
+	 * If that object is a matview or matview status, mark it as postponed into
+	 * post-data.
+	 */
 	if (nextobj->objType == DO_TABLE)
 	{
 		TableInfo  *nextinfo = (TableInfo *) nextobj;
 
+		if (nextinfo->relkind == RELKIND_MATVIEW)
+			nextinfo->postponed_def = true;
+	}
+	else if (nextobj->objType == DO_REL_STATS)
+	{
+		RelStatsInfo *nextinfo = (RelStatsInfo *) nextobj;
+
 		if (nextinfo->relkind == RELKIND_MATVIEW)
 			nextinfo->postponed_def = true;
 	}
@@ -1018,6 +1030,21 @@ repairDependencyLoop(DumpableObject **loop,
 					{
 						DumpableObject *nextobj;
 
+						nextobj = (j < nLoop - 1) ? loop[j + 1] : loop[0];
+						repairMatViewBoundaryMultiLoop(loop[j], nextobj);
+						return;
+					}
+				}
+			}
+			else if (loop[i]->objType == DO_REL_STATS &&
+					 ((RelStatsInfo *) loop[i])->relkind == RELKIND_MATVIEW)
+			{
+				for (j = 0; j < nLoop; j++)
+				{
+					if (loop[j]->objType == DO_POST_DATA_BOUNDARY)
+					{
+						DumpableObject *nextobj;
+
 						nextobj = (j < nLoop - 1) ? loop[j + 1] : loop[0];
 						repairMatViewBoundaryMultiLoop(loop[j], nextobj);
 						return;
@@ -1500,6 +1527,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "POST-DATA BOUNDARY  (ID %d)",
 					 obj->dumpId);
 			return;
+		case DO_REL_STATS:
+			snprintf(buf, bufsize,
+					 "RELATION STATISTICS FOR %s  (ID %d OID %u)",
+					 obj->name, obj->dumpId, obj->catId.oid);
+			return;
 	}
 	/* shouldn't get here */
 	snprintf(buf, bufsize,
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 64a60a26092..90847c7c8c7 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -103,6 +103,9 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_data = 0;
+static int	no_schema = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -139,6 +142,7 @@ main(int argc, char *argv[])
 		{"port", required_argument, NULL, 'p'},
 		{"roles-only", no_argument, NULL, 'r'},
 		{"schema-only", no_argument, NULL, 's'},
+		{"statistics-only", no_argument, NULL, 'X'},
 		{"superuser", required_argument, NULL, 'S'},
 		{"tablespaces-only", no_argument, NULL, 't'},
 		{"username", required_argument, NULL, 'U'},
@@ -168,10 +172,13 @@ main(int argc, char *argv[])
 		{"role", required_argument, NULL, 3},
 		{"use-set-session-authorization", no_argument, &use_setsessauth, 1},
 		{"no-comments", no_argument, &no_comments, 1},
+		{"no-data", no_argument, &no_data, 1},
 		{"no-publications", no_argument, &no_publications, 1},
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
+		{"no-schema", no_argument, &no_schema, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -328,6 +335,10 @@ main(int argc, char *argv[])
 				appendPQExpBufferStr(pgdumpopts, " -x");
 				break;
 
+			case 'X':
+				appendPQExpBufferStr(pgdumpopts, " -X");
+				break;
+
 			case 0:
 				break;
 
@@ -447,10 +458,16 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --use-set-session-authorization");
 	if (no_comments)
 		appendPQExpBufferStr(pgdumpopts, " --no-comments");
+	if (no_data)
+		appendPQExpBufferStr(pgdumpopts, " --no-data");
 	if (no_publications)
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_schema)
+		appendPQExpBufferStr(pgdumpopts, " --no-schema");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -667,6 +684,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index c602272d7db..02af89bae15 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -63,6 +63,9 @@ main(int argc, char **argv)
 	int			numWorkers = 1;
 	Archive    *AH;
 	char	   *inputFileSpec;
+	bool		data_only = false;
+	bool		schema_only = false;
+	bool		statistics_only = false;
 	static int	disable_triggers = 0;
 	static int	enable_row_security = 0;
 	static int	if_exists = 0;
@@ -71,12 +74,13 @@ main(int argc, char **argv)
 	static int	outputNoTablespaces = 0;
 	static int	use_setsessauth = 0;
 	static int	no_comments = 0;
+	static int	no_data = 0;
 	static int	no_publications = 0;
+	static int	no_schema = 0;
 	static int	no_security_labels = 0;
+	static int  no_statistics = 0;
 	static int	no_subscriptions = 0;
 	static int	strict_names = 0;
-	bool		data_only = false;
-	bool		schema_only = false;
 
 	struct option cmdopts[] = {
 		{"clean", 0, NULL, 'c'},
@@ -108,6 +112,7 @@ main(int argc, char **argv)
 		{"username", 1, NULL, 'U'},
 		{"verbose", 0, NULL, 'v'},
 		{"single-transaction", 0, NULL, '1'},
+		{"statistics-only", no_argument, NULL, 'X'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -124,9 +129,12 @@ main(int argc, char **argv)
 		{"transaction-size", required_argument, NULL, 5},
 		{"use-set-session-authorization", no_argument, &use_setsessauth, 1},
 		{"no-comments", no_argument, &no_comments, 1},
+		{"no-data", no_argument, &no_data, 1},
 		{"no-publications", no_argument, &no_publications, 1},
+		{"no-schema", no_argument, &no_schema, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"filter", required_argument, NULL, 4},
 
 		{NULL, 0, NULL, 0}
@@ -271,6 +279,10 @@ main(int argc, char **argv)
 				opts->aclsSkip = 1;
 				break;
 
+			case 'X':			/* Restore statistics only */
+				statistics_only = true;
+				break;
+
 			case '1':			/* Restore data in a single transaction */
 				opts->single_txn = true;
 				opts->exit_on_error = true;
@@ -343,6 +355,10 @@ main(int argc, char **argv)
 
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
+	if (schema_only && statistics_only)
+		pg_fatal("options -s/--schema-only and -X/--statistics-only cannot be used together");
 
 	if (data_only && opts->dropSchema)
 		pg_fatal("options -c/--clean and -a/--data-only cannot be used together");
@@ -362,8 +378,9 @@ main(int argc, char **argv)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
 	/* set derivative flags */
-	opts->dumpSchema = (!data_only);
-	opts->dumpData = (!schema_only);
+	opts->dumpData = data_only || (!no_data && !schema_only && !statistics_only);
+	opts->dumpSchema = schema_only || (!no_schema && !data_only && !statistics_only);
+	opts->dumpStatistics = statistics_only || (!no_statistics && !data_only && !schema_only);
 
 	opts->disable_triggers = disable_triggers;
 	opts->enable_row_security = enable_row_security;
@@ -375,6 +392,8 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
+	opts->no_data = no_data;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
@@ -482,6 +501,7 @@ usage(const char *progname)
 	printf(_("  -t, --table=NAME             restore named relation (table, view, etc.)\n"));
 	printf(_("  -T, --trigger=NAME           restore named trigger\n"));
 	printf(_("  -x, --no-privileges          skip restoration of access privileges (grant/revoke)\n"));
+	printf(_("  -X, --statistics-only        restore only the statistics, not schema or data\n"));
 	printf(_("  -1, --single-transaction     restore as a single transaction\n"));
 	printf(_("  --disable-triggers           disable triggers during data-only restore\n"));
 	printf(_("  --enable-row-security        enable row security\n"));
@@ -489,10 +509,13 @@ usage(const char *progname)
 			 "                               in FILENAME\n"));
 	printf(_("  --if-exists                  use IF EXISTS when dropping objects\n"));
 	printf(_("  --no-comments                do not restore comment commands\n"));
+	printf(_("  --no-data                    do not restore data\n"));
 	printf(_("  --no-data-for-failed-tables  do not restore data of tables that could not be\n"
 			 "                               created\n"));
 	printf(_("  --no-publications            do not restore publications\n"));
+	printf(_("  --no-schema                  do not restore schema\n"));
 	printf(_("  --no-security-labels         do not restore security labels\n"));
+	printf(_("  --no-statistics              do not restore statistics\n"));
 	printf(_("  --no-subscriptions           do not restore subscriptions\n"));
 	printf(_("  --no-table-access-method     do not restore table access methods\n"));
 	printf(_("  --no-tablespaces             do not restore tablespace assignments\n"));
diff --git a/src/bin/pg_dump/t/001_basic.pl b/src/bin/pg_dump/t/001_basic.pl
index 214240f1ae5..f29da06ed28 100644
--- a/src/bin/pg_dump/t/001_basic.pl
+++ b/src/bin/pg_dump/t/001_basic.pl
@@ -50,12 +50,30 @@ command_fails_like(
 	'pg_dump: options -s/--schema-only and -a/--data-only cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '-s', '-X' ],
+	qr/\Qpg_dump: error: options -s\/--schema-only and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: error: options -s/--schema-only and -X/--statistics-only cannot be used together'
+);
+
+command_fails_like(
+	[ 'pg_dump', '-a', '-X' ],
+	qr/\Qpg_dump: error: options -a\/--data-only and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: error: options -a/--data-only and -X/--statistics-only cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-s', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: options -s\/--schema-only and --include-foreign-data cannot be used together\E/,
 	'pg_dump: options -s/--schema-only and --include-foreign-data cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '--statistics-only', '--no-statistics' ],
+	qr/\Qpg_dump: error: options -X\/--statistics-only and --no-statistics cannot be used together\E/,
+	'pg_dump: options -X\/--statistics-only and --no-statistics cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-j2', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: option --include-foreign-data is not supported with parallel backup\E/,
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index bc5d9222a20..e1545e797a7 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -65,7 +65,7 @@ my %pgdump_runs = (
 			'--format' => 'custom',
 			'--file' => "$tempdir/binary_upgrade.dump",
 			'--no-password',
-			'--schema-only',
+			'--no-data',
 			'--binary-upgrade',
 			'--dbname' => 'postgres',    # alternative way to specify database
 		],
@@ -710,6 +710,39 @@ my %pgdump_runs = (
 			'--no-large-objects',
 			'postgres',
 		],
+	},
+	no_statistics => {
+		dump_cmd => [
+			'pg_dump', '--no-sync',
+			"--file=$tempdir/no_statistics.sql",
+			'--no-statistics',
+			'postgres',
+		],
+	},
+	no_data_no_schema => {
+		dump_cmd => [
+			'pg_dump', '--no-sync',
+			"--file=$tempdir/no_data_no_schema.sql",
+			'--no-data',
+			'--no-schema',
+			'postgres',
+		],
+	},
+	statistics_only => {
+		dump_cmd => [
+			'pg_dump', '--no-sync',
+			"--file=$tempdir/statistics_only.sql",
+			'--statistics-only',
+			'postgres',
+		],
+	},
+	no_schema => {
+		dump_cmd => [
+			'pg_dump', '--no-sync',
+			"--file=$tempdir/no_schema.sql",
+			'--no-schema',
+			'postgres',
+		],
 	},);
 
 ###############################################################
@@ -776,6 +809,7 @@ my %full_runs = (
 	no_large_objects => 1,
 	no_owner => 1,
 	no_privs => 1,
+	no_statistics => 1,
 	no_table_access_method => 1,
 	pg_dumpall_dbprivs => 1,
 	pg_dumpall_exclude => 1,
@@ -977,6 +1011,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1390,6 +1425,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1411,6 +1447,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1432,6 +1469,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1598,6 +1636,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 		},
@@ -1751,6 +1790,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			only_dump_test_table => 1,
 			section_data => 1,
 		},
@@ -1778,6 +1818,7 @@ my %tests = (
 			data_only => 1,
 			exclude_test_table => 1,
 			exclude_test_table_data => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1798,7 +1839,10 @@ my %tests = (
 			\QCOPY dump_test.fk_reference_test_table (col1) FROM stdin;\E
 			\n(?:\d\n){5}\\\.\n
 			/xms,
-		like => { data_only => 1, },
+		like => {
+			data_only => 1,
+			no_schema => 1,
+		},
 	},
 
 	'COPY test_second_table' => {
@@ -1814,6 +1858,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1836,6 +1881,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1859,6 +1905,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1881,6 +1928,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -1903,6 +1951,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 		},
 		unlike => {
@@ -3299,6 +3348,7 @@ my %tests = (
 		like => {
 			%full_runs,
 			data_only => 1,
+			no_schema => 1,
 			section_data => 1,
 			only_dump_test_schema => 1,
 			test_schema_plus_large_objects => 1,
@@ -3469,6 +3519,7 @@ my %tests = (
 			%full_runs,
 			%dump_test_schema_runs,
 			data_only => 1,
+			no_schema => 1,
 			only_dump_measurement => 1,
 			section_data => 1,
 			only_dump_test_schema => 1,
@@ -4353,6 +4404,7 @@ my %tests = (
 			column_inserts => 1,
 			data_only => 1,
 			inserts => 1,
+			no_schema => 1,
 			section_data => 1,
 			test_schema_plus_large_objects => 1,
 			binary_upgrade => 1,
@@ -4653,6 +4705,61 @@ my %tests = (
 		},
 	},
 
+	#
+	# TABLE and MATVIEW stats will end up in SECTION_DATA.
+	# INDEX stats (expression columns only) will end up in SECTION_POST_DATA.
+	#
+	'statistics_import' => {
+		create_sql => '
+			CREATE TABLE dump_test.has_stats
+			AS SELECT g.g AS x, g.g / 2 AS y FROM generate_series(1,100) AS g(g);
+			CREATE MATERIALIZED VIEW dump_test.has_stats_mv AS SELECT * FROM dump_test.has_stats;
+			CREATE INDEX dup_test_post_data_ix ON dump_test.has_stats((x - 1));
+			ANALYZE dump_test.has_stats, dump_test.has_stats_mv;',
+		regexp => qr/pg_catalog.pg_restore_attribute_stats/,
+		like => {
+			%full_runs,
+			%dump_test_schema_runs,
+			no_data_no_schema => 1,
+			no_schema => 1,
+			section_data => 1,
+			section_post_data => 1,
+			statistics_only => 1,
+			},
+		unlike => {
+			exclude_dump_test_schema => 1,
+			no_statistics => 1,
+			only_dump_measurement => 1,
+			schema_only => 1,
+			},
+	},
+
+	#
+	# While attribute stats (aka pg_statistic stats) only appear for tables
+	# that have been analyzed, all tables will have relation stats because
+	# those come from pg_class.
+	#
+	'relstats_on_unanalyzed_tables' => {
+		regexp => qr/pg_catalog.pg_restore_relation_stats/,
+
+		like => {
+			%full_runs,
+			%dump_test_schema_runs,
+			no_data_no_schema => 1,
+			no_schema => 1,
+			only_dump_test_table => 1,
+			role => 1,
+			role_parallel => 1,
+			section_data => 1,
+			section_post_data => 1,
+			statistics_only => 1,
+			},
+		unlike => {
+			no_statistics => 1,
+			schema_only => 1,
+			},
+	},
+
 	# CREATE TABLE with partitioned table and various AMs.  One
 	# partition uses the same default as the parent, and a second
 	# uses its own AM.
diff --git a/src/bin/pg_upgrade/dump.c b/src/bin/pg_upgrade/dump.c
index 8ce0fa3020e..7d0871e3c5e 100644
--- a/src/bin/pg_upgrade/dump.c
+++ b/src/bin/pg_upgrade/dump.c
@@ -21,8 +21,8 @@ generate_old_dump(void)
 
 	/* run new pg_dumpall binary for globals */
 	exec_prog(UTILITY_LOG_FILE, NULL, true, true,
-			  "\"%s/pg_dumpall\" %s --globals-only --quote-all-identifiers "
-			  "--binary-upgrade %s --no-sync -f \"%s/%s\"",
+			  "\"%s/pg_dumpall\" %s %s --globals-only --quote-all-identifiers "
+			  "--binary-upgrade --no-sync -f \"%s/%s\"",
 			  new_cluster.bindir, cluster_conn_opts(&old_cluster),
 			  log_opts.verbose ? "--verbose" : "",
 			  log_opts.dumpdir,
@@ -52,10 +52,11 @@ generate_old_dump(void)
 		snprintf(log_file_name, sizeof(log_file_name), DB_DUMP_LOG_FILE_MASK, old_db->db_oid);
 
 		parallel_exec_prog(log_file_name, NULL,
-						   "\"%s/pg_dump\" %s --schema-only --quote-all-identifiers "
+						   "\"%s/pg_dump\" %s --no-data %s --quote-all-identifiers "
 						   "--binary-upgrade --format=custom %s --no-sync --file=\"%s/%s\" %s",
 						   new_cluster.bindir, cluster_conn_opts(&old_cluster),
 						   log_opts.verbose ? "--verbose" : "",
+						   user_opts.do_statistics ? "" : "--no-statistics",
 						   log_opts.dumpdir,
 						   sql_file_name, escaped_connstr.data);
 
diff --git a/src/bin/pg_upgrade/option.c b/src/bin/pg_upgrade/option.c
index 108eb7a1ba4..3b6c7ec994e 100644
--- a/src/bin/pg_upgrade/option.c
+++ b/src/bin/pg_upgrade/option.c
@@ -60,6 +60,8 @@ parseCommandLine(int argc, char *argv[])
 		{"copy", no_argument, NULL, 2},
 		{"copy-file-range", no_argument, NULL, 3},
 		{"sync-method", required_argument, NULL, 4},
+		{"with-statistics", no_argument, NULL, 5},
+		{"no-statistics", no_argument, NULL, 6},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -70,6 +72,7 @@ parseCommandLine(int argc, char *argv[])
 
 	user_opts.do_sync = true;
 	user_opts.transfer_mode = TRANSFER_MODE_COPY;
+	user_opts.do_statistics = true;
 
 	os_info.progname = get_progname(argv[0]);
 
@@ -212,6 +215,13 @@ parseCommandLine(int argc, char *argv[])
 				user_opts.sync_method = pg_strdup(optarg);
 				break;
 
+			case 5:
+				user_opts.do_statistics = true;
+				break;
+			case 6:
+				user_opts.do_statistics = false;
+				break;
+
 			default:
 				fprintf(stderr, _("Try \"%s --help\" for more information.\n"),
 						os_info.progname);
@@ -306,7 +316,9 @@ usage(void)
 	printf(_("  --clone                       clone instead of copying files to new cluster\n"));
 	printf(_("  --copy                        copy files to new cluster (default)\n"));
 	printf(_("  --copy-file-range             copy files to new cluster with copy_file_range\n"));
+	printf(_("  --no-statistics               do not import statistics from old cluster\n"));
 	printf(_("  --sync-method=METHOD          set method for syncing files to disk\n"));
+	printf(_("  --with-statistics             import statistics from old cluster (default)\n"));
 	printf(_("  -?, --help                    show this help, then exit\n"));
 	printf(_("\n"
 			 "Before running pg_upgrade you must:\n"
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index 0cdd675e4f1..3fe111fbde5 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -327,6 +327,7 @@ typedef struct
 	int			jobs;			/* number of processes/threads to use */
 	char	   *socketdir;		/* directory to use for Unix sockets */
 	char	   *sync_method;
+	bool		do_statistics;	/* carry over statistics from old cluster */
 } UserOpts;
 
 typedef struct
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index 24fcc76d72c..0d5e7d5a2d7 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -123,7 +123,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Dump only the data, not the schema (data definitions).
+        Dump only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are dumped.
        </para>
 
@@ -141,13 +141,11 @@ PostgreSQL documentation
       <listitem>
        <para>
         Include large objects in the dump.  This is the default behavior
-        except when <option>--schema</option>, <option>--table</option>, or
-        <option>--schema-only</option> is specified.  The <option>-b</option>
-        switch is therefore only useful to add large objects to dumps
-        where a specific schema or table has been requested.  Note that
-        large objects are considered data and therefore will be included when
-        <option>--data-only</option> is used, but not
-        when <option>--schema-only</option> is.
+        except when <option>--schema</option>, <option>--table</option>,
+        <option>--schema-only</option>, or <option>--statistics-only</option>, or
+        <option>--no-data</option> is specified.  The <option>-b</option>
+        switch is therefore only useful to add large objects to dumps where a
+        specific schema or table has been requested.
        </para>
       </listitem>
      </varlistentry>
@@ -516,10 +514,11 @@ PostgreSQL documentation
       <term><option>--schema-only</option></term>
       <listitem>
        <para>
-        Dump only the object definitions (schema), not data.
+        Dump only the object definitions (schema), not data or statistics.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive to <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
         <option>--section=pre-data --section=post-data</option>.
@@ -652,6 +651,18 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Dump only the statistics, not the schema (data definitions) or data.
+        Statistics for tables, materialized views, and indexes are dumped.
+       </para>
+
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-Z <replaceable class="parameter">level</replaceable></option></term>
       <term><option>-Z <replaceable class="parameter">method</replaceable></option>[:<replaceable>detail</replaceable>]</term>
@@ -741,7 +752,8 @@ PostgreSQL documentation
       <term><option>--disable-triggers</option></term>
       <listitem>
        <para>
-        This option is relevant only when creating a data-only dump.
+        This option is relevant only when creating a dump that includes data
+        but does not include schema.
         It instructs <application>pg_dump</application> to include commands
         to temporarily disable triggers on the target tables while
         the data is restored.  Use this if you have referential
@@ -824,16 +836,17 @@ PostgreSQL documentation
       <term><option>--exclude-table-data=<replaceable class="parameter">pattern</replaceable></option></term>
       <listitem>
        <para>
-        Do not dump data for any tables matching <replaceable
+        Do not dump data or statistics for any tables matching <replaceable
         class="parameter">pattern</replaceable>. The pattern is
         interpreted according to the same rules as for <option>-t</option>.
         <option>--exclude-table-data</option> can be given more than once to
-        exclude tables matching any of several patterns. This option is
-        useful when you need the definition of a particular table even
-        though you do not need the data in it.
+        exclude tables matching any of several patterns. This option is useful
+        when you need the definition of a particular table even though you do
+        not need the data in it.
        </para>
        <para>
-        To exclude data for all tables in the database, see <option>--schema-only</option>.
+        To exclude data for all tables in the database, see <option>--schema-only</option>
+        or <option>--statistics-only</option>.
        </para>
       </listitem>
      </varlistentry>
@@ -1080,6 +1093,15 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-data</option></term>
+      <listitem>
+       <para>
+        Do not dump data.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-publications</option></term>
       <listitem>
@@ -1098,6 +1120,24 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-schema</option></term>
+      <listitem>
+       <para>
+        Do not dump schema (data definitions).
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
@@ -1236,9 +1276,11 @@ PostgreSQL documentation
          </para>
          <para>
           The data section contains actual table data, large-object
-          contents, and sequence values.
+          contents, statitistics for tables and materialized views and
+          sequence values.
           Post-data items include definitions of indexes, triggers, rules,
-          and constraints other than validated check constraints.
+          statistics for indexes, and constraints other than validated check
+          constraints.
           Pre-data items include all other data definition items.
          </para>
        </listitem>
@@ -1581,7 +1623,7 @@ CREATE DATABASE foo WITH TEMPLATE template0;
   </para>
 
   <para>
-   When a data-only dump is chosen and the option <option>--disable-triggers</option>
+   When a dump without schema is chosen and the option <option>--disable-triggers</option>
    is used, <application>pg_dump</application> emits commands
    to disable triggers on user tables before inserting the data,
    and then commands to re-enable them after the data has been
diff --git a/doc/src/sgml/ref/pg_dumpall.sgml b/doc/src/sgml/ref/pg_dumpall.sgml
index 39d93c2c0e3..15fb40e7be9 100644
--- a/doc/src/sgml/ref/pg_dumpall.sgml
+++ b/doc/src/sgml/ref/pg_dumpall.sgml
@@ -81,7 +81,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Dump only the data, not the schema (data definitions).
+        Dump only the data, not the schema (data definitions) or statistics.
        </para>
       </listitem>
      </varlistentry>
@@ -265,6 +265,17 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Dump only the statistics, not the schema (data definitions) or data.
+        Statistics for tables, materialized views, and indexes are dumped.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--binary-upgrade</option></term>
       <listitem>
@@ -307,7 +318,7 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       <term><option>--disable-triggers</option></term>
       <listitem>
        <para>
-        This option is relevant only when creating a data-only dump.
+        This option is relevant only when creating a dump with data and without schema.
         It instructs <application>pg_dumpall</application> to include commands
         to temporarily disable triggers on the target tables while
         the data is restored.  Use this if you have referential
@@ -422,6 +433,15 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-data</option></term>
+      <listitem>
+       <para>
+        Do not dump data.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-publications</option></term>
       <listitem>
@@ -447,6 +467,15 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-schema</option></term>
+      <listitem>
+       <para>
+        Do not dump schema (data definitions).
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-security-labels</option></term>
       <listitem>
@@ -456,6 +485,15 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index b8b27e1719e..86e4264714d 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -94,7 +94,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Restore only the data, not the schema (data definitions).
+        Restore only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are restored,
         if present in the archive.
        </para>
@@ -483,10 +483,11 @@ PostgreSQL documentation
         to the extent that schema entries are present in the archive.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive of <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
-        <option>--section=pre-data --section=post-data</option>.
+        <option>--section=pre-data --section=post-data --no-statistics</option>.
        </para>
        <para>
         (Do not confuse this with the <option>--schema</option> option, which
@@ -599,6 +600,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Restore only the statistics, not schema (data definitions) or data.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-1</option></term>
       <term><option>--single-transaction</option></term>
@@ -617,7 +628,7 @@ PostgreSQL documentation
       <term><option>--disable-triggers</option></term>
       <listitem>
        <para>
-        This option is relevant only when performing a data-only restore.
+        This option is relevant only when performing a restore without schema.
         It instructs <application>pg_restore</application> to execute commands
         to temporarily disable triggers on the target tables while
         the data is restored.  Use this if you have referential
@@ -681,6 +692,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-data</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore data, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-data-for-failed-tables</option></term>
       <listitem>
@@ -713,6 +734,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-schema</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore schema (data definitions), even if
+        the archive contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-security-labels</option></term>
       <listitem>
@@ -723,6 +754,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore statistics, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pgupgrade.sgml b/doc/src/sgml/ref/pgupgrade.sgml
index 4777381dac2..64a1ebd613b 100644
--- a/doc/src/sgml/ref/pgupgrade.sgml
+++ b/doc/src/sgml/ref/pgupgrade.sgml
@@ -145,6 +145,24 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--with-statistics</option></term>
+      <listitem>
+       <para>
+        Restore statistics from the old cluster into the new cluster.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not restore statistics from the old cluster into the new cluster.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-o</option> <replaceable class="parameter">options</replaceable></term>
       <term><option>--old-options</option> <replaceable class="parameter">options</replaceable></term>
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index b6c170ac249..534481ed8a3 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2401,6 +2401,7 @@ RelMapFile
 RelMapping
 RelOptInfo
 RelOptKind
+RelStatsInfo
 RelToCheck
 RelToCluster
 RelabelType

base-commit: 4dd09a1d415d143386d5e6dc9519615a8b18f850
-- 
2.48.1

#308Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#307)
Re: Statistics Import and Export

On Tue, 2025-02-11 at 14:02 -0500, Corey Huinker wrote:

The previous 0001 is now committed (thanks!) so only one remains.
 

Summary of the decisions made in this thread:

* pg_dump --data-only does not include stats[1]/messages/by-id/b40b81d38c3a87fdef61e4f7abfbc7f27c7fbcd9.camel@j-davis.com. This behavior was
not fully resolved, but I didn't see a reasonable set of options
where (a) --data-only would include stats; and (b) we could
specify what pg_upgrade needs, which is schema+stats. Jian seemed
to agree. However, this leaves us with the behavior where
--data-only doesn't get everything in SECTION_DATA, which might be
undesirable.
* stats are in SECTION_DATA[2]/messages/by-id/1798867.1712376328@sss.pgh.pa.us, except for stats on objects that
are created in SECTION_POST_DATA, in which case the stats are
also in SECTION_POST_DATA
- indexes are created in SECTION_POST_DATA, and though plain
indexes don't have stats, expression indexes do
- MVs are normally created in SECTION_PRE_DATA, in which case
the stats are in SECTION_DATA; but MVs can be deferred to
SECTION_POST_DATA due to dependency on a primary key
* SECTION_NONE was proposed, but rejected[2]/messages/by-id/1798867.1712376328@sss.pgh.pa.us
* The default is to include the stats.[3]/messages/by-id/3228677.1713844341@sss.pgh.pa.us
* pg_dump[all] options are designed to allow specifying any
combination of schema[4]/messages/by-id/CACJufxG6K4EAUROhdr0wkzMh5QyFLmdLZeAoh7Vh0-VbuAtHcw@mail.gmail.com, data, and stats:
--schema-only (schema), --no-schema (data+stats)
--data-only (data), --no-data (schema+stats)
--stats-only (stats), --no-stats (schema+data)
* A SECTION_STATS was proposed and rejected due to complexity[5]/messages/by-id/3156140.1713817153@sss.pgh.pa.us
* The prefix in the dump output will be "Statistics for " (instead
of "Data for ")[6]/messages/by-id/d8df5339cab25b5720667beaaed8a8bb8e11578c.camel@j-davis.com
* The TOC description will be "STATISTICS DATA", differentiating
it from an extended statistics object[6]/messages/by-id/d8df5339cab25b5720667beaaed8a8bb8e11578c.camel@j-davis.com
* pg_upgrade will now pass --no-data (schema+stats) to pg_dump
instead of --schema-only, thereby transferring the stats to the
new cluster[7]/messages/by-id/c2bc08dfec336c03f7a7165d1347e2b52cf98b17.camel@j-davis.com

It's been a long thread, so please tell me if I missed something or if
something needs more discussion.

I'm still reviewing v48, but I intend to commit something soon.

Regards,
Jeff Davis

[1]: /messages/by-id/b40b81d38c3a87fdef61e4f7abfbc7f27c7fbcd9.camel@j-davis.com
/messages/by-id/b40b81d38c3a87fdef61e4f7abfbc7f27c7fbcd9.camel@j-davis.com

[2]: /messages/by-id/1798867.1712376328@sss.pgh.pa.us
/messages/by-id/1798867.1712376328@sss.pgh.pa.us

[3]: /messages/by-id/3228677.1713844341@sss.pgh.pa.us
/messages/by-id/3228677.1713844341@sss.pgh.pa.us

[4]: /messages/by-id/CACJufxG6K4EAUROhdr0wkzMh5QyFLmdLZeAoh7Vh0-VbuAtHcw@mail.gmail.com
/messages/by-id/CACJufxG6K4EAUROhdr0wkzMh5QyFLmdLZeAoh7Vh0-VbuAtHcw@mail.gmail.com

[5]: /messages/by-id/3156140.1713817153@sss.pgh.pa.us
/messages/by-id/3156140.1713817153@sss.pgh.pa.us

[6]: /messages/by-id/d8df5339cab25b5720667beaaed8a8bb8e11578c.camel@j-davis.com
/messages/by-id/d8df5339cab25b5720667beaaed8a8bb8e11578c.camel@j-davis.com

[7]: /messages/by-id/c2bc08dfec336c03f7a7165d1347e2b52cf98b17.camel@j-davis.com
/messages/by-id/c2bc08dfec336c03f7a7165d1347e2b52cf98b17.camel@j-davis.com

#309Jeff Davis
pgsql@j-davis.com
In reply to: Jeff Davis (#308)
Re: Statistics Import and Export

On Wed, 2025-02-12 at 19:00 -0800, Jeff Davis wrote:

I'm still reviewing v48, but I intend to commit something soon.

Committed with some revisions on top of v48:

* removed the short option -X, leaving the long option "--statistics-
only" with the same meaning.

* removed the redundant --with-statistics option for pg_upgrade,
because that's the default anyway.

* removed an unnecessary enum TocEntryType and cleaned up the API to
just pass the desired prefix directly to _printTocEntry().

* stabilized the 002_pg_upgrade test by turning off autovacuum before
the first pg_dumpall (we still want it to run before that to collect
stats).

* stabilized the 027_stream_regress recovery test by specifying --no-
statistics when comparing the data on primary and standby

* fixed the cross-version upgrade tests by using the
adjust_old_dumpfile to replace the version specifier with 000000 in the
argument list to pg_restore_* functions.

Regards,
Jeff Davis

#310vignesh C
vignesh21@gmail.com
In reply to: Jeff Davis (#309)
Re: Statistics Import and Export

On Thu, 20 Feb 2025 at 15:09, Jeff Davis <pgsql@j-davis.com> wrote:

On Wed, 2025-02-12 at 19:00 -0800, Jeff Davis wrote:

I'm still reviewing v48, but I intend to commit something soon.

Committed with some revisions on top of v48:

I was checking buildfarm for another commit of mine, while checking I
noticed there is a failure in crake at [1].  I felt it might be
related to this commit as it had passed with earlier runs:
--- /home/andrew/bf/root/upgrade.crake/HEAD/origin-REL9_2_STABLE.sql.fixed
2025-02-20 04:43:40.461092087 -0500
+++ /home/andrew/bf/root/upgrade.crake/HEAD/converted-REL9_2_STABLE-to-HEAD.sql.fixed
2025-02-20 04:43:40.463092092 -0500
@@ -184,21 +184,87 @@
 --
 SELECT * FROM pg_catalog.pg_restore_relation_stats(
  'relation', '"MySchema"."Foo"'::regclass,
- 'version', '90224'::integer,
- 'relpages', '0'::integer,
- 'reltuples', '0'::real,
+ 'version', '000000'::integer,
+ 'relpages', '1'::integer,
+ 'reltuples', '1'::real,
  'relallvisible', '0'::integer

[1]: https://buildfarm.postgresql.org/cgi-bin/show_stage_log.pl?nm=crake&amp;dt=2025-02-20%2009%3A32%3A03&amp;stg=xversion-upgrade-REL9_2_STABLE-HEAD

Regards,
Vignesh

#311Andrew Dunstan
andrew@dunslane.net
In reply to: Jeff Davis (#309)
Re: Statistics Import and Export

On 2025-02-20 Th 4:39 AM, Jeff Davis wrote:

On Wed, 2025-02-12 at 19:00 -0800, Jeff Davis wrote:

I'm still reviewing v48, but I intend to commit something soon.

Committed with some revisions on top of v48:

* removed the short option -X, leaving the long option "--statistics-
only" with the same meaning.

* removed the redundant --with-statistics option for pg_upgrade,
because that's the default anyway.

* removed an unnecessary enum TocEntryType and cleaned up the API to
just pass the desired prefix directly to _printTocEntry().

* stabilized the 002_pg_upgrade test by turning off autovacuum before
the first pg_dumpall (we still want it to run before that to collect
stats).

* stabilized the 027_stream_regress recovery test by specifying --no-
statistics when comparing the data on primary and standby

* fixed the cross-version upgrade tests by using the
adjust_old_dumpfile to replace the version specifier with 000000 in the
argument list to pg_restore_* functions.

The buildfarm doesn't like with this.
<https://buildfarm.postgresql.org/cgi-bin/show_stage_log.pl?nm=crake&amp;dt=2025-02-20%2009%3A32%3A03&amp;stg=xversion-upgrade-REL9_2_STABLE-HEAD&gt;

The conversion regexes are wrong for versions < 10, where the major
version is '9.x', but that just seems to be the tip of the iceberg.

cheers

andrew

--
Andrew Dunstan
EDB: https://www.enterprisedb.com

#312Hayato Kuroda (Fujitsu)
kuroda.hayato@fujitsu.com
In reply to: Andrew Dunstan (#311)
RE: Statistics Import and Export

Dear members,

I hope I'm in the correct thread. I found the commit 1fd1bd8 - it is so cool.
I have a question for it.

ISTM commit message said that no need to do ANALYZE again.

```
Add support to pg_dump for dumping stats, and use that during
pg_upgrade so that statistics are transferred during upgrade. In most
cases this removes the need for a costly re-analyze after upgrade.
```

But pgupgrade.sgml [2]``` Because optimizer statistics are not transferred by <command>pg_upgrade</command>, you will be instructed to run a command to regenerate that information at the end of the upgrade. You might need to set connection parameters to match your new cluster. ``` and source code [3]``` pg_log(PG_REPORT, "Optimizer statistics are not transferred by pg_upgrade.\n" "Once you start the new server, consider running:\n" " %s/vacuumdb %s--all --analyze-in-stages", new_cluster.bindir, user_specification.data); ``` said that statistics must be updated.
Did I miss something, or you have been updating this?

[2]: ``` Because optimizer statistics are not transferred by <command>pg_upgrade</command>, you will be instructed to run a command to regenerate that information at the end of the upgrade. You might need to set connection parameters to match your new cluster. ```
```
Because optimizer statistics are not transferred by <command>pg_upgrade</command>, you will
be instructed to run a command to regenerate that information at the end
of the upgrade. You might need to set connection parameters to
match your new cluster.
```
[3]: ``` pg_log(PG_REPORT, "Optimizer statistics are not transferred by pg_upgrade.\n" "Once you start the new server, consider running:\n" " %s/vacuumdb %s--all --analyze-in-stages", new_cluster.bindir, user_specification.data); ```
```
pg_log(PG_REPORT,
"Optimizer statistics are not transferred by pg_upgrade.\n"
"Once you start the new server, consider running:\n"
" %s/vacuumdb %s--all --analyze-in-stages", new_cluster.bindir, user_specification.data);
```

Best regards,
Hayato Kuroda
FUJITSU LIMITED

#313Andres Freund
andres@anarazel.de
In reply to: Jeff Davis (#309)
Re: Statistics Import and Export

Hi,

On 2025-02-20 01:39:34 -0800, Jeff Davis wrote:

Committed with some revisions on top of v48:

This made the pg_upgrade tests considerably slower.

In an assert build without optimization (since that's what I use for normal
dev work):

1fd1bd87101^ 65.03s
1fd1bd87101 86.84s

Looking at the times in the in the regress_log, I see:

good: [15:17:31.278](36.851s) ok 5 - regression tests pass
bad: [15:15:37.857](37.437s) ok 5 - regression tests pass

good: [15:17:36.721](5.436s) ok 6 - dump before running pg_upgrade
bad: [15:15:50.845](12.980s) ok 6 - dump before running pg_upgrade

good: [15:17:39.759](2.441s) ok 12 - run of pg_upgrade --check for new instance
bad: [15:15:53.861](2.415s) ok 12 - run of pg_upgrade --check for new instance

good: [15:17:51.249](11.489s) ok 14 - run of pg_upgrade for new instance
bad: [15:16:13.304](19.443s) ok 14 - run of pg_upgrade for new instance

good: [15:17:55.382](3.958s) ok 17 - dump after running pg_upgrade
bad: [15:16:23.766](10.290s) ok 17 - dump after running pg_upgrade

Which to me rather strongly suggests pg_dump has gotten a *lot* slower with
this change.

Greetings,

Andres

#314Tom Lane
tgl@sss.pgh.pa.us
In reply to: Andres Freund (#313)
Re: Statistics Import and Export

Andres Freund <andres@anarazel.de> writes:

Which to me rather strongly suggests pg_dump has gotten a *lot* slower with
this change.

Well, it's doing strictly more work, so somewhat slower is to be
expected. But yeah, more than 2x slower is not nice.

In a quick look at the committed patch, it doesn't seem to have
used any of the speedup strategies we applied to pg_dump a couple
of years ago. One or the other of these should help:

* Issue a single query to fetch stats from every table we're dumping

* Set up a prepared query to avoid re-planning the per-table query
(compare be85727a3)

I'm not sure how workable the first of these would be though.
It's not hard to imagine it blowing out pg_dump's memory usage
for a DB with a lot of tables and high default_statistics_target.
The second one should be relatively downside-free.

regards, tom lane

#315Andres Freund
andres@anarazel.de
In reply to: Andres Freund (#313)
Re: Statistics Import and Export

Hi,

On 2025-02-21 15:23:00 -0500, Andres Freund wrote:

On 2025-02-20 01:39:34 -0800, Jeff Davis wrote:

Committed with some revisions on top of v48:

This made the pg_upgrade tests considerably slower.

In an assert build without optimization (since that's what I use for normal
dev work):

1fd1bd87101^ 65.03s
1fd1bd87101 86.84s

Looking at the times in the in the regress_log, I see:
[...]
Which to me rather strongly suggests pg_dump has gotten a *lot* slower with
this change.

Indeed. While the slowdown is worse with assertions and without compiler
optimizations, it's pretty bad otherwise too.

optimized, non-cassert, pg_dump and server with the regression database contents:

$ time ./src/bin/pg_dump/pg_dump regression > /dev/null

real 0m1.314s
user 0m0.189s
sys 0m0.059s

$ time ./src/bin/pg_dump/pg_dump --no-statistics regression > /dev/null

real 0m0.472s
user 0m0.179s
sys 0m0.035s

Unoptimized, cassert server and pg_dump:

$ time ./src/bin/pg_dump/pg_dump regression > /dev/null

real 0m9.008s
user 0m0.396s
sys 0m0.108s

$ time ./src/bin/pg_dump/pg_dump --no-statistics regression > /dev/null

real 0m2.590s
user 0m0.347s
sys 0m0.037s

Looking at the query log, the biggest culprit is a *lot* of additional
queries, I think primarily these two:

SELECT c.oid::regclass AS relation, current_setting('server_version_num') AS version, c.relpages, c.reltuples, c.relallvisible FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = 'public' AND c.relname = 'alpha_neg_p2'

SELECT c.oid::regclass AS relation, s.attname,s.inherited,current_setting('server_version_num') AS version, s.null_frac,s.avg_width,s.n_distinct,s.most_common_vals,s.most_common_freqs,s.histogram_bounds,s.correlation,s.most_common_elems,s.most_common_elem_freqs,s.elem_count_histogram,s.range_length_histogram,s.range_empty_frac,s.range_bounds_histogram FROM pg_stats s JOIN pg_namespace n ON n.nspname = s.schemaname JOIN pg_class c ON c.relname = s.tablename AND c.relnamespace = n.oid WHERE s.schemaname = 'public' AND s.tablename = 'alpha_neg_p2' ORDER BY s.attname, s.inherited

I think there are a few things wrong here:

1) Why do we need to plan this over and over? Tom a while ago put in a fair
bit of work to make frequent queries use prepared statements.

In this case we spend more time replanning the query than executing it.

2) Querying this one-by-one makes this much more expensive than if it were
queried in a batched fashion, for multiple tables at once. This is
especially true if actually executed over network, rather than locally.

3) The query is unnecessarily expensive due to repeated joins gathering the
same information. pg_stats has a join to pg_namespace and pg_class, but
then the query above joins to both *again*.

And afaict the joins in the pg_stats query are pretty useless? Isn't all
that information already available in pg_stats? I guess you did that to get
it as a ::regclass, but isn't that already known, why requery it?

4) Why do we need to fetch the version twice for every table, that can't be
right? It won't change while pg_dump is running.

Greetings,

Andres Freund

#316Andres Freund
andres@anarazel.de
In reply to: Tom Lane (#314)
Re: Statistics Import and Export

Hi,

On 2025-02-21 15:49:10 -0500, Tom Lane wrote:

Andres Freund <andres@anarazel.de> writes:

Which to me rather strongly suggests pg_dump has gotten a *lot* slower with
this change.

Well, it's doing strictly more work, so somewhat slower is to be
expected.

Yea, if we had talked a few percent, I'd not have balked. It's more like 2-4x
though and it'll probably be worse when not connecting over local TCP
connections.

This is a slowdown to the point that the downtime for pg_upgrade will be
substantially lengthened compared to before. But I think we should be able to
address that to a large degree.

In a quick look at the committed patch, it doesn't seem to have
used any of the speedup strategies we applied to pg_dump a couple
of years ago. One or the other of these should help:

* Issue a single query to fetch stats from every table we're dumping
* Set up a prepared query to avoid re-planning the per-table query
(compare be85727a3)

I'm not sure how workable the first of these would be though.
It's not hard to imagine it blowing out pg_dump's memory usage
for a DB with a lot of tables and high default_statistics_target.

We could presumably do the one-query approach for the relation stats, that's
just three integers. That way we'd at least not end up with two queries for
each table (for pg_class.reltuples etc and for pg_stats).

I guess the memory usage could also be addressed by using COPY, but that's
probably unrealistically complicated.

The second one should be relatively downside-free.

Yea. And at least with pg_dump running locally that's where a lot of the CPU
time is spent.

Remotely doing lots of one-by-one queries will hurt even with prepared
statements though.

One way to largely address that would be to use a prepared statement combined
with libpq pipelining. That still has separate executor startup etc, but I
think it should still reduce the cost to a point where we don't care anymore.

Greetings,

Andres Freund

#317Tom Lane
tgl@sss.pgh.pa.us
In reply to: Andres Freund (#315)
Re: Statistics Import and Export

Andres Freund <andres@anarazel.de> writes:

Looking at the query log, the biggest culprit is a *lot* of additional
queries, I think primarily these two:

SELECT c.oid::regclass AS relation, current_setting('server_version_num') AS version, c.relpages, c.reltuples, c.relallvisible FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = 'public' AND c.relname = 'alpha_neg_p2'

SELECT c.oid::regclass AS relation, s.attname,s.inherited,current_setting('server_version_num') AS version, s.null_frac,s.avg_width,s.n_distinct,s.most_common_vals,s.most_common_freqs,s.histogram_bounds,s.correlation,s.most_common_elems,s.most_common_elem_freqs,s.elem_count_histogram,s.range_length_histogram,s.range_empty_frac,s.range_bounds_histogram FROM pg_stats s JOIN pg_namespace n ON n.nspname = s.schemaname JOIN pg_class c ON c.relname = s.tablename AND c.relnamespace = n.oid WHERE s.schemaname = 'public' AND s.tablename = 'alpha_neg_p2' ORDER BY s.attname, s.inherited

Oy. Those are outright horrid, even without any consideration of
pre-preparing them. We know the OID of the table we want to dump,
we should be doing "FROM pg_class WHERE oid = whatever" and lose
the join to pg_namespace altogether. The explicit casts to regclass
are quite expensive too to fetch information that pg_dump already
has. It already knows the server version, too.

Moreover, the first of these shouldn't be a separate query at all.
I objected to fetching pg_statistic content for all tables at once,
but relpages/reltuples/relallvisible is a pretty small amount of
new info. We should just collect those fields as part of getTables'
main query of pg_class (which, indeed, is already fetching relpages).

On the second one, if we want to go through the pg_stats view then
we can't rely on table OID, but I don't see why we need the joins
to anything else. "WHERE s.schemaname = 'x' AND s.tablename = 'y'"
seems sufficient.

I wonder whether we ought to issue different queries depending on
whether we're superuser. The pg_stats view is rather expensive
because of its security restrictions, and if we're superuser we
could just look directly at pg_statistic. Maybe those checks are
fast enough not to matter, but ...

regards, tom lane

#318Corey Huinker
corey.huinker@gmail.com
In reply to: Tom Lane (#317)
Re: Statistics Import and Export

Oy. Those are outright horrid, even without any consideration of
pre-preparing them. We know the OID of the table we want to dump,
we should be doing "FROM pg_class WHERE oid = whatever" and lose
the join to pg_namespace altogether. The explicit casts to regclass
are quite expensive too to fetch information that pg_dump already
has. It already knows the server version, too.

+1
Earlier versions had prepared statements, but those were removed to keep
things simple. Easy enough to revive.

Moreover, the first of these shouldn't be a separate query at all.
I objected to fetching pg_statistic content for all tables at once,
but relpages/reltuples/relallvisible is a pretty small amount of
new info. We should just collect those fields as part of getTables'
main query of pg_class (which, indeed, is already fetching relpages).

+1

On the second one, if we want to go through the pg_stats view then
we can't rely on table OID, but I don't see why we need the joins
to anything else. "WHERE s.schemaname = 'x' AND s.tablename = 'y'"
seems sufficient.

+1

I wonder whether we ought to issue different queries depending on
whether we're superuser. The pg_stats view is rather expensive
because of its security restrictions, and if we're superuser we
could just look directly at pg_statistic. Maybe those checks are
fast enough not to matter, but ...

That could lead to a rather complicated query that has to replicate the
guts of pg_stats for every server-specific version of pg_stats,
specifically the CASE statements that transform
the stakindN/stanumbersN/stavaluesN to mcv, correlation, etc, so I'd like
to avoid that if possible.

#319Tom Lane
tgl@sss.pgh.pa.us
In reply to: Corey Huinker (#318)
Re: Statistics Import and Export

Corey Huinker <corey.huinker@gmail.com> writes:

I wonder whether we ought to issue different queries depending on
whether we're superuser. The pg_stats view is rather expensive
because of its security restrictions, and if we're superuser we
could just look directly at pg_statistic. Maybe those checks are
fast enough not to matter, but ...

That could lead to a rather complicated query that has to replicate the
guts of pg_stats for every server-specific version of pg_stats,
specifically the CASE statements that transform
the stakindN/stanumbersN/stavaluesN to mcv, correlation, etc, so I'd like
to avoid that if possible.

Yeah, it'd be notationally ugly for sure. Let's keep that idea in the
back pocket and see how far we get with the other ideas.

regards, tom lane

#320Andres Freund
andres@anarazel.de
In reply to: Tom Lane (#317)
Re: Statistics Import and Export

Hi,

On 2025-02-21 16:24:38 -0500, Tom Lane wrote:

Oy. Those are outright horrid, even without any consideration of
pre-preparing them. We know the OID of the table we want to dump,
we should be doing "FROM pg_class WHERE oid = whatever" and lose
the join to pg_namespace altogether. The explicit casts to regclass
are quite expensive too to fetch information that pg_dump already
has. It already knows the server version, too.

Moreover, the first of these shouldn't be a separate query at all.
I objected to fetching pg_statistic content for all tables at once,
but relpages/reltuples/relallvisible is a pretty small amount of
new info. We should just collect those fields as part of getTables'
main query of pg_class (which, indeed, is already fetching relpages).

On the second one, if we want to go through the pg_stats view then
we can't rely on table OID, but I don't see why we need the joins
to anything else. "WHERE s.schemaname = 'x' AND s.tablename = 'y'"
seems sufficient.

Agreed on all those.

I wonder whether we ought to issue different queries depending on
whether we're superuser. The pg_stats view is rather expensive
because of its security restrictions, and if we're superuser we
could just look directly at pg_statistic. Maybe those checks are
fast enough not to matter, but ...

It doesn't seem to make much of a difference, from what I can tell.

At execution time most of the time is is in
a) the joins to pg_attribute and pg_class (the ones in pg_stats)
b) array_out().

The times get way worse if you dump stats for catalog tables, because there
some of arrays are regproc and regprocout calls FuncnameGetCandidates(), which
then ends up iterating over a long cached list... I think that's basically
O(N^2)?

Of course that's nothing we should encounter frequently, but ugh.

Greetings,

Andres Freund

#321Tom Lane
tgl@sss.pgh.pa.us
In reply to: Andres Freund (#320)
Re: Statistics Import and Export

Andres Freund <andres@anarazel.de> writes:

The times get way worse if you dump stats for catalog tables, because there
some of arrays are regproc and regprocout calls FuncnameGetCandidates(), which
then ends up iterating over a long cached list... I think that's basically
O(N^2)?

Can't be that bad. I don't see any proname values that occur more
than 2 dozen times. You can call that a long list if you want,
but it's not scaling with the size of pg_proc.

Of course that's nothing we should encounter frequently, but ugh.

Yeah, I can't get excited about the cost of that for normal user
dumps. The 002_pg_dump test does run a dump with --schema pg_catalog,
but it's dubious that that test is worth its cycles.

regards, tom lane

#322Corey Huinker
corey.huinker@gmail.com
In reply to: Tom Lane (#321)
1 attachment(s)
Re: Statistics Import and Export

Yeah, I can't get excited about the cost of that for normal user
dumps. The 002_pg_dump test does run a dump with --schema pg_catalog,
but it's dubious that that test is worth its cycles.

Attached is the first optimization, which gets rid of the pg_class queries
entirely, instead getting the same information from the existing queries in
getTables and getIndexes.

Additionally, the string representation of the server version number is now
stored in the Archive struct. Yes, we already have remoteVersion, but
that's in integer form, and remoteVersionStr is "18devel" rather than
"180000".

I didn't include any work on the attribute query as I wanted to keep that
separate for clarity purposes.

Attachments:

v1-0001-Leverage-existing-functions-for-relation-stats.patchtext/x-patch; charset=US-ASCII; name=v1-0001-Leverage-existing-functions-for-relation-stats.patchDownload
From 45467a69813cbf25c2850b254c5d2710c231a723 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Fri, 21 Feb 2025 23:31:04 -0500
Subject: [PATCH v1] Leverage existing functions for relation stats.

Rather than query pg_class once per relation in order to fetch relation
stats (reltuples, relpages, relallvisible), instead just add those
fields to the existing queries in getTables and getIndexes, and then
store their string representations in RelStatsInfo.

Additionally, we will need the string representation of the server
version number so render that one and store in the Archive struct.

No modification has been made for the attribute query, that will be
addressed in a later patch.
---
 src/bin/pg_dump/pg_backup.h    |   1 +
 src/bin/pg_dump/pg_backup_db.c |   1 +
 src/bin/pg_dump/pg_dump.c      | 113 +++++++++++++--------------------
 src/bin/pg_dump/pg_dump.h      |   9 +++
 4 files changed, 54 insertions(+), 70 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 350cf659c41..bbca3419b45 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -224,6 +224,7 @@ typedef struct Archive
 	int			verbose;
 	char	   *remoteVersionStr;	/* server's version string */
 	int			remoteVersion;	/* same in numeric form */
+	char		remoteVersionNumStr[32]; /* server version number, as string */
 	bool		isStandby;		/* is server a standby node */
 
 	int			minRemoteVersion;	/* allowable range */
diff --git a/src/bin/pg_dump/pg_backup_db.c b/src/bin/pg_dump/pg_backup_db.c
index 71c55d2466a..59c7b70d90f 100644
--- a/src/bin/pg_dump/pg_backup_db.c
+++ b/src/bin/pg_dump/pg_backup_db.c
@@ -41,6 +41,7 @@ _check_database_version(ArchiveHandle *AH)
 
 	AH->public.remoteVersionStr = pg_strdup(remoteversion_str);
 	AH->public.remoteVersion = remoteversion;
+	sprintf(AH->public.remoteVersionNumStr, "%d", remoteversion);
 	if (!AH->archiveRemoteVersion)
 		AH->archiveRemoteVersion = AH->public.remoteVersionStr;
 
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index afd79287177..4311ed5c65d 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -6814,7 +6814,8 @@ getFuncs(Archive *fout)
  *
  */
 static RelStatsInfo *
-getRelationStatistics(Archive *fout, DumpableObject *rel, char relkind)
+getRelationStatistics(Archive *fout, DumpableObject *rel, const char *relpages,
+					  const char *reltuples, const char *relallvisible, char relkind)
 {
 	if (!fout->dopt->dumpStatistics)
 		return NULL;
@@ -6839,6 +6840,9 @@ getRelationStatistics(Archive *fout, DumpableObject *rel, char relkind)
 		dobj->components |= DUMP_COMPONENT_STATISTICS;
 		dobj->name = pg_strdup(rel->name);
 		dobj->namespace = rel->namespace;
+		info->relpages = relpages;
+		info->reltuples = reltuples;
+		info->relallvisible = relallvisible;
 		info->relkind = relkind;
 		info->postponed_def = false;
 
@@ -6874,6 +6878,8 @@ getTables(Archive *fout, int *numTables)
 	int			i_relhasindex;
 	int			i_relhasrules;
 	int			i_relpages;
+	int			i_reltuples;
+	int			i_relallvisible;
 	int			i_toastpages;
 	int			i_owning_tab;
 	int			i_owning_col;
@@ -6921,6 +6927,7 @@ getTables(Archive *fout, int *numTables)
 	appendPQExpBufferStr(query,
 						 "SELECT c.tableoid, c.oid, c.relname, "
 						 "c.relnamespace, c.relkind, c.reltype, "
+						 "c.relpages, c.reltuples, c.relallvisible, "
 						 "c.relowner, "
 						 "c.relchecks, "
 						 "c.relhasindex, c.relhasrules, c.relpages, "
@@ -7088,6 +7095,8 @@ getTables(Archive *fout, int *numTables)
 	i_relhasindex = PQfnumber(res, "relhasindex");
 	i_relhasrules = PQfnumber(res, "relhasrules");
 	i_relpages = PQfnumber(res, "relpages");
+	i_reltuples = PQfnumber(res, "reltuples");
+	i_relallvisible = PQfnumber(res, "relallvisible");
 	i_toastpages = PQfnumber(res, "toastpages");
 	i_owning_tab = PQfnumber(res, "owning_tab");
 	i_owning_col = PQfnumber(res, "owning_col");
@@ -7151,7 +7160,10 @@ getTables(Archive *fout, int *numTables)
 		tblinfo[i].ncheck = atoi(PQgetvalue(res, i, i_relchecks));
 		tblinfo[i].hasindex = (strcmp(PQgetvalue(res, i, i_relhasindex), "t") == 0);
 		tblinfo[i].hasrules = (strcmp(PQgetvalue(res, i, i_relhasrules), "t") == 0);
-		tblinfo[i].relpages = atoi(PQgetvalue(res, i, i_relpages));
+		tblinfo[i].relpages_s = pg_strdup(PQgetvalue(res, i, i_relpages));
+		tblinfo[i].relpages = atoi(tblinfo[i].relpages_s);
+		tblinfo[i].reltuples_s = pg_strdup(PQgetvalue(res, i, i_reltuples));
+		tblinfo[i].relallvisible_s = pg_strdup(PQgetvalue(res, i, i_relallvisible));
 		if (PQgetisnull(res, i, i_toastpages))
 			tblinfo[i].toastpages = 0;
 		else
@@ -7233,7 +7245,9 @@ getTables(Archive *fout, int *numTables)
 
 		/* Add statistics */
 		if (tblinfo[i].interesting)
-			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relkind);
+			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relpages_s,
+								  tblinfo[i].reltuples_s, tblinfo[i].relallvisible_s,
+								  tblinfo[i].relkind);
 
 		/*
 		 * Read-lock target tables to make sure they aren't DROPPED or altered
@@ -7499,6 +7513,9 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				i_oid,
 				i_indrelid,
 				i_indexname,
+				i_relpages,
+				i_reltuples,
+				i_relallvisible,
 				i_parentidx,
 				i_indexdef,
 				i_indnkeyatts,
@@ -7552,6 +7569,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 	appendPQExpBufferStr(query,
 						 "SELECT t.tableoid, t.oid, i.indrelid, "
 						 "t.relname AS indexname, "
+						 "t.relpages, t.reltuples, t.relallvisible, "
 						 "pg_catalog.pg_get_indexdef(i.indexrelid) AS indexdef, "
 						 "i.indkey, i.indisclustered, "
 						 "c.contype, c.conname, "
@@ -7659,6 +7677,9 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_oid = PQfnumber(res, "oid");
 	i_indrelid = PQfnumber(res, "indrelid");
 	i_indexname = PQfnumber(res, "indexname");
+	i_relpages = PQfnumber(res, "relpages");
+	i_reltuples = PQfnumber(res, "reltuples");
+	i_relallvisible = PQfnumber(res, "relallvisible");
 	i_parentidx = PQfnumber(res, "parentidx");
 	i_indexdef = PQfnumber(res, "indexdef");
 	i_indnkeyatts = PQfnumber(res, "indnkeyatts");
@@ -7732,6 +7753,9 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			AssignDumpId(&indxinfo[j].dobj);
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
+			indxinfo[j].relpages_s = pg_strdup(PQgetvalue(res, j, i_relpages));
+			indxinfo[j].reltuples_s = pg_strdup(PQgetvalue(res, j, i_reltuples));
+			indxinfo[j].relallvisible_s = pg_strdup(PQgetvalue(res, j, i_relallvisible));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
 			indxinfo[j].indextable = tbinfo;
 			indxinfo[j].indexdef = pg_strdup(PQgetvalue(res, j, i_indexdef));
@@ -7759,7 +7783,9 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				indexkind = RELKIND_PARTITIONED_INDEX;
 
 			contype = *(PQgetvalue(res, j, i_contype));
-			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, indexkind);
+			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, indxinfo[j].relpages_s,
+											 indxinfo[j].reltuples_s, indxinfo[j].relallvisible_s,
+									indexkind);
 
 			if (contype == 'p' || contype == 'u' || contype == 'x')
 			{
@@ -10383,18 +10409,6 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
-/*
- * Tabular description of the parameters to pg_restore_relation_stats()
- * param_name, param_type
- */
-static const char *rel_stats_arginfo[][2] = {
-	{"relation", "regclass"},
-	{"version", "integer"},
-	{"relpages", "integer"},
-	{"reltuples", "real"},
-	{"relallvisible", "integer"},
-};
-
 /*
  * Tabular description of the parameters to pg_restore_attribute_stats()
  * param_name, param_type
@@ -10419,30 +10433,6 @@ static const char *att_stats_arginfo[][2] = {
 	{"range_bounds_histogram", "text"},
 };
 
-/*
- * getRelStatsExportQuery --
- *
- * Generate a query that will fetch all relation (e.g. pg_class)
- * stats for a given relation.
- */
-static void
-getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
-					   const char *schemaname, const char *relname)
-{
-	resetPQExpBuffer(query);
-	appendPQExpBufferStr(query,
-						 "SELECT c.oid::regclass AS relation, "
-						 "current_setting('server_version_num') AS version, "
-						 "c.relpages, c.reltuples, c.relallvisible "
-						 "FROM pg_class c "
-						 "JOIN pg_namespace n "
-						 "ON n.oid = c.relnamespace "
-						 "WHERE n.nspname = ");
-	appendStringLiteralAH(query, schemaname, fout);
-	appendPQExpBufferStr(query, " AND c.relname = ");
-	appendStringLiteralAH(query, relname, fout);
-}
-
 /*
  * getAttStatsExportQuery --
  *
@@ -10521,33 +10511,20 @@ appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
  * Append a formatted pg_restore_relation_stats statement.
  */
 static void
-appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+appendRelStatsImport(PQExpBuffer out, Archive *fout, const RelStatsInfo *rsinfo)
 {
-	const char *sep = "";
-
-	if (PQntuples(res) == 0)
-		return;
+	const char *qualname = fmtQualifiedId(rsinfo->dobj.namespace->dobj.name, rsinfo->dobj.name);
 
 	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
-
-	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
-	{
-		const char *argname = rel_stats_arginfo[argno][0];
-		const char *argtype = rel_stats_arginfo[argno][1];
-		int			fieldno = PQfnumber(res, argname);
-
-		if (fieldno < 0)
-			pg_fatal("relation stats export query missing field '%s'",
-					 argname);
-
-		if (PQgetisnull(res, 0, fieldno))
-			continue;
-
-		appendPQExpBufferStr(out, sep);
-		appendNamedArgument(out, fout, argname, PQgetvalue(res, 0, fieldno), argtype);
-
-		sep = ",\n";
-	}
+	appendNamedArgument(out, fout, "relation", qualname, "regclass");
+	appendPQExpBufferStr(out, ",\n");
+	appendNamedArgument(out, fout, "version", fout->remoteVersionNumStr, "integer");
+	appendPQExpBufferStr(out, ",\n");
+	appendNamedArgument(out, fout, "relpages", rsinfo->relpages, "integer");
+	appendPQExpBufferStr(out, ",\n");
+	appendNamedArgument(out, fout, "reltuples", rsinfo->reltuples, "real");
+	appendPQExpBufferStr(out, ",\n");
+	appendNamedArgument(out, fout, "relallvisible", rsinfo->relallvisible, "integer");
 	appendPQExpBufferStr(out, "\n);\n");
 }
 
@@ -10643,15 +10620,11 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 	tag = createPQExpBuffer();
 	appendPQExpBufferStr(tag, fmtId(dobj->name));
 
-	query = createPQExpBuffer();
 	out = createPQExpBuffer();
 
-	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
-						   dobj->name);
-	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
-	appendRelStatsImport(out, fout, res);
-	PQclear(res);
+	appendRelStatsImport(out, fout, rsinfo);
 
+	query = createPQExpBuffer();
 	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
 						   dobj->name);
 	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index f08f5905aa3..1fd1812d348 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -329,6 +329,9 @@ typedef struct _tableInfo
 	int			owning_col;		/* attr # of column owning sequence */
 	bool		is_identity_sequence;
 	int			relpages;		/* table's size in pages (from pg_class) */
+	const char *relpages_s;		/* table's size in pages (from pg_class) */
+	const char *reltuples_s;	/* table's approx number of tuples (from pg_class) */
+	const char *relallvisible_s;	/* table's number of pages all-visible (from pg_class) */
 	int			toastpages;		/* toast table's size in pages, if any */
 
 	bool		interesting;	/* true if need to collect more data */
@@ -418,6 +421,9 @@ typedef struct _indxInfo
 	int			indnattrs;		/* total number of index attributes */
 	Oid		   *indkeys;		/* In spite of the name 'indkeys' this field
 								 * contains both key and nonkey attributes */
+	const char *relpages_s;
+	const char *reltuples_s;
+	const char *relallvisible_s;
 	bool		indisclustered;
 	bool		indisreplident;
 	bool		indnullsnotdistinct;
@@ -438,6 +444,9 @@ typedef struct _indexAttachInfo
 typedef struct _relStatsInfo
 {
 	DumpableObject dobj;
+	const char *relpages;
+	const char *reltuples;
+	const char *relallvisible;
 	char		relkind;		/* 'r', 'm', 'i', etc */
 	bool		postponed_def;	/* stats must be postponed into post-data */
 } RelStatsInfo;

base-commit: f8d7f29b3e81db59b95e4b5baaa6943178c89fd8
-- 
2.48.1

#323Jeff Davis
pgsql@j-davis.com
In reply to: Hayato Kuroda (Fujitsu) (#312)
Re: Statistics Import and Export

On Fri, 2025-02-21 at 07:24 +0000, Hayato Kuroda (Fujitsu) wrote:

I hope I'm in the correct thread. I found the commit 1fd1bd8 - it is
so cool.

Yes, documentation corrections are appreciated, thank you.

But pgupgrade.sgml [2] and source code [3] said that statistics must
be updated.

Changed.

Regards,
Jeff Davis

#324Tom Lane
tgl@sss.pgh.pa.us
In reply to: Corey Huinker (#322)
Re: Statistics Import and Export

BTW, while nosing around looking for an explanation for our
cross-version-upgrade woes, I chanced to notice that
relation_statistics_update rejects negative values for
relpages and relallvisible. This is nonsense. Internally
those values are BlockNumbers and can have any value from
0 to UINT32_MAX. We represent them as signed int32 at the
SQL level, which means they can read out as any int32 value.
So the range checks that are being applied to them are flat
wrong and should be removed. Admittedly, you'd need a table
exceeding 16TB (if I did the math right) to see a problem,
but that doesn't make it not wrong.

It might be a good idea to change the code so that it declares
these values internally as BlockNumber and uses PG_GETARG_UINT32,
but I think that would only be a cosmetic change not a
correctness issue.

regards, tom lane

#325Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#322)
1 attachment(s)
Re: Statistics Import and Export

On Sat, 2025-02-22 at 00:00 -0500, Corey Huinker wrote:

Attached is the first optimization, which gets rid of the pg_class
queries entirely, instead getting the same information from the
existing queries in getTables and getIndexes.

Attached a revised version. The main changes are that the only struct
it changes is RelStatsInfo, and it doesn't carry around string values.

IIUC, your version carried around the string values so that there would
be no conversion; it would hold the string from one result to the next.
That makes sense, but it seemed to change a lot of struct fields, and
have unnecessary string copying and memory usage which was not freed.
Instead, I used float_to_shortest_decimal_buf(), which is what
float4out() uses, which should be a consistent way to convert the float
value.

That meant that we couldn't use appendNamedArgument() as easily, but it
wasn't helping much in that function anyway, because it was no longer a
loop.

I didn't measure any performance difference between your version and
mine, but avoiding a few allocations couldn't hurt. It seems to save
just under 20% on an unoptimized build.

Regards,
Jeff Davis

Attachments:

v2j-0001-Avoid-unnecessary-relation-stats-query-in-pg_dum.patchtext/x-patch; charset=UTF-8; name=v2j-0001-Avoid-unnecessary-relation-stats-query-in-pg_dum.patchDownload
From 2f16b7cf941fc14e3156e8ddc536c8f93f97eb77 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Fri, 21 Feb 2025 23:31:04 -0500
Subject: [PATCH v2j] Avoid unnecessary relation stats query in pg_dump.

The few fields we need can be easily collected in getTables() and
getIndexes() and stored in RelStatsInfo.

Co-authored-by: Corey Huinker <corey.huinker@gmail.com>
Co-authored-by: Jeff Davis <pgsql@j-davis.com>
Discussion: https://postgr.es/m/CADkLM=f0a43aTd88xW4xCFayEF25g-7hTrHX_WhV40HyocsUGg@mail.gmail.com
---
 src/bin/pg_dump/pg_dump.c | 142 +++++++++++++++-----------------------
 src/bin/pg_dump/pg_dump.h |   5 +-
 2 files changed, 61 insertions(+), 86 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index afd79287177..d119ce716b0 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -56,6 +56,7 @@
 #include "common/connect.h"
 #include "common/int.h"
 #include "common/relpath.h"
+#include "common/shortest_dec.h"
 #include "compress_io.h"
 #include "dumputils.h"
 #include "fe_utils/option_utils.h"
@@ -6814,7 +6815,8 @@ getFuncs(Archive *fout)
  *
  */
 static RelStatsInfo *
-getRelationStatistics(Archive *fout, DumpableObject *rel, char relkind)
+getRelationStatistics(Archive *fout, DumpableObject *rel, int32 relpages,
+					  float reltuples, int32 relallvisible, char relkind)
 {
 	if (!fout->dopt->dumpStatistics)
 		return NULL;
@@ -6839,6 +6841,9 @@ getRelationStatistics(Archive *fout, DumpableObject *rel, char relkind)
 		dobj->components |= DUMP_COMPONENT_STATISTICS;
 		dobj->name = pg_strdup(rel->name);
 		dobj->namespace = rel->namespace;
+		info->relpages = relpages;
+		info->reltuples = reltuples;
+		info->relallvisible = relallvisible;
 		info->relkind = relkind;
 		info->postponed_def = false;
 
@@ -6874,6 +6879,8 @@ getTables(Archive *fout, int *numTables)
 	int			i_relhasindex;
 	int			i_relhasrules;
 	int			i_relpages;
+	int			i_reltuples;
+	int			i_relallvisible;
 	int			i_toastpages;
 	int			i_owning_tab;
 	int			i_owning_col;
@@ -6924,7 +6931,7 @@ getTables(Archive *fout, int *numTables)
 						 "c.relowner, "
 						 "c.relchecks, "
 						 "c.relhasindex, c.relhasrules, c.relpages, "
-						 "c.relhastriggers, "
+						 "c.reltuples, c.relallvisible, c.relhastriggers, "
 						 "c.relpersistence, "
 						 "c.reloftype, "
 						 "c.relacl, "
@@ -7088,6 +7095,8 @@ getTables(Archive *fout, int *numTables)
 	i_relhasindex = PQfnumber(res, "relhasindex");
 	i_relhasrules = PQfnumber(res, "relhasrules");
 	i_relpages = PQfnumber(res, "relpages");
+	i_reltuples = PQfnumber(res, "reltuples");
+	i_relallvisible = PQfnumber(res, "relallvisible");
 	i_toastpages = PQfnumber(res, "toastpages");
 	i_owning_tab = PQfnumber(res, "owning_tab");
 	i_owning_col = PQfnumber(res, "owning_col");
@@ -7134,6 +7143,9 @@ getTables(Archive *fout, int *numTables)
 
 	for (i = 0; i < ntups; i++)
 	{
+		float		reltuples = atof(PQgetvalue(res, i, i_reltuples));
+		int32		relallvisible = atoi(PQgetvalue(res, i, i_relallvisible));
+
 		tblinfo[i].dobj.objType = DO_TABLE;
 		tblinfo[i].dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_reltableoid));
 		tblinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_reloid));
@@ -7233,7 +7245,8 @@ getTables(Archive *fout, int *numTables)
 
 		/* Add statistics */
 		if (tblinfo[i].interesting)
-			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relkind);
+			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relpages,
+								  reltuples, relallvisible, tblinfo[i].relkind);
 
 		/*
 		 * Read-lock target tables to make sure they aren't DROPPED or altered
@@ -7499,6 +7512,9 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				i_oid,
 				i_indrelid,
 				i_indexname,
+				i_relpages,
+				i_reltuples,
+				i_relallvisible,
 				i_parentidx,
 				i_indexdef,
 				i_indnkeyatts,
@@ -7552,6 +7568,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 	appendPQExpBufferStr(query,
 						 "SELECT t.tableoid, t.oid, i.indrelid, "
 						 "t.relname AS indexname, "
+						 "t.relpages, t.reltuples, t.relallvisible, "
 						 "pg_catalog.pg_get_indexdef(i.indexrelid) AS indexdef, "
 						 "i.indkey, i.indisclustered, "
 						 "c.contype, c.conname, "
@@ -7659,6 +7676,9 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_oid = PQfnumber(res, "oid");
 	i_indrelid = PQfnumber(res, "indrelid");
 	i_indexname = PQfnumber(res, "indexname");
+	i_relpages = PQfnumber(res, "relpages");
+	i_reltuples = PQfnumber(res, "reltuples");
+	i_relallvisible = PQfnumber(res, "relallvisible");
 	i_parentidx = PQfnumber(res, "parentidx");
 	i_indexdef = PQfnumber(res, "indexdef");
 	i_indnkeyatts = PQfnumber(res, "indnkeyatts");
@@ -7725,6 +7745,9 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			char		contype;
 			char		indexkind;
 			RelStatsInfo *relstats;
+			int32		relpages = atoi(PQgetvalue(res, j, i_relpages));
+			float		reltuples = atof(PQgetvalue(res, j, i_reltuples));
+			int32		relallvisible = atoi(PQgetvalue(res, j, i_relallvisible));
 
 			indxinfo[j].dobj.objType = DO_INDEX;
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
@@ -7759,7 +7782,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				indexkind = RELKIND_PARTITIONED_INDEX;
 
 			contype = *(PQgetvalue(res, j, i_contype));
-			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, indexkind);
+			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, relpages,
+											 reltuples, relallvisible, indexkind);
 
 			if (contype == 'p' || contype == 'u' || contype == 'x')
 			{
@@ -10383,18 +10407,6 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
-/*
- * Tabular description of the parameters to pg_restore_relation_stats()
- * param_name, param_type
- */
-static const char *rel_stats_arginfo[][2] = {
-	{"relation", "regclass"},
-	{"version", "integer"},
-	{"relpages", "integer"},
-	{"reltuples", "real"},
-	{"relallvisible", "integer"},
-};
-
 /*
  * Tabular description of the parameters to pg_restore_attribute_stats()
  * param_name, param_type
@@ -10419,30 +10431,6 @@ static const char *att_stats_arginfo[][2] = {
 	{"range_bounds_histogram", "text"},
 };
 
-/*
- * getRelStatsExportQuery --
- *
- * Generate a query that will fetch all relation (e.g. pg_class)
- * stats for a given relation.
- */
-static void
-getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
-					   const char *schemaname, const char *relname)
-{
-	resetPQExpBuffer(query);
-	appendPQExpBufferStr(query,
-						 "SELECT c.oid::regclass AS relation, "
-						 "current_setting('server_version_num') AS version, "
-						 "c.relpages, c.reltuples, c.relallvisible "
-						 "FROM pg_class c "
-						 "JOIN pg_namespace n "
-						 "ON n.oid = c.relnamespace "
-						 "WHERE n.nspname = ");
-	appendStringLiteralAH(query, schemaname, fout);
-	appendPQExpBufferStr(query, " AND c.relname = ");
-	appendStringLiteralAH(query, relname, fout);
-}
-
 /*
  * getAttStatsExportQuery --
  *
@@ -10454,21 +10442,22 @@ getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
 					   const char *schemaname, const char *relname)
 {
 	resetPQExpBuffer(query);
-	appendPQExpBufferStr(query,
-						 "SELECT c.oid::regclass AS relation, "
-						 "s.attname,"
-						 "s.inherited,"
-						 "current_setting('server_version_num') AS version, "
-						 "s.null_frac,"
-						 "s.avg_width,"
-						 "s.n_distinct,"
-						 "s.most_common_vals,"
-						 "s.most_common_freqs,"
-						 "s.histogram_bounds,"
-						 "s.correlation,"
-						 "s.most_common_elems,"
-						 "s.most_common_elem_freqs,"
-						 "s.elem_count_histogram,");
+	appendPQExpBuffer(query,
+					  "SELECT c.oid::regclass AS relation, "
+					  "s.attname,"
+					  "s.inherited,"
+					  "'%u'::integer AS version, "
+					  "s.null_frac,"
+					  "s.avg_width,"
+					  "s.n_distinct,"
+					  "s.most_common_vals,"
+					  "s.most_common_freqs,"
+					  "s.histogram_bounds,"
+					  "s.correlation,"
+					  "s.most_common_elems,"
+					  "s.most_common_elem_freqs,"
+					  "s.elem_count_histogram,",
+					  fout->remoteVersion);
 
 	if (fout->remoteVersion >= 170000)
 		appendPQExpBufferStr(query,
@@ -10521,34 +10510,21 @@ appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
  * Append a formatted pg_restore_relation_stats statement.
  */
 static void
-appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+appendRelStatsImport(PQExpBuffer out, Archive *fout, const RelStatsInfo *rsinfo)
 {
-	const char *sep = "";
+	const char *qualname = fmtQualifiedId(rsinfo->dobj.namespace->dobj.name, rsinfo->dobj.name);
+	char		reltuples_str[FLOAT_SHORTEST_DECIMAL_LEN];
 
-	if (PQntuples(res) == 0)
-		return;
+	float_to_shortest_decimal_buf(rsinfo->reltuples, reltuples_str);
 
 	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
-
-	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
-	{
-		const char *argname = rel_stats_arginfo[argno][0];
-		const char *argtype = rel_stats_arginfo[argno][1];
-		int			fieldno = PQfnumber(res, argname);
-
-		if (fieldno < 0)
-			pg_fatal("relation stats export query missing field '%s'",
-					 argname);
-
-		if (PQgetisnull(res, 0, fieldno))
-			continue;
-
-		appendPQExpBufferStr(out, sep);
-		appendNamedArgument(out, fout, argname, PQgetvalue(res, 0, fieldno), argtype);
-
-		sep = ",\n";
-	}
-	appendPQExpBufferStr(out, "\n);\n");
+	appendPQExpBuffer(out, "\t'relation', '%s'::regclass,\n", qualname);
+	appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
+					  fout->remoteVersion);
+	appendPQExpBuffer(out, "\t'relpages', '%d'::integer,\n", rsinfo->relpages);
+	appendPQExpBuffer(out, "\t'reltuples', '%s'::real,\n", reltuples_str);
+	appendPQExpBuffer(out, "\t'relallvisible', '%d'::integer\n);\n",
+					  rsinfo->relallvisible);
 }
 
 /*
@@ -10643,15 +10619,11 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 	tag = createPQExpBuffer();
 	appendPQExpBufferStr(tag, fmtId(dobj->name));
 
-	query = createPQExpBuffer();
 	out = createPQExpBuffer();
 
-	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
-						   dobj->name);
-	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
-	appendRelStatsImport(out, fout, res);
-	PQclear(res);
+	appendRelStatsImport(out, fout, rsinfo);
 
+	query = createPQExpBuffer();
 	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
 						   dobj->name);
 	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index f08f5905aa3..9d6a4857c4b 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -328,7 +328,7 @@ typedef struct _tableInfo
 	Oid			owning_tab;		/* OID of table owning sequence */
 	int			owning_col;		/* attr # of column owning sequence */
 	bool		is_identity_sequence;
-	int			relpages;		/* table's size in pages (from pg_class) */
+	int32		relpages;		/* table's size in pages (from pg_class) */
 	int			toastpages;		/* toast table's size in pages, if any */
 
 	bool		interesting;	/* true if need to collect more data */
@@ -438,6 +438,9 @@ typedef struct _indexAttachInfo
 typedef struct _relStatsInfo
 {
 	DumpableObject dobj;
+	int32		relpages;
+	float		reltuples;
+	int32		relallvisible;
 	char		relkind;		/* 'r', 'm', 'i', etc */
 	bool		postponed_def;	/* stats must be postponed into post-data */
 } RelStatsInfo;
-- 
2.34.1

#326Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#325)
Re: Statistics Import and Export

On Sun, Feb 23, 2025 at 7:22 PM Jeff Davis <pgsql@j-davis.com> wrote:

On Sat, 2025-02-22 at 00:00 -0500, Corey Huinker wrote:

Attached is the first optimization, which gets rid of the pg_class
queries entirely, instead getting the same information from the
existing queries in getTables and getIndexes.

Attached a revised version. The main changes are that the only struct
it changes is RelStatsInfo, and it doesn't carry around string values.

IIUC, your version carried around the string values so that there would
be no conversion; it would hold the string from one result to the next.
That makes sense, but it seemed to change a lot of struct fields, and
have unnecessary string copying and memory usage which was not freed.
Instead, I used float_to_shortest_decimal_buf(), which is what
float4out() uses, which should be a consistent way to convert the float
value.

If we're fine with giving up on appendNamedArgument() for relstats,
wouldn't we also want to mash these into a single call?

appendPQExpBuffer(out, "\t'relation', '%s'::regclass,\n", qualname);
appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
fout->remoteVersion);
appendPQExpBuffer(out, "\t'relpages', '%d'::integer,\n", rsinfo->relpages);
appendPQExpBuffer(out, "\t'reltuples', '%s'::real,\n", reltuples_str);
appendPQExpBuffer(out, "\t'relallvisible', '%d'::integer\n);\n",
rsinfo->relallvisible);

to:
appendPQExpBuffer(out, "\t'relation', '%s'::regclass"
",\n\t'version', '%u'::integer"
",\n\t'relpages', '%d'::integer"
",\n\t'reltuples', '%s'::real"
",\n\t'relallvisible', '%d'::integer",
qualname, fout->remoteVersion, rsinfo->relpages,
rsinfo->reltuples_str, rsinfo->relallvisible);
appendPQExpBufferStr(out, "\n);\n");

Also, there's work elsewhere that may add relallfrozen to pg_class, which
would be something we'd want to add depending on the remoteVersion, and
this format will make that change pretty clear.

That meant that we couldn't use appendNamedArgument() as easily, but it
wasn't helping much in that function anyway, because it was no longer a
loop.

It still served to encapsulate the format of a kwarg pair, but little more,
agreed.

I didn't measure any performance difference between your version and
mine, but avoiding a few allocations couldn't hurt. It seems to save
just under 20% on an unoptimized build.

Part of me thinks we'd want to do the reverse - change the struct to store
char[32] to for each of relpages, reltuples, and relallvisible, and then
convert reltpages to int in the one place where we actually need to use in
its numeric form, and even then only in one place. Conversions to and from
other data types introduce the possibility, though very remote, of the
converted-and-then-unconverted value being cosmetically different from what
we got from the server, and if down the road we're dealing with more
complex data types, those conversions might become significant.

#327Tom Lane
tgl@sss.pgh.pa.us
In reply to: Corey Huinker (#326)
Re: Statistics Import and Export

Corey Huinker <corey.huinker@gmail.com> writes:

If we're fine with giving up on appendNamedArgument() for relstats,
wouldn't we also want to mash these into a single call?

appendPQExpBuffer(out, "\t'relation', '%s'::regclass,\n", qualname);
appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
fout->remoteVersion);
appendPQExpBuffer(out, "\t'relpages', '%d'::integer,\n", rsinfo->relpages);
appendPQExpBuffer(out, "\t'reltuples', '%s'::real,\n", reltuples_str);
appendPQExpBuffer(out, "\t'relallvisible', '%d'::integer\n);\n",
rsinfo->relallvisible);

to:
appendPQExpBuffer(out, "\t'relation', '%s'::regclass"
",\n\t'version', '%u'::integer"
",\n\t'relpages', '%d'::integer"
",\n\t'reltuples', '%s'::real"
",\n\t'relallvisible', '%d'::integer",
qualname, fout->remoteVersion, rsinfo->relpages,
rsinfo->reltuples_str, rsinfo->relallvisible);
appendPQExpBufferStr(out, "\n);\n");

That doesn't seem like an improvement. It's less readable ---
you have to match up %'s with arguments that are some distance
away --- and harder to modify. There might be some microscopic
performance benefit but it'd be pretty microscopic.

regards, tom lane

#328Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#326)
Re: Statistics Import and Export

On Sun, 2025-02-23 at 20:03 -0500, Corey Huinker wrote:

If we're fine with giving up on appendNamedArgument() for relstats,
wouldn't we also want to mash these into a single call?

...

appendPQExpBuffer(out, "\t'relation', '%s'::regclass"
                       ",\n\t'version', '%u'::integer"
                       ",\n\t'relpages', '%d'::integer"
                       ",\n\t'reltuples', '%s'::real"
                       ",\n\t'relallvisible', '%d'::integer",
                       qualname, fout->remoteVersion, rsinfo-

relpages,

                       rsinfo->reltuples_str, rsinfo->relallvisible);
appendPQExpBufferStr(out, "\n);\n");

+1.

Part of me thinks we'd want to do the reverse - change the struct to
store char[32] to for each of relpages, reltuples, and relallvisible,
and then convert reltpages to int in the one place where we actually
need to use in its numeric form, and even then only in one place.
Conversions to and from other data types introduce the possibility,
though very remote, of the converted-and-then-unconverted value being
cosmetically different from what we got from the server, and if down
the road we're dealing with more complex data types, those
conversions might become significant.

That's a good point but let's avoid excessive redundancy in the
structures. Adding a few fields to RelStatsInfo should be enough.

Regards,
Jeff Davis

#329Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#328)
3 attachment(s)
Re: Statistics Import and Export

That's a good point but let's avoid excessive redundancy in the
structures. Adding a few fields to RelStatsInfo should be enough.

Regards,
Jeff Davis

Incorporating most of the feedback (I kept a few of
the appendNamedArgument() calls) presented over the weekend.

* removeVersionNumStr is gone
* relpages/reltuples/relallvisible are now char[32] buffers in RelStatsInfo
and nowhere else (existing relpages conversion remains, however)
* attribute stats export query is now prepared, and queries pg_stats with
no joins
* version parameter moved to end of both queries for consistency.

Attachments:

v2-0001-Leverage-existing-functions-for-relation-stats.patchtext/x-patch; charset=US-ASCII; name=v2-0001-Leverage-existing-functions-for-relation-stats.patchDownload
From 95127f6fd82bde843e5840de93678ff784750c8a Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Mon, 24 Feb 2025 02:38:22 -0500
Subject: [PATCH v2 1/3] Leverage existing functions for relation stats.

Rather than quer pg_class once per relation in order to fetch relation
stats (reltuples, relpages, relallvisible), instead just add those
fields to the existing queries in getTables() and getIndexes(), then
then store their string representations in RelStatsInfo.
---
 src/bin/pg_dump/pg_dump.c | 117 ++++++++++++++++----------------------
 src/bin/pg_dump/pg_dump.h |   3 +
 2 files changed, 52 insertions(+), 68 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index afd79287177..a5e7aa73671 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -6814,7 +6814,8 @@ getFuncs(Archive *fout)
  *
  */
 static RelStatsInfo *
-getRelationStatistics(Archive *fout, DumpableObject *rel, char relkind)
+getRelationStatistics(Archive *fout, DumpableObject *rel, const char *relpages,
+					  const char *reltuples, const char *relallvisible, char relkind)
 {
 	if (!fout->dopt->dumpStatistics)
 		return NULL;
@@ -6839,6 +6840,9 @@ getRelationStatistics(Archive *fout, DumpableObject *rel, char relkind)
 		dobj->components |= DUMP_COMPONENT_STATISTICS;
 		dobj->name = pg_strdup(rel->name);
 		dobj->namespace = rel->namespace;
+		strncpy(info->relpages, relpages,sizeof(info->relpages));
+		strncpy(info->reltuples, reltuples, sizeof(info->reltuples));
+		strncpy(info->relallvisible, relallvisible, sizeof(info->relallvisible));
 		info->relkind = relkind;
 		info->postponed_def = false;
 
@@ -6874,6 +6878,8 @@ getTables(Archive *fout, int *numTables)
 	int			i_relhasindex;
 	int			i_relhasrules;
 	int			i_relpages;
+	int			i_reltuples;
+	int			i_relallvisible;
 	int			i_toastpages;
 	int			i_owning_tab;
 	int			i_owning_col;
@@ -6921,6 +6927,7 @@ getTables(Archive *fout, int *numTables)
 	appendPQExpBufferStr(query,
 						 "SELECT c.tableoid, c.oid, c.relname, "
 						 "c.relnamespace, c.relkind, c.reltype, "
+						 "c.relpages, c.reltuples, c.relallvisible, "
 						 "c.relowner, "
 						 "c.relchecks, "
 						 "c.relhasindex, c.relhasrules, c.relpages, "
@@ -7088,6 +7095,8 @@ getTables(Archive *fout, int *numTables)
 	i_relhasindex = PQfnumber(res, "relhasindex");
 	i_relhasrules = PQfnumber(res, "relhasrules");
 	i_relpages = PQfnumber(res, "relpages");
+	i_reltuples = PQfnumber(res, "reltuples");
+	i_relallvisible = PQfnumber(res, "relallvisible");
 	i_toastpages = PQfnumber(res, "toastpages");
 	i_owning_tab = PQfnumber(res, "owning_tab");
 	i_owning_col = PQfnumber(res, "owning_col");
@@ -7233,7 +7242,14 @@ getTables(Archive *fout, int *numTables)
 
 		/* Add statistics */
 		if (tblinfo[i].interesting)
-			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relkind);
+		{
+			char   *relpages = PQgetvalue(res, i, i_relpages);
+			char   *reltuples = PQgetvalue(res, i, i_reltuples);
+			char   *relallvisible = PQgetvalue(res, i, i_relallvisible);
+
+			getRelationStatistics(fout, &tblinfo[i].dobj, relpages, reltuples,
+								  relallvisible, tblinfo[i].relkind);
+		}
 
 		/*
 		 * Read-lock target tables to make sure they aren't DROPPED or altered
@@ -7499,6 +7515,9 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				i_oid,
 				i_indrelid,
 				i_indexname,
+				i_relpages,
+				i_reltuples,
+				i_relallvisible,
 				i_parentidx,
 				i_indexdef,
 				i_indnkeyatts,
@@ -7552,6 +7571,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 	appendPQExpBufferStr(query,
 						 "SELECT t.tableoid, t.oid, i.indrelid, "
 						 "t.relname AS indexname, "
+						 "t.relpages, t.reltuples, t.relallvisible, "
 						 "pg_catalog.pg_get_indexdef(i.indexrelid) AS indexdef, "
 						 "i.indkey, i.indisclustered, "
 						 "c.contype, c.conname, "
@@ -7659,6 +7679,9 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_oid = PQfnumber(res, "oid");
 	i_indrelid = PQfnumber(res, "indrelid");
 	i_indexname = PQfnumber(res, "indexname");
+	i_relpages = PQfnumber(res, "relpages");
+	i_reltuples = PQfnumber(res, "reltuples");
+	i_relallvisible = PQfnumber(res, "relallvisible");
 	i_parentidx = PQfnumber(res, "parentidx");
 	i_indexdef = PQfnumber(res, "indexdef");
 	i_indnkeyatts = PQfnumber(res, "indnkeyatts");
@@ -7725,6 +7748,9 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			char		contype;
 			char		indexkind;
 			RelStatsInfo *relstats;
+			char	   *relpages;
+			char	   *reltuples;
+			char	   *relallvisible;
 
 			indxinfo[j].dobj.objType = DO_INDEX;
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
@@ -7758,8 +7784,13 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			else
 				indexkind = RELKIND_PARTITIONED_INDEX;
 
+			relpages = PQgetvalue(res, j, i_relpages);
+			reltuples = PQgetvalue(res, j, i_reltuples);
+			relallvisible = PQgetvalue(res, j, i_relallvisible);
+
 			contype = *(PQgetvalue(res, j, i_contype));
-			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, indexkind);
+			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, relpages,
+											 reltuples, relallvisible, indexkind);
 
 			if (contype == 'p' || contype == 'u' || contype == 'x')
 			{
@@ -10383,18 +10414,6 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
-/*
- * Tabular description of the parameters to pg_restore_relation_stats()
- * param_name, param_type
- */
-static const char *rel_stats_arginfo[][2] = {
-	{"relation", "regclass"},
-	{"version", "integer"},
-	{"relpages", "integer"},
-	{"reltuples", "real"},
-	{"relallvisible", "integer"},
-};
-
 /*
  * Tabular description of the parameters to pg_restore_attribute_stats()
  * param_name, param_type
@@ -10419,30 +10438,6 @@ static const char *att_stats_arginfo[][2] = {
 	{"range_bounds_histogram", "text"},
 };
 
-/*
- * getRelStatsExportQuery --
- *
- * Generate a query that will fetch all relation (e.g. pg_class)
- * stats for a given relation.
- */
-static void
-getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
-					   const char *schemaname, const char *relname)
-{
-	resetPQExpBuffer(query);
-	appendPQExpBufferStr(query,
-						 "SELECT c.oid::regclass AS relation, "
-						 "current_setting('server_version_num') AS version, "
-						 "c.relpages, c.reltuples, c.relallvisible "
-						 "FROM pg_class c "
-						 "JOIN pg_namespace n "
-						 "ON n.oid = c.relnamespace "
-						 "WHERE n.nspname = ");
-	appendStringLiteralAH(query, schemaname, fout);
-	appendPQExpBufferStr(query, " AND c.relname = ");
-	appendStringLiteralAH(query, relname, fout);
-}
-
 /*
  * getAttStatsExportQuery --
  *
@@ -10521,33 +10516,23 @@ appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
  * Append a formatted pg_restore_relation_stats statement.
  */
 static void
-appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+appendRelStatsImport(PQExpBuffer out, Archive *fout, const RelStatsInfo *rsinfo)
 {
-	const char *sep = "";
+	const char *qualname = fmtQualifiedId(rsinfo->dobj.namespace->dobj.name, rsinfo->dobj.name);
+	char		version[32];
 
-	if (PQntuples(res) == 0)
-		return;
+	snprintf(version, sizeof(version), "%d", fout->remoteVersion);
 
 	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
-
-	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
-	{
-		const char *argname = rel_stats_arginfo[argno][0];
-		const char *argtype = rel_stats_arginfo[argno][1];
-		int			fieldno = PQfnumber(res, argname);
-
-		if (fieldno < 0)
-			pg_fatal("relation stats export query missing field '%s'",
-					 argname);
-
-		if (PQgetisnull(res, 0, fieldno))
-			continue;
-
-		appendPQExpBufferStr(out, sep);
-		appendNamedArgument(out, fout, argname, PQgetvalue(res, 0, fieldno), argtype);
-
-		sep = ",\n";
-	}
+	appendNamedArgument(out, fout, "relation", qualname, "regclass");
+	appendPQExpBufferStr(out, ",\n");
+	appendNamedArgument(out, fout, "version", version, "integer");
+	appendPQExpBufferStr(out, ",\n");
+	appendNamedArgument(out, fout, "relpages", rsinfo->relpages, "integer");
+	appendPQExpBufferStr(out, ",\n");
+	appendNamedArgument(out, fout, "reltuples", rsinfo->reltuples, "real");
+	appendPQExpBufferStr(out, ",\n");
+	appendNamedArgument(out, fout, "relallvisible", rsinfo->relallvisible, "integer");
 	appendPQExpBufferStr(out, "\n);\n");
 }
 
@@ -10643,15 +10628,11 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 	tag = createPQExpBuffer();
 	appendPQExpBufferStr(tag, fmtId(dobj->name));
 
-	query = createPQExpBuffer();
 	out = createPQExpBuffer();
 
-	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
-						   dobj->name);
-	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
-	appendRelStatsImport(out, fout, res);
-	PQclear(res);
+	appendRelStatsImport(out, fout, rsinfo);
 
+	query = createPQExpBuffer();
 	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
 						   dobj->name);
 	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index f08f5905aa3..1ab134ae414 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -438,6 +438,9 @@ typedef struct _indexAttachInfo
 typedef struct _relStatsInfo
 {
 	DumpableObject dobj;
+	char		relpages[32];
+	char		reltuples[32];
+	char		relallvisible[32];
 	char		relkind;		/* 'r', 'm', 'i', etc */
 	bool		postponed_def;	/* stats must be postponed into post-data */
 } RelStatsInfo;

base-commit: 2421e9a51d20bb83154e54a16ce628f9249fa907
-- 
2.48.1

v2-0002-Move-attribute-statistics-fetching-to-a-prepared-.patchtext/x-patch; charset=US-ASCII; name=v2-0002-Move-attribute-statistics-fetching-to-a-prepared-.patchDownload
From 5a03d2935999cac037daeaac566e7709f1824f5e Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Mon, 24 Feb 2025 04:36:16 -0500
Subject: [PATCH v2 2/3] Move attribute statistics fetching to a prepared
 statement.

Simplify the query to pg_stats by removing the joins to namespace and
pg_class, these were only needed because we wanted to get the relation
oid, but that's information that we already have.

Also, create a new prepared statement getAttributeStats for this query,
as it will be run once per relation.

Additionally, pull the server_version_num out of the export query, as we
already have that information. However, because version appeared in the
middle of the arginfo array, it has to move to either before the arginfo
loop, which would mix it in with the grain of the call (attname and
inherited would follow it) or to the last argument pair, which is what
was done.

Make the same move for the version parameter in the
pg_set_relation_stats() calls for consistency.
---
 src/bin/pg_dump/pg_backup.h |   3 +-
 src/bin/pg_dump/pg_dump.c   | 137 +++++++++++++++++-------------------
 2 files changed, 68 insertions(+), 72 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 350cf659c41..b78724671c5 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -74,9 +74,10 @@ enum _dumpPreparedQueries
 	PREPQUERY_DUMPTABLEATTACH,
 	PREPQUERY_GETCOLUMNACLS,
 	PREPQUERY_GETDOMAINCONSTRAINTS,
+	PREPQUERY_ATTRIBUTESTATS,
 };
 
-#define NUM_PREP_QUERIES (PREPQUERY_GETDOMAINCONSTRAINTS + 1)
+#define NUM_PREP_QUERIES (PREPQUERY_ATTRIBUTESTATS + 1)
 
 /* Parameters needed by ConnectDatabase; same for dump and restore */
 typedef struct _connParams
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a5e7aa73671..bbd415d5477 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -10419,10 +10419,8 @@ dumpComment(Archive *fout, const char *type,
  * param_name, param_type
  */
 static const char *att_stats_arginfo[][2] = {
-	{"relation", "regclass"},
 	{"attname", "name"},
 	{"inherited", "boolean"},
-	{"version", "integer"},
 	{"null_frac", "float4"},
 	{"avg_width", "integer"},
 	{"n_distinct", "float4"},
@@ -10438,59 +10436,6 @@ static const char *att_stats_arginfo[][2] = {
 	{"range_bounds_histogram", "text"},
 };
 
-/*
- * getAttStatsExportQuery --
- *
- * Generate a query that will fetch all attribute (e.g. pg_statistic)
- * stats for a given relation.
- */
-static void
-getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
-					   const char *schemaname, const char *relname)
-{
-	resetPQExpBuffer(query);
-	appendPQExpBufferStr(query,
-						 "SELECT c.oid::regclass AS relation, "
-						 "s.attname,"
-						 "s.inherited,"
-						 "current_setting('server_version_num') AS version, "
-						 "s.null_frac,"
-						 "s.avg_width,"
-						 "s.n_distinct,"
-						 "s.most_common_vals,"
-						 "s.most_common_freqs,"
-						 "s.histogram_bounds,"
-						 "s.correlation,"
-						 "s.most_common_elems,"
-						 "s.most_common_elem_freqs,"
-						 "s.elem_count_histogram,");
-
-	if (fout->remoteVersion >= 170000)
-		appendPQExpBufferStr(query,
-							 "s.range_length_histogram,"
-							 "s.range_empty_frac,"
-							 "s.range_bounds_histogram ");
-	else
-		appendPQExpBufferStr(query,
-							 "NULL AS range_length_histogram,"
-							 "NULL AS range_empty_frac,"
-							 "NULL AS range_bounds_histogram ");
-
-	appendPQExpBufferStr(query,
-						 "FROM pg_stats s "
-						 "JOIN pg_namespace n "
-						 "ON n.nspname = s.schemaname "
-						 "JOIN pg_class c "
-						 "ON c.relname = s.tablename "
-						 "AND c.relnamespace = n.oid "
-						 "WHERE s.schemaname = ");
-	appendStringLiteralAH(query, schemaname, fout);
-	appendPQExpBufferStr(query, " AND s.tablename = ");
-	appendStringLiteralAH(query, relname, fout);
-	appendPQExpBufferStr(query, " ORDER BY s.attname, s.inherited");
-}
-
-
 /*
  * appendNamedArgument --
  *
@@ -10516,23 +10461,20 @@ appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
  * Append a formatted pg_restore_relation_stats statement.
  */
 static void
-appendRelStatsImport(PQExpBuffer out, Archive *fout, const RelStatsInfo *rsinfo)
+appendRelStatsImport(PQExpBuffer out, Archive *fout,
+					 const RelStatsInfo *rsinfo, const char *qualname,
+					 const char *version)
 {
-	const char *qualname = fmtQualifiedId(rsinfo->dobj.namespace->dobj.name, rsinfo->dobj.name);
-	char		version[32];
-
-	snprintf(version, sizeof(version), "%d", fout->remoteVersion);
-
 	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
 	appendNamedArgument(out, fout, "relation", qualname, "regclass");
 	appendPQExpBufferStr(out, ",\n");
-	appendNamedArgument(out, fout, "version", version, "integer");
-	appendPQExpBufferStr(out, ",\n");
 	appendNamedArgument(out, fout, "relpages", rsinfo->relpages, "integer");
 	appendPQExpBufferStr(out, ",\n");
 	appendNamedArgument(out, fout, "reltuples", rsinfo->reltuples, "real");
 	appendPQExpBufferStr(out, ",\n");
 	appendNamedArgument(out, fout, "relallvisible", rsinfo->relallvisible, "integer");
+	appendPQExpBufferStr(out, ",\n");
+	appendNamedArgument(out, fout, "version", version, "integer");
 	appendPQExpBufferStr(out, "\n);\n");
 }
 
@@ -10542,13 +10484,16 @@ appendRelStatsImport(PQExpBuffer out, Archive *fout, const RelStatsInfo *rsinfo)
  * Append a series of formatted pg_restore_attribute_stats statements.
  */
 static void
-appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res,
+					 const char *qualname, const char *version)
 {
+	const char *sep = ",\n";
+
 	for (int rownum = 0; rownum < PQntuples(res); rownum++)
 	{
-		const char *sep = "";
-
 		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+		appendNamedArgument(out, fout, "relation", qualname, "regclass");
+
 		for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
 		{
 			const char *argname = att_stats_arginfo[argno][0];
@@ -10564,8 +10509,9 @@ appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
 
 			appendPQExpBufferStr(out, sep);
 			appendNamedArgument(out, fout, argname, PQgetvalue(res, rownum, fieldno), argtype);
-			sep = ",\n";
 		}
+		appendPQExpBufferStr(out, sep);
+		appendNamedArgument(out, fout, "version", version, "integer");
 		appendPQExpBufferStr(out, "\n);\n");
 	}
 }
@@ -10613,6 +10559,8 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 	DumpableObject *dobj = (DumpableObject *) &rsinfo->dobj;
 	DumpId	   *deps = NULL;
 	int			ndeps = 0;
+	const char *qualname;
+	char		version[32];
 
 	/* nothing to do if we are not dumping statistics */
 	if (!fout->dopt->dumpStatistics)
@@ -10625,18 +10573,64 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 		ndeps = dobj->nDeps;
 	}
 
+	qualname = pg_strdup(fmtQualifiedId(rsinfo->dobj.namespace->dobj.name, rsinfo->dobj.name));
+	snprintf(version, sizeof(version), "%d", fout->remoteVersion);
+
 	tag = createPQExpBuffer();
 	appendPQExpBufferStr(tag, fmtId(dobj->name));
 
 	out = createPQExpBuffer();
 
-	appendRelStatsImport(out, fout, rsinfo);
+	appendRelStatsImport(out, fout, rsinfo, qualname, version);
 
 	query = createPQExpBuffer();
-	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
-						   dobj->name);
+	if (!fout->is_prepared[PREPQUERY_ATTRIBUTESTATS])
+	{
+		appendPQExpBufferStr(query,
+							 "PREPARE getAttributeStats(pg_catalog.name, pg_catalog.name) AS\n"
+							"SELECT "
+							"s.attname, "
+							"s.inherited, "
+							"s.null_frac, "
+							"s.avg_width, "
+							"s.n_distinct, "
+							"s.most_common_vals, "
+							"s.most_common_freqs, "
+							"s.histogram_bounds, "
+							"s.correlation, "
+							"s.most_common_elems, "
+							"s.most_common_elem_freqs, "
+							"s.elem_count_histogram, ");
+
+		if (fout->remoteVersion >= 170000)
+			appendPQExpBufferStr(query,
+								"s.range_length_histogram,"
+								"s.range_empty_frac,"
+								"s.range_bounds_histogram ");
+		else
+			appendPQExpBufferStr(query,
+								"NULL AS range_length_histogram,"
+								"NULL AS range_empty_frac,"
+								"NULL AS range_bounds_histogram ");
+
+		appendPQExpBufferStr(query,
+							"FROM pg_stats s "
+							"WHERE s.schemaname = $1 "
+							"AND s.tablename = $2 "
+							"ORDER BY s.attname, s.inherited");
+
+		ExecuteSqlStatement(fout, query->data);
+
+		fout->is_prepared[PREPQUERY_ATTRIBUTESTATS] = true;
+	}
+
+	printfPQExpBuffer(query, "EXECUTE getAttributeStats(");
+	appendStringLiteralAH(query, dobj->namespace->dobj.name, fout);
+	appendPQExpBufferStr(query, ", ");
+	appendStringLiteralAH(query, dobj->name, fout);
+	appendPQExpBufferStr(query, "); ");
 	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
-	appendAttStatsImport(out, fout, res);
+	appendAttStatsImport(out, fout, res, qualname, version);
 	PQclear(res);
 
 	ArchiveEntry(fout, nilCatalogId, createDumpId(),
@@ -10652,6 +10646,7 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 	destroyPQExpBuffer(query);
 	destroyPQExpBuffer(out);
 	destroyPQExpBuffer(tag);
+	pg_free((void *) qualname);
 }
 
 /*
-- 
2.48.1

v2-0003-Remove-nonsense-bounds-checking-of-relpages-relal.patchtext/x-patch; charset=US-ASCII; name=v2-0003-Remove-nonsense-bounds-checking-of-relpages-relal.patchDownload
From 02bb2a98e7e290c53e3e130b330073d56af93f53 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Mon, 24 Feb 2025 05:01:59 -0500
Subject: [PATCH v2 3/3] Remove nonsense bounds checking of relpages,
 relallvisible.

Change both to type BlockNumber and fetch as PG_GETARG_UINT32 while
we're at it.
---
 src/backend/statistics/relation_stats.c | 49 ++++---------------------
 1 file changed, 8 insertions(+), 41 deletions(-)

diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index 046661d7c3f..e532c1ef6c7 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -62,62 +62,29 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
 {
 	Oid			reloid;
 	Relation	crel;
-	int32		relpages = DEFAULT_RELPAGES;
+	BlockNumber relpages = DEFAULT_RELPAGES;
 	bool		update_relpages = false;
 	float		reltuples = DEFAULT_RELTUPLES;
 	bool		update_reltuples = false;
-	int32		relallvisible = DEFAULT_RELALLVISIBLE;
+	BlockNumber	relallvisible = DEFAULT_RELALLVISIBLE;
 	bool		update_relallvisible = false;
-	bool		result = true;
 
 	if (!PG_ARGISNULL(RELPAGES_ARG))
 	{
-		relpages = PG_GETARG_INT32(RELPAGES_ARG);
-
-		/*
-		 * Partitioned tables may have relpages=-1. Note: for relations with
-		 * no storage, relpages=-1 is not used consistently, but must be
-		 * supported here.
-		 */
-		if (relpages < -1)
-		{
-			ereport(elevel,
-					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("relpages cannot be < -1")));
-			result = false;
-		}
-		else
-			update_relpages = true;
+		relpages = PG_GETARG_UINT32(RELPAGES_ARG);
+		update_relpages = true;
 	}
 
 	if (!PG_ARGISNULL(RELTUPLES_ARG))
 	{
 		reltuples = PG_GETARG_FLOAT4(RELTUPLES_ARG);
-
-		if (reltuples < -1.0)
-		{
-			ereport(elevel,
-					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("reltuples cannot be < -1.0")));
-			result = false;
-		}
-		else
-			update_reltuples = true;
+		update_reltuples = true;
 	}
 
 	if (!PG_ARGISNULL(RELALLVISIBLE_ARG))
 	{
-		relallvisible = PG_GETARG_INT32(RELALLVISIBLE_ARG);
-
-		if (relallvisible < 0)
-		{
-			ereport(elevel,
-					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("relallvisible cannot be < 0")));
-			result = false;
-		}
-		else
-			update_relallvisible = true;
+		relallvisible = PG_GETARG_UINT32(RELALLVISIBLE_ARG);
+		update_relallvisible = true;
 	}
 
 	stats_check_required_arg(fcinfo, relarginfo, RELATION_ARG);
@@ -237,7 +204,7 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
 
 	CommandCounterIncrement();
 
-	return result;
+	return true;
 }
 
 /*
-- 
2.48.1

#330Andres Freund
andres@anarazel.de
In reply to: Corey Huinker (#329)
Re: Statistics Import and Export

Hi,

On 2025-02-24 05:11:48 -0500, Corey Huinker wrote:

Incorporating most of the feedback (I kept a few of
the appendNamedArgument() calls) presented over the weekend.

* removeVersionNumStr is gone
* relpages/reltuples/relallvisible are now char[32] buffers in RelStatsInfo
and nowhere else (existing relpages conversion remains, however)

I don't see the point. This will use more memory and if we can't get
conversions between integers and strings right we have much bigger
problems. The same code was used in the backend too!

And it leads to storing relpages in two places, with different
transformations, which doesn't seem great.

@@ -6921,6 +6927,7 @@ getTables(Archive *fout, int *numTables)
appendPQExpBufferStr(query,
"SELECT c.tableoid, c.oid, c.relname, "
"c.relnamespace, c.relkind, c.reltype, "
+						 "c.relpages, c.reltuples, c.relallvisible, "
"c.relowner, "
"c.relchecks, "
"c.relhasindex, c.relhasrules, c.relpages, "

That query is already querying relpages a bit later in the query, so we'd
query the column twice.

+	printfPQExpBuffer(query, "EXECUTE getAttributeStats(");
+	appendStringLiteralAH(query, dobj->namespace->dobj.name, fout);
+	appendPQExpBufferStr(query, ", ");
+	appendStringLiteralAH(query, dobj->name, fout);
+	appendPQExpBufferStr(query, "); ");
res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);

It seems somewhat ugly that we're building an SQL string with non-trivial
constants. It'd be better to use PQexecParams() - but I guess we don't have
any uses of it yet in pg_dump.

ISTM that we ought to expose the relation oid in pg_stats. This query would be
simpler and faster if we could just use the oid as the predicate. Will take a
while till we can rely on that, but still.

Have you compared performance of with/without stats after these optimizations?

Greetings,

Andres Freund

#331Jeff Davis
pgsql@j-davis.com
In reply to: Andres Freund (#330)
Re: Statistics Import and Export

On Mon, 2025-02-24 at 09:54 -0500, Andres Freund wrote:

ISTM that we ought to expose the relation oid in pg_stats. This query
would be
simpler and faster if we could just use the oid as the predicate.
Will take a
while till we can rely on that, but still.

+1. Maybe an internal view that exposes only starelid/staattnum, and
pg_stats could just be a simple join on top of that?

There's another annoyance, which is that pg_stats doesn't expose any
custom stakinds, so we lose those, but I'm not sure if that's worth
trying to fix.

Regards,
Jeff Davis

#332Tom Lane
tgl@sss.pgh.pa.us
In reply to: Andres Freund (#330)
1 attachment(s)
Re: Statistics Import and Export

Andres Freund <andres@anarazel.de> writes:

On 2025-02-24 05:11:48 -0500, Corey Huinker wrote:

* relpages/reltuples/relallvisible are now char[32] buffers in RelStatsInfo
and nowhere else (existing relpages conversion remains, however)

I don't see the point. This will use more memory and if we can't get
conversions between integers and strings right we have much bigger
problems. The same code was used in the backend too!

I don't like that either. But there's a bigger problem with 0002:
it's still got mostly table-driven output. I've been working on
fixing the problem discussed over in the -committers thread about how
we need to identify index-expression columns by number not name [1]/messages/by-id/816167.1740278884@sss.pgh.pa.us.
It's not too awful in the backend (WIP patch attached), but
getting appendAttStatsImport to do it seems like a complete disaster,
and this patch fails to make that any easier. It'd be much better
if you gave up on that table-driven business and just open-coded the
handling of the successive output values as was discussed upthread.

I don't think the table-driven approach has anything to recommend it
anyway. It requires keeping att_stats_arginfo[] in sync with the
query in getAttStatsExportQuery, an extremely nonobvious (and
undocumented) connection. Personally I would nuke the separate
getAttStatsExportQuery and appendAttStatsImport functions altogether,
and have one function that executes a query and immediately interprets
the PGresult.

Also, while working on the attached, I couldn't help forming the
opinion that we'd be better off to nuke pg_set_attribute_stats()
from orbit and require people to use pg_restore_attribute_stats().
pg_set_attribute_stats() would be fine if we had a way to force
people to call it with only named-argument notation, but we don't.
So I'm afraid that its existence will encourage people to rely
on a specific parameter order, and then they'll whine if we
add/remove/reorder parameters, as indeed I had to do below.

BTW, I pushed the 0003 patch with minor adjustments.

regards, tom lane

[1]: /messages/by-id/816167.1740278884@sss.pgh.pa.us

Attachments:

allow-stats-att-name-or-number-wip.patchtext/x-diff; charset=us-ascii; name=allow-stats-att-name-or-number-wip.patchDownload
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 9f60a476eb..ad59e3be9d 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30302,6 +30302,7 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          <function>pg_set_attribute_stats</function> (
          <parameter>relation</parameter> <type>regclass</type>,
          <parameter>attname</parameter> <type>name</type>,
+         <parameter>attnum</parameter> <type>integer</type>,
          <parameter>inherited</parameter> <type>boolean</type>
          <optional>, <parameter>null_frac</parameter> <type>real</type></optional>
          <optional>, <parameter>avg_width</parameter> <type>integer</type></optional>
@@ -30318,14 +30319,17 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
         </para>
         <para>
          Creates or updates attribute-level statistics for the given relation
-         and attribute name to the specified values. The parameters correspond
+         and attribute name (or number) to the specified values. The
+         parameters correspond
          to attributes of the same name found in the <link
          linkend="view-pg-stats"><structname>pg_stats</structname></link>
          view.
         </para>
         <para>
          Optional parameters default to <literal>NULL</literal>, which leave
-         the corresponding statistic unchanged.
+         the corresponding statistic unchanged.  Exactly one
+         of <parameter>attname</parameter> and <parameter>attnum</parameter>
+         must be non-<literal>NULL</literal>.
         </para>
         <para>
          Ordinarily, these statistics are collected automatically or updated
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 591157b1d1..876500824e 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -648,8 +648,9 @@ AS 'pg_set_relation_stats';
 
 CREATE OR REPLACE FUNCTION
   pg_set_attribute_stats(relation regclass,
-                         attname name,
-                         inherited bool,
+                         attname name DEFAULT NULL,
+                         attnum integer DEFAULT NULL,
+                         inherited bool DEFAULT NULL,
                          null_frac real DEFAULT NULL,
                          avg_width integer DEFAULT NULL,
                          n_distinct real DEFAULT NULL,
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index c0c398a4bb..4886f79611 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -38,6 +38,7 @@ enum attribute_stats_argnum
 {
 	ATTRELATION_ARG = 0,
 	ATTNAME_ARG,
+	ATTNUM_ARG,
 	INHERITED_ARG,
 	NULL_FRAC_ARG,
 	AVG_WIDTH_ARG,
@@ -59,6 +60,7 @@ static struct StatsArgInfo attarginfo[] =
 {
 	[ATTRELATION_ARG] = {"relation", REGCLASSOID},
 	[ATTNAME_ARG] = {"attname", NAMEOID},
+	[ATTNUM_ARG] = {"attnum", INT4OID},
 	[INHERITED_ARG] = {"inherited", BOOLOID},
 	[NULL_FRAC_ARG] = {"null_frac", FLOAT4OID},
 	[AVG_WIDTH_ARG] = {"avg_width", INT4OID},
@@ -76,6 +78,22 @@ static struct StatsArgInfo attarginfo[] =
 	[NUM_ATTRIBUTE_STATS_ARGS] = {0}
 };
 
+enum clear_attribute_stats_argnum
+{
+	C_ATTRELATION_ARG = 0,
+	C_ATTNAME_ARG,
+	C_INHERITED_ARG,
+	C_NUM_ATTRIBUTE_STATS_ARGS
+};
+
+static struct StatsArgInfo cleararginfo[] =
+{
+	[C_ATTRELATION_ARG] = {"relation", REGCLASSOID},
+	[C_ATTNAME_ARG] = {"attname", NAMEOID},
+	[C_INHERITED_ARG] = {"inherited", BOOLOID},
+	[C_NUM_ATTRIBUTE_STATS_ARGS] = {0}
+};
+
 static bool attribute_statistics_update(FunctionCallInfo fcinfo, int elevel);
 static Node *get_attr_expr(Relation rel, int attnum);
 static void get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
@@ -116,9 +134,9 @@ static bool
 attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 {
 	Oid			reloid;
-	Name		attname;
-	bool		inherited;
+	char	   *attname;
 	AttrNumber	attnum;
+	bool		inherited;
 
 	Relation	starel;
 	HeapTuple	statup;
@@ -164,21 +182,51 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	/* lock before looking up attribute */
 	stats_lock_check_privileges(reloid);
 
-	stats_check_required_arg(fcinfo, attarginfo, ATTNAME_ARG);
-	attname = PG_GETARG_NAME(ATTNAME_ARG);
-	attnum = get_attnum(reloid, NameStr(*attname));
+	/* user can specify either attname or attnum, but not both */
+	if (!PG_ARGISNULL(ATTNAME_ARG))
+	{
+		Name		attnamename;
+
+		if (!PG_ARGISNULL(ATTNUM_ARG))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("must specify one of attname and attnum")));
+		attnamename = PG_GETARG_NAME(ATTNAME_ARG);
+		attname = NameStr(*attnamename);
+		attnum = get_attnum(reloid, attname);
+		/* note that this test covers attisdropped cases too: */
+		if (attnum == InvalidAttrNumber)
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_COLUMN),
+					 errmsg("column \"%s\" of relation \"%s\" does not exist",
+							attname, get_rel_name(reloid))));
+	}
+	else if (!PG_ARGISNULL(ATTNUM_ARG))
+	{
+		attnum = PG_GETARG_INT32(ATTNUM_ARG);
+		attname = get_attname(reloid, attnum, true);
+		/* Annoyingly, get_attname doesn't check attisdropped */
+		if (attname == NULL ||
+			!SearchSysCacheExistsAttName(reloid, attname))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_COLUMN),
+					 errmsg("column %d of relation \"%s\" does not exist",
+							attnum, get_rel_name(reloid))));
+	}
+	else
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("must specify one of attname and attnum")));
+		attname = NULL;			/* keep compiler quiet */
+		attnum = 0;
+	}
 
 	if (attnum < 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot modify statistics on system column \"%s\"",
-						NameStr(*attname))));
-
-	if (attnum == InvalidAttrNumber)
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_COLUMN),
-				 errmsg("column \"%s\" of relation \"%s\" does not exist",
-						NameStr(*attname), get_rel_name(reloid))));
+						attname)));
 
 	stats_check_required_arg(fcinfo, attarginfo, INHERITED_ARG);
 	inherited = PG_GETARG_BOOL(INHERITED_ARG);
@@ -245,7 +293,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 								&elemtypid, &elem_eq_opr))
 		{
 			ereport(elevel,
-					(errmsg("unable to determine element type of attribute \"%s\"", NameStr(*attname)),
+					(errmsg("unable to determine element type of attribute \"%s\"", attname),
 					 errdetail("Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.")));
 			elemtypid = InvalidOid;
 			elem_eq_opr = InvalidOid;
@@ -261,7 +309,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	{
 		ereport(elevel,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				 errmsg("could not determine less-than operator for attribute \"%s\"", NameStr(*attname)),
+				 errmsg("could not determine less-than operator for attribute \"%s\"", attname),
 				 errdetail("Cannot set STATISTIC_KIND_HISTOGRAM or STATISTIC_KIND_CORRELATION.")));
 
 		do_histogram = false;
@@ -275,7 +323,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	{
 		ereport(elevel,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				 errmsg("attribute \"%s\" is not a range type", NameStr(*attname)),
+				 errmsg("attribute \"%s\" is not a range type", attname),
 				 errdetail("Cannot set STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM or STATISTIC_KIND_BOUNDS_HISTOGRAM.")));
 
 		do_bounds_histogram = false;
@@ -855,8 +903,8 @@ init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
  * Import statistics for a given relation attribute.
  *
  * Inserts or replaces a row in pg_statistic for the given relation and
- * attribute name. It takes input parameters that correspond to columns in the
- * view pg_stats.
+ * attribute name or number. It takes input parameters that correspond to
+ * columns in the view pg_stats.
  *
  * Parameters null_frac, avg_width, and n_distinct all correspond to NOT NULL
  * columns in pg_statistic. The remaining parameters all belong to a specific
@@ -889,8 +937,8 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 	AttrNumber	attnum;
 	bool		inherited;
 
-	stats_check_required_arg(fcinfo, attarginfo, ATTRELATION_ARG);
-	reloid = PG_GETARG_OID(ATTRELATION_ARG);
+	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELATION_ARG);
+	reloid = PG_GETARG_OID(C_ATTRELATION_ARG);
 
 	if (RecoveryInProgress())
 		ereport(ERROR,
@@ -900,8 +948,8 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 
 	stats_lock_check_privileges(reloid);
 
-	stats_check_required_arg(fcinfo, attarginfo, ATTNAME_ARG);
-	attname = PG_GETARG_NAME(ATTNAME_ARG);
+	stats_check_required_arg(fcinfo, cleararginfo, C_ATTNAME_ARG);
+	attname = PG_GETARG_NAME(C_ATTNAME_ARG);
 	attnum = get_attnum(reloid, NameStr(*attname));
 
 	if (attnum < 0)
@@ -916,8 +964,8 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 				 errmsg("column \"%s\" of relation \"%s\" does not exist",
 						NameStr(*attname), get_rel_name(reloid))));
 
-	stats_check_required_arg(fcinfo, attarginfo, INHERITED_ARG);
-	inherited = PG_GETARG_BOOL(INHERITED_ARG);
+	stats_check_required_arg(fcinfo, cleararginfo, C_INHERITED_ARG);
+	inherited = PG_GETARG_BOOL(C_INHERITED_ARG);
 
 	delete_pg_statistic(reloid, attnum, inherited);
 	PG_RETURN_VOID();
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index af9546de23..ce714b1fd1 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12433,8 +12433,8 @@
   descr => 'set statistics on attribute',
   proname => 'pg_set_attribute_stats', provolatile => 'v', proisstrict => 'f',
   proparallel => 'u', prorettype => 'void',
-  proargtypes => 'regclass name bool float4 int4 float4 text _float4 text float4 text _float4 _float4 text float4 text',
-  proargnames => '{relation,attname,inherited,null_frac,avg_width,n_distinct,most_common_vals,most_common_freqs,histogram_bounds,correlation,most_common_elems,most_common_elem_freqs,elem_count_histogram,range_length_histogram,range_empty_frac,range_bounds_histogram}',
+  proargtypes => 'regclass name int4 bool float4 int4 float4 text _float4 text float4 text _float4 _float4 text float4 text',
+  proargnames => '{relation,attname,attnum,inherited,null_frac,avg_width,n_distinct,most_common_vals,most_common_freqs,histogram_bounds,correlation,most_common_elems,most_common_elem_freqs,elem_count_histogram,range_length_histogram,range_empty_frac,range_bounds_histogram}',
   prosrc => 'pg_set_attribute_stats' },
 { oid => '9163',
   descr => 'clear statistics on attribute',
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index 0e8491131e..a020ff015d 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -364,7 +364,7 @@ SELECT pg_catalog.pg_set_attribute_stats(
     null_frac => 0.1::real,
     avg_width => 2::integer,
     n_distinct => 0.3::real);
-ERROR:  "attname" cannot be NULL
+ERROR:  must specify one of attname and attnum
 -- error: inherited null
 SELECT pg_catalog.pg_set_attribute_stats(
     relation => 'stats_import.test'::regclass,
@@ -968,7 +968,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'null_frac', 0.1::real,
     'avg_width', 2::integer,
     'n_distinct', 0.3::real);
-ERROR:  "attname" cannot be NULL
+ERROR:  must specify one of attname and attnum
 -- error: attname doesn't exist
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
#333Corey Huinker
corey.huinker@gmail.com
In reply to: Andres Freund (#330)
Re: Statistics Import and Export

On Mon, Feb 24, 2025 at 9:54 AM Andres Freund <andres@anarazel.de> wrote:

Hi,

On 2025-02-24 05:11:48 -0500, Corey Huinker wrote:

Incorporating most of the feedback (I kept a few of
the appendNamedArgument() calls) presented over the weekend.

* removeVersionNumStr is gone
* relpages/reltuples/relallvisible are now char[32] buffers in

RelStatsInfo

and nowhere else (existing relpages conversion remains, however)

I don't see the point. This will use more memory and if we can't get
conversions between integers and strings right we have much bigger
problems. The same code was used in the backend too!

As I see it, the point is that we're getting an input that is a string
representation from the query, and the end-goal is to convey that value
with fidelity to the destination database, so there's nothing we can do to
get us closer to the string that we already have.

I don't have benchmark numbers beyond the instinct that doing something
takes more time than doing nothing. Granted, "nothing" here means 96 bytes
of memory and 3 strncpy()s, and "something" is 24 bytes of memory, 2
atoi()s, 1 strtof() plus whatever memory and processing we do back in
converting back to strings.

And it leads to storing relpages in two places, with different
transformations, which doesn't seem great.

I didn't like that either, but balanced the ugliness of that vs the cost of
grinding the values back to where we started.

@@ -6921,6 +6927,7 @@ getTables(Archive *fout, int *numTables)
appendPQExpBufferStr(query,
"SELECT c.tableoid,

c.oid, c.relname, "

"c.relnamespace,

c.relkind, c.reltype, "

+ "c.relpages, c.reltuples,

c.relallvisible, "

"c.relowner, "
"c.relchecks, "
"c.relhasindex,

c.relhasrules, c.relpages, "

That query is already querying relpages a bit later in the query, so we'd
query the column twice.

+1, must eliminate that duplicate.

+     printfPQExpBuffer(query, "EXECUTE getAttributeStats(");
+     appendStringLiteralAH(query, dobj->namespace->dobj.name, fout);
+     appendPQExpBufferStr(query, ", ");
+     appendStringLiteralAH(query, dobj->name, fout);
+     appendPQExpBufferStr(query, "); ");
res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);

It seems somewhat ugly that we're building an SQL string with non-trivial
constants. It'd be better to use PQexecParams() - but I guess we don't have
any uses of it yet in pg_dump.

+1, I would like to see that change.

ISTM that we ought to expose the relation oid in pg_stats. This query
would be
simpler and faster if we could just use the oid as the predicate. Will
take a
while till we can rely on that, but still.

+1, but we will need to support this until v18 is as old as v9.2 is
now...approx 2038.

Have you compared performance of with/without stats after these
optimizations?

I have not.

#334Jeff Davis
pgsql@j-davis.com
In reply to: Tom Lane (#332)
Re: Statistics Import and Export

On Mon, 2025-02-24 at 12:50 -0500, Tom Lane wrote:

Also, while working on the attached, I couldn't help forming the
opinion that we'd be better off to nuke pg_set_attribute_stats()
from orbit and require people to use pg_restore_attribute_stats().

I had intended the pg_set variants to be useful for ad-hoc stats
hacking (e.g. for reproducing a plan or for testing the optimizer). For
those use cases, the following differences seem nice:

1. named arguments are easier to write ad-hoc than lining up the
parameters in pairs
2. elevel=ERROR makes more sense than WARNING for that kind of use
case.
3. for relation stats, we don't want in-place updates, because you
want ROLLBACK to work

Those seemed different enough from the restore case that another entry
point made sense to me.

pg_set_attribute_stats() would be fine if we had a way to force
people to call it with only named-argument notation, but we don't.
So I'm afraid that its existence will encourage people to rely
on a specific parameter order, and then they'll whine if we
add/remove/reorder parameters, as indeed I had to do below.

That's a good point that I hadn't considered, so perhaps we can't solve
problem #1. The other two problems might be solvable though:

* To avoid in-place updates I think we do need a separate function,
at least for relation stats (attribute stats never do in-place
updates). We could potentially have another name/value pair to choose,
but it's impossible to choose a reasonable default: if "inplace" is the
default, that means the user would need to opt-out of it for ROLLBACK
to work; if "mvcc" is the default, that means pg_dump would need to
choose "inplace", and I don't think pg_dump should be making those
kinds of decisions.

* The elevel=ERROR is not terribly important, so perhaps we can just
always do elevel=WARNING. If we did try to present it as an option,
then that presents the same problems as an "inplace" option.

So perhaps we can just have the pg_set variants set elevel=ERROR and
inplace=false, and otherwise be identical to the pg_restore variants?

Regards,
Jeff Davis

#335Corey Huinker
corey.huinker@gmail.com
In reply to: Tom Lane (#332)
Re: Statistics Import and Export

On Mon, Feb 24, 2025 at 12:51 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:

Andres Freund <andres@anarazel.de> writes:

On 2025-02-24 05:11:48 -0500, Corey Huinker wrote:

* relpages/reltuples/relallvisible are now char[32] buffers in

RelStatsInfo

and nowhere else (existing relpages conversion remains, however)

I don't see the point. This will use more memory and if we can't get
conversions between integers and strings right we have much bigger
problems. The same code was used in the backend too!

I don't like that either. But there's a bigger problem with 0002:
it's still got mostly table-driven output. I've been working on
fixing the problem discussed over in the -committers thread about how
we need to identify index-expression columns by number not name [1].

There doesn't seem to be any way around it, but it will slightly complicate
the dump-ing side of things, in that we need to either:

a) switch to attnums for index expressions and keep attname calls for
everything else.

b) track what the attnum will be on the destination side, which will be
different when we're not doing a binary upgrade and there are any preceding
dropped columns.

The patch Tom provided opens the door for option "a", and I'm inclined to
take it.

It's not too awful in the backend (WIP patch attached), but
getting appendAttStatsImport to do it seems like a complete disaster,
and this patch fails to make that any easier. It'd be much better
if you gave up on that table-driven business and just open-coded the
handling of the successive output values as was discussed upthread.

Can do.

I don't think the table-driven approach has anything to recommend it
anyway. It requires keeping att_stats_arginfo[] in sync with the
query in getAttStatsExportQuery, an extremely nonobvious (and
undocumented) connection. Personally I would nuke the separate
getAttStatsExportQuery and appendAttStatsImport functions altogether,
and have one function that executes a query and immediately interprets
the PGresult.

+1, though that comes at the cost of shutting off the possibility of a mass
fetch from pg_stats without also rendering the pg_restore_attribute_stats
calls at the same time.

Also, while working on the attached, I couldn't help forming the
opinion that we'd be better off to nuke pg_set_attribute_stats()
from orbit and require people to use pg_restore_attribute_stats().
pg_set_attribute_stats() would be fine if we had a way to force
people to call it with only named-argument notation, but we don't.
So I'm afraid that its existence will encourage people to rely
on a specific parameter order, and then they'll whine if we
add/remove/reorder parameters, as indeed I had to do below.

They've always had split goals. To recap for people just joining the show,
the "set" family had the following properties:

1. transactional, even for pg_class
2. assumes all stats given are relevant and correct for current db version
3. guaranteed to ERROR if any parameter doesn't check out
4. unstable call signature, can and will change to match the realities of
the current version
5. intended for planner experiments and fuzzing

and the "restore" family has the following properties:

1. will inplace update pg_class to avoid table bloat
2. states the version from whence the stats came, so that adjustments can
be made to suit the current db version, up to and including rejecting that
particular statistic
3. attempts to sidestep errors with WARNINGs so as not to kill a restore
4. stable but highly fluid kwargs-ish call signature
5. intended to be machine generated and used only in restore/upgrade

The attnum change certainly throws a wrench into that, and if we get rid of
the setter functions then we will need to (re)introduce parameters to
indicate our choice for properties 1 and 3. I suppose we could use the
existence or non-existence of the "version" parameter as an indicator of
which mode we want (if it exists, we want WARNINGS and inplace updates, if
not we want pure transactional and ERROR at the first problem), but I'm not
certain that proxy will hold true in the future.

BTW, I pushed the 0003 patch with minor adjustments.

Thanks!

#336Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#335)
Re: Statistics Import and Export

On Mon, 2025-02-24 at 13:47 -0500, Corey Huinker wrote:

There doesn't seem to be any way around it, but it will
slightly complicate the dump-ing side of things, in that we need to
either:

a) switch to attnums for index expressions and keep attname calls for
everything else.

The only stats for indexes are on expression columns, so AFAICT there's
no difference between the above description and "use attnums for
indexes and attnames for tables". Either way, I agree that's the way to
go.

We certainly want attnames for tables to keep it working reasonably
well for cases where the user might be doing something more interesting
than a binary upgrade, as you point out. But attribute numbers for
indexes seem much more reliable: an index with a different attribute
order is a fundamentally different index.

Regards,
Jeff Davis

#337Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#336)
Re: Statistics Import and Export

On Mon, Feb 24, 2025 at 1:54 PM Jeff Davis <pgsql@j-davis.com> wrote:

On Mon, 2025-02-24 at 13:47 -0500, Corey Huinker wrote:

There doesn't seem to be any way around it, but it will
slightly complicate the dump-ing side of things, in that we need to
either:

a) switch to attnums for index expressions and keep attname calls for
everything else.

The only stats for indexes are on expression columns, so AFAICT there's
no difference between the above description and "use attnums for
indexes and attnames for tables". Either way, I agree that's the way to
go.

That's true now, but may not be in the future, like if we started keeping
separate stats for partial indexes.

We certainly want attnames for tables to keep it working reasonably
well for cases where the user might be doing something more interesting
than a binary upgrade, as you point out. But attribute numbers for
indexes seem much more reliable: an index with a different attribute
order is a fundamentally different index.

Sadly, that attnum isn't available in pg_stats, so we'd have to reintroduce
the joins to pg_namespace and pg_class to get at pg_attribute, at least for
indexes.

#338Tom Lane
tgl@sss.pgh.pa.us
In reply to: Jeff Davis (#336)
Re: Statistics Import and Export

Jeff Davis <pgsql@j-davis.com> writes:

We certainly want attnames for tables to keep it working reasonably
well for cases where the user might be doing something more interesting
than a binary upgrade, as you point out. But attribute numbers for
indexes seem much more reliable: an index with a different attribute
order is a fundamentally different index.

Right. We went through pretty much this reasoning, as I recall,
when we invented ALTER INDEX ... SET STATISTICS. The original
version used a column name like ALTER TABLE did, and we ran into
exactly the present problem that the names aren't too stable across
dump/restore, and we decided that index column numbers would do
instead. You can't add or drop a column of an index, nor redefine it
meaningfully, except by dropping the whole index which will make any
associated stats go away.

The draft patch I posted allows callers to use attname or attnum
at their option, because I didn't see a reason to restrict that.
But I envisioned that pg_dump would always use attname for table
columns and attnum for index columns.

regards, tom lane

#339Tom Lane
tgl@sss.pgh.pa.us
In reply to: Corey Huinker (#337)
Re: Statistics Import and Export

Corey Huinker <corey.huinker@gmail.com> writes:

Sadly, that attnum isn't available in pg_stats, so we'd have to reintroduce
the joins to pg_namespace and pg_class to get at pg_attribute, at least for
indexes.

This argument seems to be made still from the mindset that you are
going to form a query that produces exactly what needs to be dumped,
without any additional processing. pg_dump has all that info at
hand; there is no need to re-query the server for it.

regards, tom lane

#340Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#333)
Re: Statistics Import and Export

On Mon, 2025-02-24 at 12:57 -0500, Corey Huinker wrote:

As I see it, the point is that we're getting an input that is a
string representation from the query, and the end-goal is to convey
that value with fidelity to the destination database, so there's
nothing we can do to get us closer to the string that we already
have.

As Andres mentioned, for the float-to-string conversion, we're using
what the backend does, so it doesn't seem like a problem.

But you have a point in that float4in() does slightly more work than
strtof() to handle platform differences about NaN/Inf. I'm not sure how
much to weigh that concern, but I agree that there is non-zero
cognitive overhead here.

Should we solve that problem by moving some of that code to src/common
src/port somewhere?

I don't have benchmark numbers beyond the instinct that
doing something takes more time than doing nothing. Granted,
"nothing" here means 96 bytes of memory and 3 strncpy()s, and
"something" is 24 bytes of memory, 2 atoi()s, 1 strtof() plus
whatever memory and processing we do back in converting back to
strings.

To me, this argument is, at best, premature optimization. Even if there
were a few cycles saved here somewhere, you'd need to compare that
against the wasted memory. Using 2 int32s and a float4 is only 12 bytes
(not 24) versus 96 for the strings.

Anyone looking at the structure would be wondering (a) why we're using
32 bytes to store something where the natural representation is 4
bytes; and (b) whether that memory adds up to anything worth worrying
about. I'm sure we could analyze that and write an explanatory comment,
but that has non-zero cognitive overhead, as well.

Regards,
Jeff Davis

#341Tom Lane
tgl@sss.pgh.pa.us
In reply to: Jeff Davis (#340)
Re: Statistics Import and Export

Jeff Davis <pgsql@j-davis.com> writes:

But you have a point in that float4in() does slightly more work than
strtof() to handle platform differences about NaN/Inf. I'm not sure how
much to weigh that concern, but I agree that there is non-zero
cognitive overhead here.

If we're speaking strictly about the reltuples value, I'm not hugely
concerned about that. reltuples should never be NaN or Inf. There
is a nonzero chance that it will round off to a fractionally
different value if we pass it through strtof/sprintf on the pg_dump
side, but nobody is really going to care about that. (Maybe our
own pg_dump test script would, thanks to its not-too-bright dump
comparison logic. But that script is never going to see reltuples
values that are big enough to be inexact in a float4.)

I do buy the better-preserve-it-exactly argument for other sorts
of statistics, where we don't have such a good sense of what might
matter.

regards, tom lane

#342Jeff Davis
pgsql@j-davis.com
In reply to: Tom Lane (#341)
1 attachment(s)
Re: Statistics Import and Export

On Mon, 2025-02-24 at 15:03 -0500, Tom Lane wrote:

Jeff Davis <pgsql@j-davis.com> writes:

But you have a point in that float4in() does slightly more work
than
strtof() to handle platform differences about NaN/Inf. I'm not sure
how
much to weigh that concern, but I agree that there is non-zero
cognitive overhead here.

If we're speaking strictly about the reltuples value, I'm not hugely
concerned about that.  reltuples should never be NaN or Inf.

There actually is a concern here, in that the backend always has
LC_NUMERIC=C when doing float4in/out, but pg_dump does not. Patch
attached.

Regards,
Jeff Davis

Attachments:

pg-dump-setlocale.difftext/x-patch; charset=UTF-8; name=pg-dump-setlocale.diffDownload
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 955550b91d2..ed85d843d5f 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -521,6 +521,9 @@ main(int argc, char **argv)
 		{NULL, 0, NULL, 0}
 	};
 
+	/* ensure that locale does not affect floating point interpretation */
+	setlocale(LC_NUMERIC, "C");
+
 	pg_logging_init(argv[0]);
 	pg_logging_set_level(PG_LOG_WARNING);
 	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_dump"));
#343Andres Freund
andres@anarazel.de
In reply to: Corey Huinker (#335)
Re: Statistics Import and Export

Hi,

On 2025-02-24 13:47:19 -0500, Corey Huinker wrote:

and the "restore" family has the following properties:

1. will inplace update pg_class to avoid table bloat

I suspect that this is a *really* bad idea. It's very very hard to get inplace
updates right. We have several unfixed correctness bugs that are related to
the use of inplace updates. I really don't think it's wise to add additional
interfaces that can reach inplace updates unless there's really no other
alternative (like not being able to assign an xid in VACUUM to be able to deal
with anti-xid-wraparound-shutdown systems).

Greetings,

Andres Freund

#344Tom Lane
tgl@sss.pgh.pa.us
In reply to: Jeff Davis (#342)
Re: Statistics Import and Export

Jeff Davis <pgsql@j-davis.com> writes:

There actually is a concern here, in that the backend always has
LC_NUMERIC=C when doing float4in/out, but pg_dump does not.

Hmm ... interesting point, but does it matter? I think we always use
our own sprintf even in frontend, and it doesn't react to LC_NUMERIC.
I guess atof might be more of a concern though.

Patch attached.

I'm a little suspicious whether that has any effect if you insert it
before set_pglocale_pgservice().

regards, tom lane

#345Corey Huinker
corey.huinker@gmail.com
In reply to: Tom Lane (#339)
Re: Statistics Import and Export

On Mon, Feb 24, 2025 at 2:36 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:

Corey Huinker <corey.huinker@gmail.com> writes:

Sadly, that attnum isn't available in pg_stats, so we'd have to

reintroduce

the joins to pg_namespace and pg_class to get at pg_attribute, at least

for

indexes.

This argument seems to be made still from the mindset that you are
going to form a query that produces exactly what needs to be dumped,
without any additional processing. pg_dump has all that info at
hand; there is no need to re-query the server for it.

I went looking just now, and I can't find it. I see where we have attname
and attnum arrays for tables, but not indexes. We keep an array of attnums
for the index, but we'd need to add an array of attnames in order to
correlate back to our results of pg_stats. If I'm missing something, please
correct me, but it seems like all the index stuff we'd get from attributes
we instead get from:

"pg_catalog.pg_get_indexdef(i.indexrelid) AS indexdef, "

#346Jeff Davis
pgsql@j-davis.com
In reply to: Andres Freund (#343)
Re: Statistics Import and Export

On Mon, 2025-02-24 at 15:36 -0500, Andres Freund wrote:

1. will inplace update pg_class to avoid table bloat

I suspect that this is a *really* bad idea.

The reason we added it is that it's what ANALYZE does, and a big
restore bloats pg_class without it.

I don't think those are major concerns for v1, so in principle I'm fine
removing it. But the problem is that it affects the documented
semantics, so it would be hard to change later, and we'd be stuck with
the bloating behavior forever.

Regards,
Jeff Davis

#347Corey Huinker
corey.huinker@gmail.com
In reply to: Andres Freund (#343)
Re: Statistics Import and Export

I suspect that this is a *really* bad idea. It's very very hard to get
inplace
updates right. We have several unfixed correctness bugs that are related to
the use of inplace updates. I really don't think it's wise to add
additional
interfaces that can reach inplace updates unless there's really no other
alternative (like not being able to assign an xid in VACUUM to be able to
deal
with anti-xid-wraparound-shutdown systems).

In this case, the alternative is an immediate doubling of the size of
pg_class right after a restore/upgrade.

#348Andres Freund
andres@anarazel.de
In reply to: Corey Huinker (#347)
Re: Statistics Import and Export

Hi,

On 2025-02-24 15:45:10 -0500, Corey Huinker wrote:

I suspect that this is a *really* bad idea. It's very very hard to get
inplace
updates right. We have several unfixed correctness bugs that are related to
the use of inplace updates. I really don't think it's wise to add
additional
interfaces that can reach inplace updates unless there's really no other
alternative (like not being able to assign an xid in VACUUM to be able to
deal
with anti-xid-wraparound-shutdown systems).

In this case, the alternative is an immediate doubling of the size of
pg_class right after a restore/upgrade.

I don't think that's necessarily true, hot pruning might help some, as afaict
the restore happens in multiple transactions.

But even if that's the case, I don't think it's worth using in place updates
to avoid it. We should work to get rid of them, not introduce them in more
places.

And typically pg_class size isn't the relevant factor, it's pg_attribute etc.

Greetings,

Andres Freund

#349Tom Lane
tgl@sss.pgh.pa.us
In reply to: Corey Huinker (#345)
Re: Statistics Import and Export

Corey Huinker <corey.huinker@gmail.com> writes:

On Mon, Feb 24, 2025 at 2:36 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:

... pg_dump has all that info at
hand; there is no need to re-query the server for it.

I went looking just now, and I can't find it. I see where we have attname
and attnum arrays for tables, but not indexes. We keep an array of attnums
for the index, but we'd need to add an array of attnames in order to
correlate back to our results of pg_stats.

Hmm ... I was thinking we had it already for ALTER INDEX SET
STATISTICS, but I see that is depending on some quite ad-hoc
code (look for indstatcols and indstatvals in pg_dump.c).
I wonder if we could generalize that a bit and share the
work with this case. Those array_agg calls don't look too fast
anyway, would be better if we could rewrite as a join I bet.

regards, tom lane

#350Jeff Davis
pgsql@j-davis.com
In reply to: Andres Freund (#348)
Re: Statistics Import and Export

On Mon, 2025-02-24 at 15:53 -0500, Andres Freund wrote:

I don't think that's necessarily true, hot pruning might help some,
as afaict
the restore happens in multiple transactions.

Yeah, I just dumped and reloaded the regression database with and
without stats, and saw no difference in the resulting size. So it's
probably more correct to say "churn" rather than "bloat".

Even running "psql -1", I see modest bloat substantially less than 2x.

So if we agree that we don't mind a bit of churn and we will never need
this (despite what ANALYZE does), then I'm OK removing it. Which makes
me wonder why ANALYZE does it with inplace updates?

Regards,
Jeff Davis

#351Corey Huinker
corey.huinker@gmail.com
In reply to: Andres Freund (#348)
Re: Statistics Import and Export

I don't think that's necessarily true, hot pruning might help some, as
afaict
the restore happens in multiple transactions.

If we're willing to take the potential bloat to avoid a nasty complexity,
then I'm all for discarding it. Jeff just indicated off-list that he isn't
seeing noticeable difference in table size, maybe we're safe with how we
use the function now.

But even if that's the case, I don't think it's worth using in place
updates
to avoid it. We should work to get rid of them, not introduce them in more
places.

As the number of statlike columns in pg_class grows, might it make sense to
break them off into their own relation, leaving pg_class to be far more
stable?

#352Jeff Davis
pgsql@j-davis.com
In reply to: Tom Lane (#344)
Re: Statistics Import and Export

On Mon, 2025-02-24 at 15:40 -0500, Tom Lane wrote:

I'm a little suspicious whether that has any effect if you insert it
before set_pglocale_pgservice().

Ah, right. Corey, can you please include that (in the right place, of
course) to the next iteration of your 0001 patch, if it's doing the
conversions to/from float4?

Regards,
Jeff Davis

#353Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#352)
Re: Statistics Import and Export

On Mon, Feb 24, 2025 at 4:07 PM Jeff Davis <pgsql@j-davis.com> wrote:

On Mon, 2025-02-24 at 15:40 -0500, Tom Lane wrote:

I'm a little suspicious whether that has any effect if you insert it
before set_pglocale_pgservice().

Ah, right. Corey, can you please include that (in the right place, of
course) to the next iteration of your 0001 patch, if it's doing the
conversions to/from float4?

Sure, I'm debating whether I want to solve the index-expression-attname
issue before embarking on the next iteration, or temporarily shelve that so
that we can review the other changes so far.

#354Tom Lane
tgl@sss.pgh.pa.us
In reply to: Tom Lane (#349)
Re: Statistics Import and Export

I wrote:

Hmm ... I was thinking we had it already for ALTER INDEX SET
STATISTICS, but I see that is depending on some quite ad-hoc
code (look for indstatcols and indstatvals in pg_dump.c).
I wonder if we could generalize that a bit and share the
work with this case. Those array_agg calls don't look too fast
anyway, would be better if we could rewrite as a join I bet.

After a bit of playing around, it seemed messy to make it into
a join, but we could replace the two array_agg sub-selects with
a single one:

(SELECT pg_catalog.array_agg(ROW(attname, attstattarget) ORDER BY attnum)
FROM pg_catalog.pg_attribute WHERE attrelid = i.indexrelid)

and then what we need could be pulled out of that, although
I'm not sure if pg_dump has logic at hand for deconstructing an
array of composite. Or we could leave it as two array_aggs,
aggregating attname and attstattarget separately but removing
the attstattarget filter.

regards, tom lane

#355Corey Huinker
corey.huinker@gmail.com
In reply to: Tom Lane (#354)
Re: Statistics Import and Export

After a bit of playing around, it seemed messy to make it into
a join, but we could replace the two array_agg sub-selects with
a single one:

(SELECT pg_catalog.array_agg(ROW(attname, attstattarget) ORDER BY attnum)
FROM pg_catalog.pg_attribute WHERE attrelid = i.indexrelid)

and then what we need could be pulled out of that, although
I'm not sure if pg_dump has logic at hand for deconstructing an
array of composite.

From what I can see, it doesn't. Moreover, the attstattarget array agg is
only done in version 11 and higher, and we need to go as far back as we've
got expression indexes.

Or we could leave it as two array_aggs,
aggregating attname and attstattarget separately but removing
the attstattarget filter.

That's what I was thinking, thanks for the confirmation.

#356Andres Freund
andres@anarazel.de
In reply to: Corey Huinker (#355)
Re: Statistics Import and Export

Hi,

On February 24, 2025 10:30:08 PM GMT+01:00, Corey Huinker <corey.huinker@gmail.com> wrote:

From what I can see, it doesn't. Moreover, the attstattarget array agg is
only done in version 11 and higher, and we need to go as far back as we've
got expression indexes.

I don't think we have to at all. It's perfectly reasonable to add a complicated feature like this only when upgrading from newer versions. I'd go even so far to say that it's a bad idea to support unsupported source versions, because it'll mean we'll practically get very very little testing for those paths but claim to support them.

Andres
--
Sent from my Android device with K-9 Mail. Please excuse my brevity.

#357Corey Huinker
corey.huinker@gmail.com
In reply to: Andres Freund (#356)
Re: Statistics Import and Export

On Mon, Feb 24, 2025 at 4:33 PM Andres Freund <andres@anarazel.de> wrote:

Hi,

On February 24, 2025 10:30:08 PM GMT+01:00, Corey Huinker <
corey.huinker@gmail.com> wrote:

From what I can see, it doesn't. Moreover, the attstattarget array agg is
only done in version 11 and higher, and we need to go as far back as we've
got expression indexes.

I don't think we have to at all. It's perfectly reasonable to add a
complicated feature like this only when upgrading from newer versions. I'd
go even so far to say that it's a bad idea to support unsupported source
versions, because it'll mean we'll practically get very very little testing
for those paths but claim to support them.

Anyone still on those versions has some serious barriers to doing an
upgrade, downtime probably being the largest of them. Any stats we don't
migrate here have to be analyzed later, which is more downtime or time in a
degraded state. I'd rather we try to make it easier for them to upgrade,
and in this case the risk is small because we're just collecting the
attname:attnum pairings for an index, and it's the same SQL that we'd use
for modern versions.

#358Daniel Gustafsson
daniel@yesql.se
In reply to: Andres Freund (#356)
Re: Statistics Import and Export

On 24 Feb 2025, at 22:33, Andres Freund <andres@anarazel.de> wrote:

I'd go even so far to say that it's a bad idea to support unsupported source versions, because it'll mean we'll practically get very very little testing for those paths but claim to support them.

+1. Maintaining pg_upgrade is hard enough without having to go rummaging
through the EOL-versions filing cabinet all too often when hacking on it.

--
Daniel Gustafsson

#359Nathan Bossart
nathandbossart@gmail.com
In reply to: Daniel Gustafsson (#358)
Re: Statistics Import and Export

On Mon, Feb 24, 2025 at 10:51:34PM +0100, Daniel Gustafsson wrote:

On 24 Feb 2025, at 22:33, Andres Freund <andres@anarazel.de> wrote:

I'd go even so far to say that it's a bad idea to support unsupported
source versions, because it'll mean we'll practically get very very
little testing for those paths but claim to support them.

+1. Maintaining pg_upgrade is hard enough without having to go rummaging
through the EOL-versions filing cabinet all too often when hacking on it.

+1. FWIW I'm planning to restrict one of my proposed pg_upgrade
optimizations to upgrades from v10 or newer. Sure, I could add a ton of
complexity to support older versions, but when v18 comes out, that
restriction will only apply to users who are still running versions that
have been out-of-support for nearly 4 years. I think it's completely
reasonable to try to help users on old versions, but I also think it's
reasonable for us as developers to prioritize maintainability and
stability. And I suspect it's unlikely that any specific pg_upgrade
feature is the reason folks haven't upgraded, although I'll admit the
downtime implications might certainly be a factor.

--
nathan

#360Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#355)
Re: Statistics Import and Export

On Mon, Feb 24, 2025 at 4:30 PM Corey Huinker <corey.huinker@gmail.com>
wrote:

After a bit of playing around, it seemed messy to make it into
a join, but we could replace the two array_agg sub-selects with
a single one:

(SELECT pg_catalog.array_agg(ROW(attname, attstattarget) ORDER BY attnum)
FROM pg_catalog.pg_attribute WHERE attrelid = i.indexrelid)

and then what we need could be pulled out of that, although
I'm not sure if pg_dump has logic at hand for deconstructing an
array of composite.

Digging some more on this, I see that we populate indxinfo[j].indkey as if
it's an array of Oids, but it's an array of AttrNumber/int2. Shouldn't have
caused any problems given that we're dealing with small integers, but it
didn't help discoverability just now. Is there some history there that I'm
not aware of?

#361Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#360)
Re: Statistics Import and Export

On Mon, 2025-02-24 at 17:26 -0500, Corey Huinker wrote:

Digging some more on this, I see that we populate indxinfo[j].indkey
as if it's an array of Oids,

It looks like it's done that way so that parseOidArray() can be used. A
hack, I suppose, but makes some sense to reuse that code.

We should probably add a comment somewhere, though.

Regards,
Jeff Davis

#362jian he
jian.universality@gmail.com
In reply to: Jeff Davis (#350)
Re: Statistics Import and Export

On Tue, Feb 25, 2025 at 5:01 AM Jeff Davis <pgsql@j-davis.com> wrote:

On Mon, 2025-02-24 at 15:53 -0500, Andres Freund wrote:

I don't think that's necessarily true, hot pruning might help some,
as afaict
the restore happens in multiple transactions.

Yeah, I just dumped and reloaded the regression database with and
without stats, and saw no difference in the resulting size. So it's
probably more correct to say "churn" rather than "bloat".

Even running "psql -1", I see modest bloat substantially less than 2x.

So if we agree that we don't mind a bit of churn and we will never need
this (despite what ANALYZE does), then I'm OK removing it. Which makes
me wonder why ANALYZE does it with inplace updates?

hi.
looking at commit:
https://git.postgresql.org/cgit/postgresql.git/commit/?id=f3dae2ae5856dec9935a51e53216400566ef8d4f

I am confused by this:
```
ctup = SearchSysCache1(RELOID, ObjectIdGetDatum(reloid));
if (!HeapTupleIsValid(ctup))
{
ereport(elevel,
(errcode(ERRCODE_OBJECT_IN_USE),
errmsg("pg_class entry for relid %u not found", reloid)));
table_close(crel, RowExclusiveLock);
return false;
}
```
First I thought ERRCODE_OBJECT_IN_USE was weird. maybe
ERRCODE_NO_DATA_FOUND would be more appropriate.

then but ``stats_lock_check_privileges(reloid);`` already proves there is
a pg_class entry for reloid.
maybe we can just use
elog(ERROR, "pg_class entry for relid %u not found", reloid)));

also in stats_lock_check_privileges.
check_inplace_rel_lock related comments should be removed?

#363Ashutosh Bapat
ashutosh.bapat.oss@gmail.com
In reply to: jian he (#362)
Re: Statistics Import and Export

Hi Jeff and Corey,

I think I have found a bug (arguably) with the dump/restore test I am
developing at [1]/messages/by-id/CAExHW5sBbMki6Xs4XxFQQF3C4Wx3wxkLAcySrtuW3vrnOxXDNQ@mail.gmail.com.

#create table t1 (a int primary key, b int);
CREATE TABLE
#insert into t1 values (1, 2);
INSERT 0 1

$ createdb rdb
$ pg_dump -d postgres | psql -d rdb

$ pg_dump -d postgres > /tmp/pgdb.out
ashutosh@localhost:~/work/units/pg_dump_test$ pg_dump -d rdb > /tmp/rdb.out
ashutosh@localhost:~/work/units/pg_dump_test$ diff /tmp/pgdb.out /tmp/rdb.out
52,53c52,53
< 'relpages', '0'::integer,
< 'reltuples', '-1'::real,
---

'relpages', '1'::integer,
'reltuples', '1'::real,

So the dumped statistics are not restored exactly. The reason for this
is the table statistics is dumped before dumping ALTER TABLE ... ADD
CONSTRAINT command which changes the statistics. I think all the
pg_restore_relation_stats() calls should be dumped after all the
schema and data modifications have been done. OR what's the point in
dumping statistics only to get rewritten even before restore finishes.

[1]: /messages/by-id/CAExHW5sBbMki6Xs4XxFQQF3C4Wx3wxkLAcySrtuW3vrnOxXDNQ@mail.gmail.com

--
Best Wishes,
Ashutosh Bapat

#364Jeff Davis
pgsql@j-davis.com
In reply to: jian he (#362)
Re: Statistics Import and Export

On Tue, 2025-02-25 at 11:30 +0800, jian he wrote:

maybe we can just use
elog(ERROR, "pg_class entry for relid %u not found", reloid)));

Thank you.

also in stats_lock_check_privileges.
check_inplace_rel_lock related comments should be removed?

In-place update locking rules still apply when updating pg_class or
pg_database even if the current caller is not performing an in-place
update. It might be better to point instead to
check_lock_if_inplace_updateable_rel()?

Regards,
Jeff Davis

#365Jeff Davis
pgsql@j-davis.com
In reply to: Tom Lane (#332)
1 attachment(s)
Re: Statistics Import and Export

On Mon, 2025-02-24 at 12:50 -0500, Tom Lane wrote:

Also, while working on the attached, I couldn't help forming the
opinion that we'd be better off to nuke pg_set_attribute_stats()
from orbit and require people to use pg_restore_attribute_stats().

Attached a patch to do so. The docs and tests required substantial
rework, but I think it's for the better now that we aren't trying to do
in-place updates.

Regards,
Jeff Davis

Attachments:

v1-0001-Remove-redundant-pg_set_-_stats-variants.patchtext/x-patch; charset=UTF-8; name=v1-0001-Remove-redundant-pg_set_-_stats-variants.patchDownload
From ea413ee48b10299530bafc3102395285b5ea8ce3 Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Mon, 24 Feb 2025 17:24:05 -0800
Subject: [PATCH v1] Remove redundant pg_set_*_stats() variants.

After commit f3dae2ae58, the primary purpose of separating the
pg_set_*_stats() from the pg_restore_*_stats() variants was
eliminated.

Leave pg_restore_relation_stats() and pg_restore_attribute_stats(),
which satisfy both purposes, and remove pg_set_relation_stats() and
pg_set_attribute_stats().

Discussion: https://postgr.es/m/1457469.1740419458@sss.pgh.pa.us
---
 doc/src/sgml/func.sgml                     | 254 +++----
 src/backend/catalog/system_functions.sql   |  32 -
 src/backend/statistics/attribute_stats.c   |  98 +--
 src/backend/statistics/relation_stats.c    |  24 +-
 src/backend/statistics/stat_utils.c        |  30 +-
 src/include/catalog/pg_proc.dat            |  14 -
 src/include/statistics/stat_utils.h        |   8 +-
 src/test/regress/expected/stats_import.out | 832 +--------------------
 src/test/regress/sql/stats_import.sql      | 648 +---------------
 9 files changed, 209 insertions(+), 1731 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index f0ccb751106..12206e0cfc6 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30180,66 +30180,6 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
      </thead>
 
      <tbody>
-      <row>
-       <entry role="func_table_entry">
-        <para role="func_signature">
-         <indexterm>
-          <primary>pg_set_relation_stats</primary>
-         </indexterm>
-         <function>pg_set_relation_stats</function> (
-         <parameter>relation</parameter> <type>regclass</type>
-         <optional>, <parameter>relpages</parameter> <type>integer</type></optional>
-         <optional>, <parameter>reltuples</parameter> <type>real</type></optional>
-         <optional>, <parameter>relallvisible</parameter> <type>integer</type></optional> )
-         <returnvalue>void</returnvalue>
-        </para>
-        <para>
-         Updates relation-level statistics for the given relation to the
-         specified values. The parameters correspond to columns in <link
-         linkend="catalog-pg-class"><structname>pg_class</structname></link>. Unspecified
-         or <literal>NULL</literal> values leave the setting unchanged.
-        </para>
-        <para>
-         Ordinarily, these statistics are collected automatically or updated
-         as a part of <xref linkend="sql-vacuum"/> or <xref
-         linkend="sql-analyze"/>, so it's not necessary to call this
-         function. However, it may be useful when testing the effects of
-         statistics on the planner to understand or anticipate plan changes.
-        </para>
-        <para>
-         The caller must have the <literal>MAINTAIN</literal> privilege on
-         the table or be the owner of the database.
-        </para>
-        <para>
-         The value of <structfield>relpages</structfield> must be greater than
-         or equal to <literal>-1</literal>,
-         <structfield>reltuples</structfield> must be greater than or equal to
-         <literal>-1.0</literal>, and <structfield>relallvisible</structfield>
-         must be greater than or equal to <literal>0</literal>.
-        </para>
-       </entry>
-      </row>
-
-      <row>
-       <entry role="func_table_entry">
-        <para role="func_signature">
-         <indexterm>
-          <primary>pg_clear_relation_stats</primary>
-         </indexterm>
-         <function>pg_clear_relation_stats</function> ( <parameter>relation</parameter> <type>regclass</type> )
-         <returnvalue>void</returnvalue>
-        </para>
-        <para>
-         Clears table-level statistics for the given relation, as though the
-         table was newly created.
-        </para>
-        <para>
-         The caller must have the <literal>MAINTAIN</literal> privilege on
-         the table or be the owner of the database.
-        </para>
-       </entry>
-      </row>
-
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
@@ -30248,26 +30188,25 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
         <function>pg_restore_relation_stats</function> (
         <literal>VARIADIC</literal> <parameter>kwargs</parameter> <type>"any"</type> )
         <returnvalue>boolean</returnvalue>
-        </para>
-        <para>
-         Similar to <function>pg_set_relation_stats()</function>, but intended
-         for bulk restore of relation statistics. The tracked statistics may
-         change from version to version, so the primary purpose of this
-         function is to maintain a consistent function signature to avoid
-         errors when restoring statistics from previous versions.
-        </para>
+       </para>
         <para>
-         Arguments are passed as pairs of <replaceable>argname</replaceable>
-         and <replaceable>argvalue</replaceable>, where
-         <replaceable>argname</replaceable> corresponds to a named argument in
-         <function>pg_set_relation_stats()</function> and
-         <replaceable>argvalue</replaceable> is of the corresponding type.
+         Updates table-level statistics.  Ordinarily, these statistics are
+         collected automatically or updated as a part of <xref
+         linkend="sql-vacuum"/> or <xref linkend="sql-analyze"/>, so it's not
+         necessary to call this function.  However, it is useful after a
+         restore to enable the optimizer to choose better plans if
+         <command>ANALYZE</command> has not been run yet.
         </para>
         <para>
-         Additionally, this function supports argument name
-         <literal>version</literal> of type <type>integer</type>, which
-         specifies the version from which the statistics originated, improving
-         interpretation of older statistics.
+         The tracked statistics may change from version to version, so
+         arguments are passed as pairs of <replaceable>argname</replaceable>
+         and <replaceable>argvalue</replaceable> in the form:
+<programlisting>
+ SELECT pg_restore_relation_stats(
+    '<replaceable>arg1name</replaceable>', '<replaceable>arg1value</replaceable>'::<replaceable>arg1type</replaceable>,
+    '<replaceable>arg2name</replaceable>', '<replaceable>arg2value</replaceable>'::<replaceable>arg2type</replaceable>,
+    '<replaceable>arg3name</replaceable>', '<replaceable>arg3value</replaceable>'::<replaceable>arg3type</replaceable>);
+</programlisting>
         </para>
         <para>
          For example, to set the <structname>relpages</structname> and
@@ -30277,62 +30216,37 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
  SELECT pg_restore_relation_stats(
     'relation',  'mytable'::regclass,
     'relpages',  173::integer,
-    'reltuples', 10000::float4);
+    'reltuples', 10000::real);
 </programlisting>
         </para>
         <para>
-         Minor errors are reported as a <literal>WARNING</literal> and
-         ignored, and remaining statistics will still be restored. If all
-         specified statistics are successfully restored, return
-         <literal>true</literal>, otherwise <literal>false</literal>.
-        </para>
-       </entry>
-      </row>
-
-      <row>
-       <entry role="func_table_entry">
-        <para role="func_signature">
-         <indexterm>
-          <primary>pg_set_attribute_stats</primary>
-         </indexterm>
-         <function>pg_set_attribute_stats</function> (
-         <parameter>relation</parameter> <type>regclass</type>,
-         <parameter>attname</parameter> <type>name</type>,
-         <parameter>inherited</parameter> <type>boolean</type>
-         <optional>, <parameter>null_frac</parameter> <type>real</type></optional>
-         <optional>, <parameter>avg_width</parameter> <type>integer</type></optional>
-         <optional>, <parameter>n_distinct</parameter> <type>real</type></optional>
-         <optional>, <parameter>most_common_vals</parameter> <type>text</type>, <parameter>most_common_freqs</parameter> <type>real[]</type> </optional>
-         <optional>, <parameter>histogram_bounds</parameter> <type>text</type> </optional>
-         <optional>, <parameter>correlation</parameter> <type>real</type> </optional>
-         <optional>, <parameter>most_common_elems</parameter> <type>text</type>, <parameter>most_common_elem_freqs</parameter> <type>real[]</type> </optional>
-         <optional>, <parameter>elem_count_histogram</parameter> <type>real[]</type> </optional>
-         <optional>, <parameter>range_length_histogram</parameter> <type>text</type> </optional>
-         <optional>, <parameter>range_empty_frac</parameter> <type>real</type> </optional>
-         <optional>, <parameter>range_bounds_histogram</parameter> <type>text</type> </optional> )
-         <returnvalue>void</returnvalue>
-        </para>
-        <para>
-         Creates or updates attribute-level statistics for the given relation
-         and attribute name to the specified values. The parameters correspond
-         to attributes of the same name found in the <link
-         linkend="view-pg-stats"><structname>pg_stats</structname></link>
-         view.
+         The argument <literal>relation</literal> with a value of type
+         <type>regclass</type> is required, and specifies the table. Other
+         arguments are the names of statistics corresponding to certain
+         columns in <link
+         linkend="catalog-pg-class"><structname>pg_class</structname></link>.
+         The currently-supported relation statistics are
+         <literal>relpages</literal> with a value of type
+         <type>integer</type>, <literal>reltuples</literal> with a value of
+         type <type>real</type>, and <literal>relallvisible</literal> with a
+         value of type <type>integer</type>.
         </para>
         <para>
-         Optional parameters default to <literal>NULL</literal>, which leave
-         the corresponding statistic unchanged.
+         Additionally, this function supports argument name
+         <literal>version</literal> of type <type>integer</type>, which
+         specifies the version from which the statistics originated, improving
+         interpretation of statistics from older versions of
+         <productname>PostgreSQL</productname>.
         </para>
         <para>
-         Ordinarily, these statistics are collected automatically or updated
-         as a part of <xref linkend="sql-vacuum"/> or <xref
-         linkend="sql-analyze"/>, so it's not necessary to call this
-         function. However, it may be useful when testing the effects of
-         statistics on the planner to understand or anticipate plan changes.
+         Minor errors are reported as a <literal>WARNING</literal> and
+         ignored, and remaining statistics will still be restored. If all
+         specified statistics are successfully restored, return
+         <literal>true</literal>, otherwise <literal>false</literal>.
         </para>
         <para>
-         The caller must have the <literal>MAINTAIN</literal> privilege on
-         the table or be the owner of the database.
+         The caller must have the <literal>MAINTAIN</literal> privilege on the
+         table or be the owner of the database.
         </para>
        </entry>
       </row>
@@ -30341,21 +30255,18 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        <entry role="func_table_entry">
         <para role="func_signature">
          <indexterm>
-          <primary>pg_clear_attribute_stats</primary>
+          <primary>pg_clear_relation_stats</primary>
          </indexterm>
-         <function>pg_clear_attribute_stats</function> (
-         <parameter>relation</parameter> <type>regclass</type>,
-         <parameter>attname</parameter> <type>name</type>,
-         <parameter>inherited</parameter> <type>boolean</type> )
+         <function>pg_clear_relation_stats</function> ( <parameter>relation</parameter> <type>regclass</type> )
          <returnvalue>void</returnvalue>
         </para>
         <para>
-         Clears table-level statistics for the given relation attribute, as
-         though the table was newly created.
+         Clears table-level statistics for the given relation, as though the
+         table was newly created.
         </para>
         <para>
-         The caller must have the <literal>MAINTAIN</literal> privilege on
-         the table or be the owner of the database.
+         The caller must have the <literal>MAINTAIN</literal> privilege on the
+         table or be the owner of the database.
         </para>
        </entry>
       </row>
@@ -30368,26 +30279,25 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
         <function>pg_restore_attribute_stats</function> (
         <literal>VARIADIC</literal> <parameter>kwargs</parameter> <type>"any"</type> )
         <returnvalue>boolean</returnvalue>
-        </para>
-        <para>
-         Similar to <function>pg_set_attribute_stats()</function>, but
-         intended for bulk restore of attribute statistics. The tracked
-         statistics may change from version to version, so the primary purpose
-         of this function is to maintain a consistent function signature to
-         avoid errors when restoring statistics from previous versions.
-        </para>
+       </para>
         <para>
-         Arguments are passed as pairs of <replaceable>argname</replaceable>
-         and <replaceable>argvalue</replaceable>, where
-         <replaceable>argname</replaceable> corresponds to a named argument in
-         <function>pg_set_attribute_stats()</function> and
-         <replaceable>argvalue</replaceable> is of the corresponding type.
+         Create or update column-level statistics.  Ordinarily, these
+         statistics are collected automatically or updated as a part of <xref
+         linkend="sql-vacuum"/> or <xref linkend="sql-analyze"/>, so it's not
+         necessary to call this function.  However, it is useful after a
+         restore to enable the optimizer to choose better plans if
+         <command>ANALYZE</command> has not been run yet.
         </para>
         <para>
-         Additionally, this function supports argument name
-         <literal>version</literal> of type <type>integer</type>, which
-         specifies the version from which the statistics originated, improving
-         interpretation of older statistics.
+         The tracked statistics may change from version to version, so
+         arguments are passed as pairs of <replaceable>argname</replaceable>
+         and <replaceable>argvalue</replaceable> in the form:
+<programlisting>
+ SELECT pg_restore_attribute_stats(
+    '<replaceable>arg1name</replaceable>', '<replaceable>arg1value</replaceable>'::<replaceable>arg1type</replaceable>,
+    '<replaceable>arg2name</replaceable>', '<replaceable>arg2value</replaceable>'::<replaceable>arg2type</replaceable>,
+    '<replaceable>arg3name</replaceable>', '<replaceable>arg3value</replaceable>'::<replaceable>arg3type</replaceable>);
+</programlisting>
         </para>
         <para>
          For example, to set the <structname>avg_width</structname> and
@@ -30403,12 +30313,56 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
     'null_frac',   0.5::real);
 </programlisting>
         </para>
+        <para>
+         The required arguments are <literal>relation</literal> with a value
+         of type <type>regclass</type>, which specifies the table;
+         <literal>attname</literal> with a value of type <type>name</type>,
+         which specifies the column; and <literal>inherited</literal>, which
+         specifies whether the statistics includes values from child tables.
+         Other arguments are the names of statistics corresponding to columns
+         in <link
+         linkend="view-pg-stats"><structname>pg_stats</structname></link>.
+        </para>
+        <para>
+         Additionally, this function supports argument name
+         <literal>version</literal> of type <type>integer</type>, which
+         specifies the version from which the statistics originated, improving
+         interpretation of statistics from older versions of
+         <productname>PostgreSQL</productname>.
+        </para>
         <para>
          Minor errors are reported as a <literal>WARNING</literal> and
          ignored, and remaining statistics will still be restored. If all
          specified statistics are successfully restored, return
          <literal>true</literal>, otherwise <literal>false</literal>.
         </para>
+        <para>
+         The caller must have the <literal>MAINTAIN</literal> privilege on the
+         table or be the owner of the database.
+        </para>
+       </entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry">
+        <para role="func_signature">
+         <indexterm>
+          <primary>pg_clear_attribute_stats</primary>
+         </indexterm>
+         <function>pg_clear_attribute_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>attname</parameter> <type>name</type>,
+         <parameter>inherited</parameter> <type>boolean</type> )
+         <returnvalue>void</returnvalue>
+        </para>
+        <para>
+         Clears column-level statistics for the given relation and
+         attribute, as though the table was newly created.
+        </para>
+        <para>
+         The caller must have the <literal>MAINTAIN</literal> privilege on
+         the table or be the owner of the database.
+        </para>
        </entry>
       </row>
      </tbody>
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 591157b1d1b..86888cd3201 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -636,38 +636,6 @@ LANGUAGE INTERNAL
 CALLED ON NULL INPUT VOLATILE PARALLEL SAFE
 AS 'pg_stat_reset_slru';
 
-CREATE OR REPLACE FUNCTION
-  pg_set_relation_stats(relation regclass,
-                        relpages integer DEFAULT NULL,
-                        reltuples real DEFAULT NULL,
-                        relallvisible integer DEFAULT NULL)
-RETURNS void
-LANGUAGE INTERNAL
-CALLED ON NULL INPUT VOLATILE
-AS 'pg_set_relation_stats';
-
-CREATE OR REPLACE FUNCTION
-  pg_set_attribute_stats(relation regclass,
-                         attname name,
-                         inherited bool,
-                         null_frac real DEFAULT NULL,
-                         avg_width integer DEFAULT NULL,
-                         n_distinct real DEFAULT NULL,
-                         most_common_vals text DEFAULT NULL,
-                         most_common_freqs real[] DEFAULT NULL,
-                         histogram_bounds text DEFAULT NULL,
-                         correlation real DEFAULT NULL,
-                         most_common_elems text DEFAULT NULL,
-                         most_common_elem_freqs real[] DEFAULT NULL,
-                         elem_count_histogram real[] DEFAULT NULL,
-                         range_length_histogram text DEFAULT NULL,
-                         range_empty_frac real DEFAULT NULL,
-                         range_bounds_histogram text DEFAULT NULL)
-RETURNS void
-LANGUAGE INTERNAL
-CALLED ON NULL INPUT VOLATILE
-AS 'pg_set_attribute_stats';
-
 --
 -- The default permissions for functions mean that anyone can execute them.
 -- A number of functions shouldn't be executable by just anyone, but rather
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index c0c398a4bb2..66a5676c810 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -76,16 +76,16 @@ static struct StatsArgInfo attarginfo[] =
 	[NUM_ATTRIBUTE_STATS_ARGS] = {0}
 };
 
-static bool attribute_statistics_update(FunctionCallInfo fcinfo, int elevel);
+static bool attribute_statistics_update(FunctionCallInfo fcinfo);
 static Node *get_attr_expr(Relation rel, int attnum);
-static void get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
+static void get_attr_stat_type(Oid reloid, AttrNumber attnum,
 							   Oid *atttypid, int32 *atttypmod,
 							   char *atttyptype, Oid *atttypcoll,
 							   Oid *eq_opr, Oid *lt_opr);
-static bool get_elem_stat_type(Oid atttypid, char atttyptype, int elevel,
+static bool get_elem_stat_type(Oid atttypid, char atttyptype,
 							   Oid *elemtypid, Oid *elem_eq_opr);
 static Datum text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d,
-							   Oid typid, int32 typmod, int elevel, bool *ok);
+							   Oid typid, int32 typmod, bool *ok);
 static void set_stats_slot(Datum *values, bool *nulls, bool *replaces,
 						   int16 stakind, Oid staop, Oid stacoll,
 						   Datum stanumbers, bool stanumbers_isnull,
@@ -109,11 +109,11 @@ static void init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
  *
  * Major errors, such as the table not existing, the attribute not existing,
  * or a permissions failure are always reported at ERROR. Other errors, such
- * as a conversion failure on one statistic kind, are reported at 'elevel',
+ * as a conversion failure on one statistic kind, are reported as a WARNING
  * and other statistic kinds may still be updated.
  */
 static bool
-attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
+attribute_statistics_update(FunctionCallInfo fcinfo)
 {
 	Oid			reloid;
 	Name		attname;
@@ -184,33 +184,29 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	inherited = PG_GETARG_BOOL(INHERITED_ARG);
 
 	/*
-	 * Check argument sanity. If some arguments are unusable, emit at elevel
+	 * Check argument sanity. If some arguments are unusable, emit a WARNING
 	 * and set the corresponding argument to NULL in fcinfo.
 	 */
 
-	if (!stats_check_arg_array(fcinfo, attarginfo, MOST_COMMON_FREQS_ARG,
-							   elevel))
+	if (!stats_check_arg_array(fcinfo, attarginfo, MOST_COMMON_FREQS_ARG))
 	{
 		do_mcv = false;
 		result = false;
 	}
 
-	if (!stats_check_arg_array(fcinfo, attarginfo, MOST_COMMON_ELEM_FREQS_ARG,
-							   elevel))
+	if (!stats_check_arg_array(fcinfo, attarginfo, MOST_COMMON_ELEM_FREQS_ARG))
 	{
 		do_mcelem = false;
 		result = false;
 	}
-	if (!stats_check_arg_array(fcinfo, attarginfo, ELEM_COUNT_HISTOGRAM_ARG,
-							   elevel))
+	if (!stats_check_arg_array(fcinfo, attarginfo, ELEM_COUNT_HISTOGRAM_ARG))
 	{
 		do_dechist = false;
 		result = false;
 	}
 
 	if (!stats_check_arg_pair(fcinfo, attarginfo,
-							  MOST_COMMON_VALS_ARG, MOST_COMMON_FREQS_ARG,
-							  elevel))
+							  MOST_COMMON_VALS_ARG, MOST_COMMON_FREQS_ARG))
 	{
 		do_mcv = false;
 		result = false;
@@ -218,7 +214,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 
 	if (!stats_check_arg_pair(fcinfo, attarginfo,
 							  MOST_COMMON_ELEMS_ARG,
-							  MOST_COMMON_ELEM_FREQS_ARG, elevel))
+							  MOST_COMMON_ELEM_FREQS_ARG))
 	{
 		do_mcelem = false;
 		result = false;
@@ -226,14 +222,14 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 
 	if (!stats_check_arg_pair(fcinfo, attarginfo,
 							  RANGE_LENGTH_HISTOGRAM_ARG,
-							  RANGE_EMPTY_FRAC_ARG, elevel))
+							  RANGE_EMPTY_FRAC_ARG))
 	{
 		do_range_length_histogram = false;
 		result = false;
 	}
 
 	/* derive information from attribute */
-	get_attr_stat_type(reloid, attnum, elevel,
+	get_attr_stat_type(reloid, attnum,
 					   &atttypid, &atttypmod,
 					   &atttyptype, &atttypcoll,
 					   &eq_opr, &lt_opr);
@@ -241,10 +237,10 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	/* if needed, derive element type */
 	if (do_mcelem || do_dechist)
 	{
-		if (!get_elem_stat_type(atttypid, atttyptype, elevel,
+		if (!get_elem_stat_type(atttypid, atttyptype,
 								&elemtypid, &elem_eq_opr))
 		{
-			ereport(elevel,
+			ereport(WARNING,
 					(errmsg("unable to determine element type of attribute \"%s\"", NameStr(*attname)),
 					 errdetail("Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.")));
 			elemtypid = InvalidOid;
@@ -259,7 +255,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	/* histogram and correlation require less-than operator */
 	if ((do_histogram || do_correlation) && !OidIsValid(lt_opr))
 	{
-		ereport(elevel,
+		ereport(WARNING,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("could not determine less-than operator for attribute \"%s\"", NameStr(*attname)),
 				 errdetail("Cannot set STATISTIC_KIND_HISTOGRAM or STATISTIC_KIND_CORRELATION.")));
@@ -273,7 +269,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	if ((do_range_length_histogram || do_bounds_histogram) &&
 		!(atttyptype == TYPTYPE_RANGE || atttyptype == TYPTYPE_MULTIRANGE))
 	{
-		ereport(elevel,
+		ereport(WARNING,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("attribute \"%s\" is not a range type", NameStr(*attname)),
 				 errdetail("Cannot set STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM or STATISTIC_KIND_BOUNDS_HISTOGRAM.")));
@@ -322,7 +318,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 												  &array_in_fn,
 												  PG_GETARG_DATUM(MOST_COMMON_VALS_ARG),
 												  atttypid, atttypmod,
-												  elevel, &converted);
+												  &converted);
 
 		if (converted)
 		{
@@ -344,7 +340,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 		stavalues = text_to_stavalues("histogram_bounds",
 									  &array_in_fn,
 									  PG_GETARG_DATUM(HISTOGRAM_BOUNDS_ARG),
-									  atttypid, atttypmod, elevel,
+									  atttypid, atttypmod,
 									  &converted);
 
 		if (converted)
@@ -382,7 +378,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 									  &array_in_fn,
 									  PG_GETARG_DATUM(MOST_COMMON_ELEMS_ARG),
 									  elemtypid, atttypmod,
-									  elevel, &converted);
+									  &converted);
 
 		if (converted)
 		{
@@ -422,7 +418,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 									  &array_in_fn,
 									  PG_GETARG_DATUM(RANGE_BOUNDS_HISTOGRAM_ARG),
 									  atttypid, atttypmod,
-									  elevel, &converted);
+									  &converted);
 
 		if (converted)
 		{
@@ -449,7 +445,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 		stavalues = text_to_stavalues("range_length_histogram",
 									  &array_in_fn,
 									  PG_GETARG_DATUM(RANGE_LENGTH_HISTOGRAM_ARG),
-									  FLOAT8OID, 0, elevel, &converted);
+									  FLOAT8OID, 0, &converted);
 
 		if (converted)
 		{
@@ -517,7 +513,7 @@ get_attr_expr(Relation rel, int attnum)
  * Derive type information from the attribute.
  */
 static void
-get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
+get_attr_stat_type(Oid reloid, AttrNumber attnum,
 				   Oid *atttypid, int32 *atttypmod,
 				   char *atttyptype, Oid *atttypcoll,
 				   Oid *eq_opr, Oid *lt_opr)
@@ -599,7 +595,7 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
  * Derive element type information from the attribute type.
  */
 static bool
-get_elem_stat_type(Oid atttypid, char atttyptype, int elevel,
+get_elem_stat_type(Oid atttypid, char atttyptype,
 				   Oid *elemtypid, Oid *elem_eq_opr)
 {
 	TypeCacheEntry *elemtypcache;
@@ -634,13 +630,13 @@ get_elem_stat_type(Oid atttypid, char atttyptype, int elevel,
 /*
  * Cast a text datum into an array with element type elemtypid.
  *
- * If an error is encountered, capture it and re-throw at elevel, and set ok
- * to false. If the resulting array contains NULLs, raise an error at elevel
- * and set ok to false. Otherwise, set ok to true.
+ * If an error is encountered, capture it and re-throw a WARNING, and set ok
+ * to false. If the resulting array contains NULLs, raise a WARNING and set ok
+ * to false. Otherwise, set ok to true.
  */
 static Datum
 text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid typid,
-				  int32 typmod, int elevel, bool *ok)
+				  int32 typmod, bool *ok)
 {
 	LOCAL_FCINFO(fcinfo, 8);
 	char	   *s;
@@ -667,8 +663,7 @@ text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid typid,
 
 	if (escontext.error_occurred)
 	{
-		if (elevel != ERROR)
-			escontext.error_data->elevel = elevel;
+		escontext.error_data->elevel = WARNING;
 		ThrowErrorData(escontext.error_data);
 		*ok = false;
 		return (Datum) 0;
@@ -676,7 +671,7 @@ text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid typid,
 
 	if (array_contains_nulls(DatumGetArrayTypeP(result)))
 	{
-		ereport(elevel,
+		ereport(WARNING,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("\"%s\" array cannot contain NULL values", staname)));
 		*ok = false;
@@ -851,33 +846,6 @@ init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
 	}
 }
 
-/*
- * Import statistics for a given relation attribute.
- *
- * Inserts or replaces a row in pg_statistic for the given relation and
- * attribute name. It takes input parameters that correspond to columns in the
- * view pg_stats.
- *
- * Parameters null_frac, avg_width, and n_distinct all correspond to NOT NULL
- * columns in pg_statistic. The remaining parameters all belong to a specific
- * stakind. Some stakinds require multiple parameters, which must be specified
- * together (or neither specified).
- *
- * Parameters are only superficially validated. Omitting a parameter or
- * passing NULL leaves the statistic unchanged.
- *
- * Parameters corresponding to ANYARRAY columns are instead passed in as text
- * values, which is a valid input string for an array of the type or element
- * type of the attribute. Any error generated by the array_in() function will
- * in turn fail the function.
- */
-Datum
-pg_set_attribute_stats(PG_FUNCTION_ARGS)
-{
-	attribute_statistics_update(fcinfo, ERROR);
-	PG_RETURN_VOID();
-}
-
 /*
  * Delete statistics for the given attribute.
  */
@@ -933,10 +901,10 @@ pg_restore_attribute_stats(PG_FUNCTION_ARGS)
 							 InvalidOid, NULL, NULL);
 
 	if (!stats_fill_fcinfo_from_arg_pairs(fcinfo, positional_fcinfo,
-										  attarginfo, WARNING))
+										  attarginfo))
 		result = false;
 
-	if (!attribute_statistics_update(positional_fcinfo, WARNING))
+	if (!attribute_statistics_update(positional_fcinfo))
 		result = false;
 
 	PG_RETURN_BOOL(result);
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index 66731290a3e..11b1ef2dbc2 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -48,13 +48,13 @@ static struct StatsArgInfo relarginfo[] =
 	[NUM_RELATION_STATS_ARGS] = {0}
 };
 
-static bool relation_statistics_update(FunctionCallInfo fcinfo, int elevel);
+static bool relation_statistics_update(FunctionCallInfo fcinfo);
 
 /*
  * Internal function for modifying statistics for a relation.
  */
 static bool
-relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
+relation_statistics_update(FunctionCallInfo fcinfo)
 {
 	bool		result = true;
 	Oid			reloid;
@@ -83,7 +83,7 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 		reltuples = PG_GETARG_FLOAT4(RELTUPLES_ARG);
 		if (reltuples < -1.0)
 		{
-			ereport(elevel,
+			ereport(WARNING,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("reltuples cannot be < -1.0")));
 			result = false;
@@ -118,7 +118,7 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	ctup = SearchSysCache1(RELOID, ObjectIdGetDatum(reloid));
 	if (!HeapTupleIsValid(ctup))
 	{
-		ereport(elevel,
+		ereport(WARNING,
 				(errcode(ERRCODE_OBJECT_IN_USE),
 				 errmsg("pg_class entry for relid %u not found", reloid)));
 		table_close(crel, RowExclusiveLock);
@@ -169,16 +169,6 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	return result;
 }
 
-/*
- * Set statistics for a given pg_class entry.
- */
-Datum
-pg_set_relation_stats(PG_FUNCTION_ARGS)
-{
-	relation_statistics_update(fcinfo, ERROR);
-	PG_RETURN_VOID();
-}
-
 /*
  * Clear statistics for a given pg_class entry; that is, set back to initial
  * stats for a newly-created table.
@@ -199,7 +189,7 @@ pg_clear_relation_stats(PG_FUNCTION_ARGS)
 	newfcinfo->args[3].value = UInt32GetDatum(0);
 	newfcinfo->args[3].isnull = false;
 
-	relation_statistics_update(newfcinfo, ERROR);
+	relation_statistics_update(newfcinfo);
 	PG_RETURN_VOID();
 }
 
@@ -214,10 +204,10 @@ pg_restore_relation_stats(PG_FUNCTION_ARGS)
 							 InvalidOid, NULL, NULL);
 
 	if (!stats_fill_fcinfo_from_arg_pairs(fcinfo, positional_fcinfo,
-										  relarginfo, WARNING))
+										  relarginfo))
 		result = false;
 
-	if (!relation_statistics_update(positional_fcinfo, WARNING))
+	if (!relation_statistics_update(positional_fcinfo))
 		result = false;
 
 	PG_RETURN_BOOL(result);
diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c
index e70ea1ce738..54ead90b5bb 100644
--- a/src/backend/statistics/stat_utils.c
+++ b/src/backend/statistics/stat_utils.c
@@ -48,13 +48,13 @@ stats_check_required_arg(FunctionCallInfo fcinfo,
  * Check that argument is either NULL or a one dimensional array with no
  * NULLs.
  *
- * If a problem is found, emit at elevel, and return false. Otherwise return
+ * If a problem is found, emit a WARNING, and return false. Otherwise return
  * true.
  */
 bool
 stats_check_arg_array(FunctionCallInfo fcinfo,
 					  struct StatsArgInfo *arginfo,
-					  int argnum, int elevel)
+					  int argnum)
 {
 	ArrayType  *arr;
 
@@ -65,7 +65,7 @@ stats_check_arg_array(FunctionCallInfo fcinfo,
 
 	if (ARR_NDIM(arr) != 1)
 	{
-		ereport(elevel,
+		ereport(WARNING,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("\"%s\" cannot be a multidimensional array",
 						arginfo[argnum].argname)));
@@ -74,7 +74,7 @@ stats_check_arg_array(FunctionCallInfo fcinfo,
 
 	if (array_contains_nulls(arr))
 	{
-		ereport(elevel,
+		ereport(WARNING,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("\"%s\" array cannot contain NULL values",
 						arginfo[argnum].argname)));
@@ -89,13 +89,13 @@ stats_check_arg_array(FunctionCallInfo fcinfo,
  * a particular stakind, such as most_common_vals and most_common_freqs for
  * STATISTIC_KIND_MCV.
  *
- * If a problem is found, emit at elevel, and return false. Otherwise return
+ * If a problem is found, emit a WARNING, and return false. Otherwise return
  * true.
  */
 bool
 stats_check_arg_pair(FunctionCallInfo fcinfo,
 					 struct StatsArgInfo *arginfo,
-					 int argnum1, int argnum2, int elevel)
+					 int argnum1, int argnum2)
 {
 	if (PG_ARGISNULL(argnum1) && PG_ARGISNULL(argnum2))
 		return true;
@@ -105,7 +105,7 @@ stats_check_arg_pair(FunctionCallInfo fcinfo,
 		int			nullarg = PG_ARGISNULL(argnum1) ? argnum1 : argnum2;
 		int			otherarg = PG_ARGISNULL(argnum1) ? argnum2 : argnum1;
 
-		ereport(elevel,
+		ereport(WARNING,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("\"%s\" must be specified when \"%s\" is specified",
 						arginfo[nullarg].argname,
@@ -216,7 +216,7 @@ stats_lock_check_privileges(Oid reloid)
  * found.
  */
 static int
-get_arg_by_name(const char *argname, struct StatsArgInfo *arginfo, int elevel)
+get_arg_by_name(const char *argname, struct StatsArgInfo *arginfo)
 {
 	int			argnum;
 
@@ -224,7 +224,7 @@ get_arg_by_name(const char *argname, struct StatsArgInfo *arginfo, int elevel)
 		if (pg_strcasecmp(argname, arginfo[argnum].argname) == 0)
 			return argnum;
 
-	ereport(elevel,
+	ereport(WARNING,
 			(errmsg("unrecognized argument name: \"%s\"", argname)));
 
 	return -1;
@@ -234,11 +234,11 @@ get_arg_by_name(const char *argname, struct StatsArgInfo *arginfo, int elevel)
  * Ensure that a given argument matched the expected type.
  */
 static bool
-stats_check_arg_type(const char *argname, Oid argtype, Oid expectedtype, int elevel)
+stats_check_arg_type(const char *argname, Oid argtype, Oid expectedtype)
 {
 	if (argtype != expectedtype)
 	{
-		ereport(elevel,
+		ereport(WARNING,
 				(errmsg("argument \"%s\" has type \"%s\", expected type \"%s\"",
 						argname, format_type_be(argtype),
 						format_type_be(expectedtype))));
@@ -260,8 +260,7 @@ stats_check_arg_type(const char *argname, Oid argtype, Oid expectedtype, int ele
 bool
 stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
 								 FunctionCallInfo positional_fcinfo,
-								 struct StatsArgInfo *arginfo,
-								 int elevel)
+								 struct StatsArgInfo *arginfo)
 {
 	Datum	   *args;
 	bool	   *argnulls;
@@ -319,11 +318,10 @@ stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
 		if (pg_strcasecmp(argname, "version") == 0)
 			continue;
 
-		argnum = get_arg_by_name(argname, arginfo, elevel);
+		argnum = get_arg_by_name(argname, arginfo);
 
 		if (argnum < 0 || !stats_check_arg_type(argname, types[i + 1],
-												arginfo[argnum].argtype,
-												elevel))
+												arginfo[argnum].argtype))
 		{
 			result = false;
 			continue;
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index af9546de23d..9f0c676e22d 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12429,13 +12429,6 @@
   proargnames => '{kwargs}',
   proargmodes => '{v}',
   prosrc => 'pg_restore_attribute_stats' },
-{ oid => '9162',
-  descr => 'set statistics on attribute',
-  proname => 'pg_set_attribute_stats', provolatile => 'v', proisstrict => 'f',
-  proparallel => 'u', prorettype => 'void',
-  proargtypes => 'regclass name bool float4 int4 float4 text _float4 text float4 text _float4 _float4 text float4 text',
-  proargnames => '{relation,attname,inherited,null_frac,avg_width,n_distinct,most_common_vals,most_common_freqs,histogram_bounds,correlation,most_common_elems,most_common_elem_freqs,elem_count_histogram,range_length_histogram,range_empty_frac,range_bounds_histogram}',
-  prosrc => 'pg_set_attribute_stats' },
 { oid => '9163',
   descr => 'clear statistics on attribute',
   proname => 'pg_clear_attribute_stats', provolatile => 'v', proisstrict => 'f',
@@ -12443,13 +12436,6 @@
   proargtypes => 'regclass name bool',
   proargnames => '{relation,attname,inherited}',
   prosrc => 'pg_clear_attribute_stats' },
-{ oid => '9944',
-  descr => 'set statistics on relation',
-  proname => 'pg_set_relation_stats', provolatile => 'v', proisstrict => 'f',
-  proparallel => 'u', prorettype => 'void',
-  proargtypes => 'regclass int4 float4 int4',
-  proargnames => '{relation,relpages,reltuples,relallvisible}',
-  prosrc => 'pg_set_relation_stats' },
 { oid => '9945',
   descr => 'clear statistics on relation',
   proname => 'pg_clear_relation_stats', provolatile => 'v', proisstrict => 'f',
diff --git a/src/include/statistics/stat_utils.h b/src/include/statistics/stat_utils.h
index 6edb5ea0321..0eb4decfcac 100644
--- a/src/include/statistics/stat_utils.h
+++ b/src/include/statistics/stat_utils.h
@@ -25,17 +25,15 @@ extern void stats_check_required_arg(FunctionCallInfo fcinfo,
 									 struct StatsArgInfo *arginfo,
 									 int argnum);
 extern bool stats_check_arg_array(FunctionCallInfo fcinfo,
-								  struct StatsArgInfo *arginfo, int argnum,
-								  int elevel);
+								  struct StatsArgInfo *arginfo, int argnum);
 extern bool stats_check_arg_pair(FunctionCallInfo fcinfo,
 								 struct StatsArgInfo *arginfo,
-								 int argnum1, int argnum2, int elevel);
+								 int argnum1, int argnum2);
 
 extern void stats_lock_check_privileges(Oid reloid);
 
 extern bool stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
 											 FunctionCallInfo positional_fcinfo,
-											 struct StatsArgInfo *arginfo,
-											 int elevel);
+											 struct StatsArgInfo *arginfo);
 
 #endif							/* STATS_UTILS_H */
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index d6713eacc2c..7c7784efaf1 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -12,6 +12,7 @@ CREATE TABLE stats_import.test(
     arange int4range,
     tags text[]
 ) WITH (autovacuum_enabled = false);
+CREATE INDEX test_i ON stats_import.test(id);
 -- starting stats
 SELECT relpages, reltuples, relallvisible
 FROM pg_class
@@ -21,80 +22,15 @@ WHERE oid = 'stats_import.test'::regclass;
         0 |        -1 |             0
 (1 row)
 
--- error: regclass not found
-SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 0::Oid,
-        relpages => 17::integer,
-        reltuples => 400.0::real,
-        relallvisible => 4::integer);
-ERROR:  could not open relation with OID 0
--- relpages default
-SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.test'::regclass,
-        relpages => NULL::integer,
-        reltuples => 400.0::real,
-        relallvisible => 4::integer);
- pg_set_relation_stats 
------------------------
- 
-(1 row)
-
--- reltuples default
-SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.test'::regclass,
-        relpages => 17::integer,
-        reltuples => NULL::real,
-        relallvisible => 4::integer);
- pg_set_relation_stats 
------------------------
- 
-(1 row)
-
--- relallvisible default
-SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.test'::regclass,
-        relpages => 17::integer,
-        reltuples => 400.0::real,
-        relallvisible => NULL::integer);
- pg_set_relation_stats 
------------------------
- 
-(1 row)
-
--- named arguments
-SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.test'::regclass,
-        relpages => 17::integer,
-        reltuples => 400.0::real,
-        relallvisible => 4::integer);
- pg_set_relation_stats 
------------------------
- 
-(1 row)
-
-SELECT relpages, reltuples, relallvisible
-FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
- relpages | reltuples | relallvisible 
-----------+-----------+---------------
-       17 |       400 |             4
-(1 row)
-
-CREATE INDEX test_i ON stats_import.test(id);
 BEGIN;
 -- regular indexes have special case locking rules
 SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.test_i'::regclass,
-        relpages => 18::integer);
- pg_set_relation_stats 
------------------------
- 
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.test_i'::regclass,
+        'relpages', 18::integer);
+ pg_restore_relation_stats 
+---------------------------
+ t
 (1 row)
 
 SELECT mode FROM pg_locks
@@ -123,34 +59,6 @@ SELECT
  t
 (1 row)
 
--- positional arguments
-SELECT
-    pg_catalog.pg_set_relation_stats(
-        'stats_import.test'::regclass,
-        18::integer,
-        401.0::real,
-        5::integer);
- pg_set_relation_stats 
------------------------
- 
-(1 row)
-
-SELECT relpages, reltuples, relallvisible
-FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
- relpages | reltuples | relallvisible 
-----------+-----------+---------------
-       18 |       401 |             5
-(1 row)
-
-SELECT relpages, reltuples, relallvisible
-FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
- relpages | reltuples | relallvisible 
-----------+-----------+---------------
-       18 |       401 |             5
-(1 row)
-
 -- clear
 SELECT
     pg_catalog.pg_clear_relation_stats(
@@ -200,21 +108,21 @@ WHERE oid = 'stats_import.part_parent'::regclass;
 -- although partitioned tables have no storage, setting relpages to a
 -- positive value is still allowed
 SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.part_parent_i'::regclass,
-        relpages => 2::integer);
- pg_set_relation_stats 
------------------------
- 
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.part_parent_i'::regclass,
+        'relpages', 2::integer);
+ pg_restore_relation_stats 
+---------------------------
+ t
 (1 row)
 
 SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.part_parent'::regclass,
-        relpages => 2::integer);
- pg_set_relation_stats 
------------------------
- 
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.part_parent'::regclass,
+        'relpages', 2::integer);
+ pg_restore_relation_stats 
+---------------------------
+ t
 (1 row)
 
 --
@@ -225,12 +133,12 @@ SELECT
 --
 BEGIN;
 SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.part_parent_i'::regclass,
-        relpages => 2::integer);
- pg_set_relation_stats 
------------------------
- 
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.part_parent_i'::regclass,
+        'relpages', 2::integer);
+ pg_restore_relation_stats 
+---------------------------
+ t
 (1 row)
 
 SELECT mode FROM pg_locks
@@ -261,56 +169,14 @@ SELECT
 
 -- nothing stops us from setting it to -1
 SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.part_parent'::regclass,
-        relpages => -1::integer);
- pg_set_relation_stats 
------------------------
- 
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.part_parent'::regclass,
+        'relpages', -1::integer);
+ pg_restore_relation_stats 
+---------------------------
+ t
 (1 row)
 
--- error: object doesn't exist
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => '0'::oid,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.1::real,
-    avg_width => 2::integer,
-    n_distinct => 0.3::real);
-ERROR:  could not open relation with OID 0
--- error: object doesn't exist
-SELECT pg_catalog.pg_clear_attribute_stats(
-    relation => '0'::oid,
-    attname => 'id'::name,
-    inherited => false::boolean);
-ERROR:  could not open relation with OID 0
--- error: relation null
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => NULL::oid,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.1::real,
-    avg_width => 2::integer,
-    n_distinct => 0.3::real);
-ERROR:  "relation" cannot be NULL
--- error: attribute is system column
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'xmin'::name,
-    inherited => false::boolean,
-    null_frac => 0.1::real,
-    avg_width => 2::integer,
-    n_distinct => 0.3::real);
-ERROR:  cannot modify statistics on system column "xmin"
--- error: attname doesn't exist
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'nope'::name,
-    inherited => false::boolean,
-    null_frac => 0.1::real,
-    avg_width => 2::integer,
-    n_distinct => 0.3::real);
-ERROR:  column "nope" of relation "test" does not exist
 -- error: attribute is system column
 SELECT pg_catalog.pg_clear_attribute_stats(
     relation => 'stats_import.test'::regclass,
@@ -323,432 +189,6 @@ SELECT pg_catalog.pg_clear_attribute_stats(
     attname => 'nope'::name,
     inherited => false::boolean);
 ERROR:  column "nope" of relation "test" does not exist
--- error: attname null
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => NULL::name,
-    inherited => false::boolean,
-    null_frac => 0.1::real,
-    avg_width => 2::integer,
-    n_distinct => 0.3::real);
-ERROR:  "attname" cannot be NULL
--- error: inherited null
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => NULL::boolean,
-    null_frac => 0.1::real,
-    avg_width => 2::integer,
-    n_distinct => 0.3::real);
-ERROR:  "inherited" cannot be NULL
--- ok: no stakinds
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.1::real,
-    avg_width => 2::integer,
-    n_distinct => 0.3::real);
- pg_set_attribute_stats 
-------------------------
- 
-(1 row)
-
-SELECT stanullfrac, stawidth, stadistinct
-FROM pg_statistic
-WHERE starelid = 'stats_import.test'::regclass;
- stanullfrac | stawidth | stadistinct 
--------------+----------+-------------
-         0.1 |        2 |         0.3
-(1 row)
-
--- error: mcv / mcf null mismatch
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_freqs => '{0.1,0.2,0.3}'::real[]
-    );
-ERROR:  "most_common_vals" must be specified when "most_common_freqs" is specified
--- error: mcv / mcf null mismatch part 2
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_vals => '{1,2,3}'::text
-    );
-ERROR:  "most_common_freqs" must be specified when "most_common_vals" is specified
--- error: mcv / mcf type mismatch
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_vals => '{2023-09-30,2024-10-31,3}'::text,
-    most_common_freqs => '{0.2,0.1}'::real[]
-    );
-ERROR:  invalid input syntax for type integer: "2023-09-30"
--- error: mcv cast failure
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_vals => '{2,four,3}'::text,
-    most_common_freqs => '{0.3,0.25,0.05}'::real[]
-    );
-ERROR:  invalid input syntax for type integer: "four"
--- ok: mcv+mcf
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_vals => '{2,1,3}'::text,
-    most_common_freqs => '{0.3,0.25,0.05}'::real[]
-    );
- pg_set_attribute_stats 
-------------------------
- 
-(1 row)
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'id';
-  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
---------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
-(1 row)
-
--- error: histogram elements null value
--- this generates no warnings, but perhaps it should
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    histogram_bounds => '{1,NULL,3,4}'::text
-    );
-ERROR:  "histogram_bounds" array cannot contain NULL values
--- ok: histogram_bounds
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    histogram_bounds => '{1,2,3,4}'::text
-    );
- pg_set_attribute_stats 
-------------------------
- 
-(1 row)
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'id';
-  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
---------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
-(1 row)
-
--- ok: correlation
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    correlation => 0.5::real);
- pg_set_attribute_stats 
-------------------------
- 
-(1 row)
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'id';
-  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
---------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |         0.5 |                   |                        |                      |                        |                  | 
-(1 row)
-
--- error: scalars can't have mcelem
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_elems => '{1,3}'::text,
-    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
-    );
-ERROR:  unable to determine element type of attribute "id"
-DETAIL:  Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.
--- error: mcelem / mcelem mismatch
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'tags'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_elems => '{one,two}'::text
-    );
-ERROR:  "most_common_elem_freqs" must be specified when "most_common_elems" is specified
--- error: mcelem / mcelem null mismatch part 2
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'tags'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
-    );
-ERROR:  "most_common_elems" must be specified when "most_common_elem_freqs" is specified
--- ok: mcelem
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'tags'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_elems => '{one,three}'::text,
-    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
-    );
- pg_set_attribute_stats 
-------------------------
- 
-(1 row)
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'tags';
-  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
---------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             | {one,three}       | {0.3,0.2,0.2,0.3,0}    |                      |                        |                  | 
-(1 row)
-
--- error: scalars can't have elem_count_histogram
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
-    );
-ERROR:  unable to determine element type of attribute "id"
-DETAIL:  Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.
--- error: elem_count_histogram null element
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'tags'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    elem_count_histogram => '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
-    );
-ERROR:  "elem_count_histogram" array cannot contain NULL values
--- ok: elem_count_histogram
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'tags'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
-    );
- pg_set_attribute_stats 
-------------------------
- 
-(1 row)
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'tags';
-  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
---------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
- stats_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             | {one,three}       | {0.3,0.2,0.2,0.3,0}    | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
-(1 row)
-
--- error: scalars can't have range stats
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    range_empty_frac => 0.5::real,
-    range_length_histogram => '{399,499,Infinity}'::text
-    );
-ERROR:  attribute "id" is not a range type
-DETAIL:  Cannot set STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM or STATISTIC_KIND_BOUNDS_HISTOGRAM.
--- error: range_empty_frac range_length_hist null mismatch
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'arange'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    range_length_histogram => '{399,499,Infinity}'::text
-    );
-ERROR:  "range_empty_frac" must be specified when "range_length_histogram" is specified
--- error: range_empty_frac range_length_hist null mismatch part 2
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'arange'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    range_empty_frac => 0.5::real
-    );
-ERROR:  "range_length_histogram" must be specified when "range_empty_frac" is specified
--- ok: range_empty_frac + range_length_hist
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'arange'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    range_empty_frac => 0.5::real,
-    range_length_histogram => '{399,499,Infinity}'::text
-    );
- pg_set_attribute_stats 
-------------------------
- 
-(1 row)
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'arange';
-  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
---------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
-(1 row)
-
--- error: scalars can't have range stats
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
-    );
-ERROR:  attribute "id" is not a range type
-DETAIL:  Cannot set STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM or STATISTIC_KIND_BOUNDS_HISTOGRAM.
--- ok: range_bounds_histogram
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'arange'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
-    );
- pg_set_attribute_stats 
-------------------------
- 
-(1 row)
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'arange';
-  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
---------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
- stats_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
-(1 row)
-
--- error: cannot set most_common_elems for range type
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'arange'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_vals => '{"[2,3)","[1,2)","[3,4)"}'::text,
-    most_common_freqs => '{0.3,0.25,0.05}'::real[],
-    histogram_bounds => '{"[1,2)","[2,3)","[3,4)","[4,5)"}'::text,
-    correlation => 1.1::real,
-    most_common_elems => '{3,1}'::text,
-    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[],
-    range_empty_frac => -0.5::real,
-    range_length_histogram => '{399,499,Infinity}'::text,
-    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
-    );
-ERROR:  unable to determine element type of attribute "arange"
-DETAIL:  Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.
---
--- Clear attribute stats to try again with restore functions
--- (relation stats were already cleared).
---
-SELECT
-  pg_catalog.pg_clear_attribute_stats(
-        'stats_import.test'::regclass,
-        s.attname,
-        s.inherited)
-FROM pg_catalog.pg_stats AS s
-WHERE s.schemaname = 'stats_import'
-AND s.tablename = 'test'
-ORDER BY s.attname, s.inherited;
- pg_clear_attribute_stats 
---------------------------
- 
- 
- 
-(3 rows)
-
 -- reject: argument name is NULL
 SELECT pg_restore_relation_stats(
         'relation', '0'::oid::regclass,
@@ -1472,216 +912,6 @@ CREATE INDEX is_odd_clone ON stats_import.test_clone(((comp).a % 2 = 1));
 --
 -- Copy stats from test to test_clone, and is_odd to is_odd_clone
 --
-SELECT s.schemaname, s.tablename, s.attname, s.inherited
-FROM pg_catalog.pg_stats AS s
-CROSS JOIN LATERAL
-    pg_catalog.pg_set_attribute_stats(
-        relation => ('stats_import.' || s.tablename || '_clone')::regclass::oid,
-        attname => s.attname,
-        inherited => s.inherited,
-        null_frac => s.null_frac,
-        avg_width => s.avg_width,
-        n_distinct => s.n_distinct,
-        most_common_vals => s.most_common_vals::text,
-        most_common_freqs => s.most_common_freqs,
-        histogram_bounds => s.histogram_bounds::text,
-        correlation => s.correlation,
-        most_common_elems => s.most_common_elems::text,
-        most_common_elem_freqs => s.most_common_elem_freqs,
-        elem_count_histogram => s.elem_count_histogram,
-        range_bounds_histogram => s.range_bounds_histogram::text,
-        range_empty_frac => s.range_empty_frac,
-        range_length_histogram => s.range_length_histogram::text) AS r
-WHERE s.schemaname = 'stats_import'
-AND s.tablename IN ('test', 'is_odd')
-ORDER BY s.tablename, s.attname, s.inherited;
-  schemaname  | tablename | attname | inherited 
---------------+-----------+---------+-----------
- stats_import | is_odd    | expr    | f
- stats_import | test      | arange  | f
- stats_import | test      | comp    | f
- stats_import | test      | id      | f
- stats_import | test      | name    | f
- stats_import | test      | tags    | f
-(6 rows)
-
-SELECT c.relname, COUNT(*) AS num_stats
-FROM pg_class AS c
-JOIN pg_statistic s ON s.starelid = c.oid
-WHERE c.relnamespace = 'stats_import'::regnamespace
-AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
-GROUP BY c.relname
-ORDER BY c.relname;
-   relname    | num_stats 
---------------+-----------
- is_odd       |         1
- is_odd_clone |         1
- test         |         5
- test_clone   |         5
-(4 rows)
-
--- check test minus test_clone
-SELECT
-    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
-    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
-    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
-    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
-    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
-    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
-    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
-    s.stavalues5::text AS sv5, 'test' AS direction
-FROM pg_statistic s
-JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
-WHERE s.starelid = 'stats_import.test'::regclass
-EXCEPT
-SELECT
-    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
-    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
-    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
-    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
-    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
-    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
-    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
-    s.stavalues5::text AS sv5, 'test' AS direction
-FROM pg_statistic s
-JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
-WHERE s.starelid = 'stats_import.test_clone'::regclass;
- attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
----------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
-(0 rows)
-
--- check test_clone minus test
-SELECT
-    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
-    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
-    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
-    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
-    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
-    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
-    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
-    s.stavalues5::text AS sv5, 'test_clone' AS direction
-FROM pg_statistic s
-JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
-WHERE s.starelid = 'stats_import.test_clone'::regclass
-EXCEPT
-SELECT
-    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
-    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
-    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
-    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
-    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
-    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
-    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
-    s.stavalues5::text AS sv5, 'test_clone' AS direction
-FROM pg_statistic s
-JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
-WHERE s.starelid = 'stats_import.test'::regclass;
- attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
----------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
-(0 rows)
-
--- check is_odd minus is_odd_clone
-SELECT
-    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
-    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
-    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
-    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
-    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
-    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
-    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
-    s.stavalues5::text AS sv5, 'is_odd' AS direction
-FROM pg_statistic s
-JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
-WHERE s.starelid = 'stats_import.is_odd'::regclass
-EXCEPT
-SELECT
-    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
-    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
-    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
-    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
-    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
-    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
-    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
-    s.stavalues5::text AS sv5, 'is_odd' AS direction
-FROM pg_statistic s
-JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
-WHERE s.starelid = 'stats_import.is_odd_clone'::regclass;
- attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
----------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
-(0 rows)
-
--- check is_odd_clone minus is_odd
-SELECT
-    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
-    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
-    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
-    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
-    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
-    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
-    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
-    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
-FROM pg_statistic s
-JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
-WHERE s.starelid = 'stats_import.is_odd_clone'::regclass
-EXCEPT
-SELECT
-    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
-    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
-    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
-    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
-    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
-    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
-    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
-    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
-FROM pg_statistic s
-JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
-WHERE s.starelid = 'stats_import.is_odd'::regclass;
- attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
----------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
-(0 rows)
-
---
-SELECT relpages, reltuples, relallvisible
-FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
- relpages | reltuples | relallvisible 
-----------+-----------+---------------
-        1 |         4 |             0
-(1 row)
-
---
--- Clear clone stats to try again with pg_restore_attribute_stats
---
-SELECT
-  pg_catalog.pg_clear_attribute_stats(
-        ('stats_import.' || s.tablename)::regclass,
-        s.attname,
-        s.inherited)
-FROM pg_catalog.pg_stats AS s
-WHERE s.schemaname = 'stats_import'
-AND s.tablename IN ('test_clone', 'is_odd_clone')
-ORDER BY s.tablename, s.attname, s.inherited;
- pg_clear_attribute_stats 
---------------------------
- 
- 
- 
- 
- 
- 
-(6 rows)
-
-SELECT
-SELECT COUNT(*)
-FROM pg_catalog.pg_stats AS s
-WHERE s.schemaname = 'stats_import'
-AND s.tablename IN ('test_clone', 'is_odd_clone');
-ERROR:  syntax error at or near "SELECT"
-LINE 2: SELECT COUNT(*)
-        ^
---
--- Copy stats from test to test_clone, and is_odd to is_odd_clone
---
 SELECT s.schemaname, s.tablename, s.attname, s.inherited, r.*
 FROM pg_catalog.pg_stats AS s
 CROSS JOIN LATERAL
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index 9740ab3ff02..f26f7857748 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -15,63 +15,19 @@ CREATE TABLE stats_import.test(
     tags text[]
 ) WITH (autovacuum_enabled = false);
 
--- starting stats
-SELECT relpages, reltuples, relallvisible
-FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
-
--- error: regclass not found
-SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 0::Oid,
-        relpages => 17::integer,
-        reltuples => 400.0::real,
-        relallvisible => 4::integer);
-
--- relpages default
-SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.test'::regclass,
-        relpages => NULL::integer,
-        reltuples => 400.0::real,
-        relallvisible => 4::integer);
-
--- reltuples default
-SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.test'::regclass,
-        relpages => 17::integer,
-        reltuples => NULL::real,
-        relallvisible => 4::integer);
-
--- relallvisible default
-SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.test'::regclass,
-        relpages => 17::integer,
-        reltuples => 400.0::real,
-        relallvisible => NULL::integer);
-
--- named arguments
-SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.test'::regclass,
-        relpages => 17::integer,
-        reltuples => 400.0::real,
-        relallvisible => 4::integer);
+CREATE INDEX test_i ON stats_import.test(id);
 
+-- starting stats
 SELECT relpages, reltuples, relallvisible
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
-CREATE INDEX test_i ON stats_import.test(id);
-
 BEGIN;
 -- regular indexes have special case locking rules
 SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.test_i'::regclass,
-        relpages => 18::integer);
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.test_i'::regclass,
+        'relpages', 18::integer);
 
 SELECT mode FROM pg_locks
 WHERE relation = 'stats_import.test'::regclass AND
@@ -88,22 +44,6 @@ SELECT
         'relation', 'stats_import.test_i'::regclass,
         'relpages', 19::integer );
 
--- positional arguments
-SELECT
-    pg_catalog.pg_set_relation_stats(
-        'stats_import.test'::regclass,
-        18::integer,
-        401.0::real,
-        5::integer);
-
-SELECT relpages, reltuples, relallvisible
-FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
-
-SELECT relpages, reltuples, relallvisible
-FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
-
 -- clear
 SELECT
     pg_catalog.pg_clear_relation_stats(
@@ -141,14 +81,14 @@ WHERE oid = 'stats_import.part_parent'::regclass;
 -- although partitioned tables have no storage, setting relpages to a
 -- positive value is still allowed
 SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.part_parent_i'::regclass,
-        relpages => 2::integer);
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.part_parent_i'::regclass,
+        'relpages', 2::integer);
 
 SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.part_parent'::regclass,
-        relpages => 2::integer);
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.part_parent'::regclass,
+        'relpages', 2::integer);
 
 --
 -- Partitioned indexes aren't analyzed but it is possible to set
@@ -159,9 +99,9 @@ SELECT
 BEGIN;
 
 SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.part_parent_i'::regclass,
-        relpages => 2::integer);
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.part_parent_i'::regclass,
+        'relpages', 2::integer);
 
 SELECT mode FROM pg_locks
 WHERE relation = 'stats_import.part_parent'::regclass AND
@@ -180,51 +120,9 @@ SELECT
 
 -- nothing stops us from setting it to -1
 SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.part_parent'::regclass,
-        relpages => -1::integer);
-
--- error: object doesn't exist
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => '0'::oid,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.1::real,
-    avg_width => 2::integer,
-    n_distinct => 0.3::real);
-
--- error: object doesn't exist
-SELECT pg_catalog.pg_clear_attribute_stats(
-    relation => '0'::oid,
-    attname => 'id'::name,
-    inherited => false::boolean);
-
--- error: relation null
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => NULL::oid,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.1::real,
-    avg_width => 2::integer,
-    n_distinct => 0.3::real);
-
--- error: attribute is system column
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'xmin'::name,
-    inherited => false::boolean,
-    null_frac => 0.1::real,
-    avg_width => 2::integer,
-    n_distinct => 0.3::real);
-
--- error: attname doesn't exist
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'nope'::name,
-    inherited => false::boolean,
-    null_frac => 0.1::real,
-    avg_width => 2::integer,
-    n_distinct => 0.3::real);
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.part_parent'::regclass,
+        'relpages', -1::integer);
 
 -- error: attribute is system column
 SELECT pg_catalog.pg_clear_attribute_stats(
@@ -238,351 +136,6 @@ SELECT pg_catalog.pg_clear_attribute_stats(
     attname => 'nope'::name,
     inherited => false::boolean);
 
--- error: attname null
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => NULL::name,
-    inherited => false::boolean,
-    null_frac => 0.1::real,
-    avg_width => 2::integer,
-    n_distinct => 0.3::real);
-
--- error: inherited null
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => NULL::boolean,
-    null_frac => 0.1::real,
-    avg_width => 2::integer,
-    n_distinct => 0.3::real);
-
--- ok: no stakinds
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.1::real,
-    avg_width => 2::integer,
-    n_distinct => 0.3::real);
-
-SELECT stanullfrac, stawidth, stadistinct
-FROM pg_statistic
-WHERE starelid = 'stats_import.test'::regclass;
-
--- error: mcv / mcf null mismatch
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_freqs => '{0.1,0.2,0.3}'::real[]
-    );
-
--- error: mcv / mcf null mismatch part 2
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_vals => '{1,2,3}'::text
-    );
-
--- error: mcv / mcf type mismatch
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_vals => '{2023-09-30,2024-10-31,3}'::text,
-    most_common_freqs => '{0.2,0.1}'::real[]
-    );
-
--- error: mcv cast failure
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_vals => '{2,four,3}'::text,
-    most_common_freqs => '{0.3,0.25,0.05}'::real[]
-    );
-
--- ok: mcv+mcf
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_vals => '{2,1,3}'::text,
-    most_common_freqs => '{0.3,0.25,0.05}'::real[]
-    );
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'id';
-
--- error: histogram elements null value
--- this generates no warnings, but perhaps it should
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    histogram_bounds => '{1,NULL,3,4}'::text
-    );
-
--- ok: histogram_bounds
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    histogram_bounds => '{1,2,3,4}'::text
-    );
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'id';
-
--- ok: correlation
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    correlation => 0.5::real);
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'id';
-
--- error: scalars can't have mcelem
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_elems => '{1,3}'::text,
-    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
-    );
-
--- error: mcelem / mcelem mismatch
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'tags'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_elems => '{one,two}'::text
-    );
-
--- error: mcelem / mcelem null mismatch part 2
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'tags'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
-    );
-
--- ok: mcelem
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'tags'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_elems => '{one,three}'::text,
-    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
-    );
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'tags';
-
--- error: scalars can't have elem_count_histogram
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
-    );
--- error: elem_count_histogram null element
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'tags'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    elem_count_histogram => '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
-    );
--- ok: elem_count_histogram
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'tags'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
-    );
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'tags';
-
--- error: scalars can't have range stats
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    range_empty_frac => 0.5::real,
-    range_length_histogram => '{399,499,Infinity}'::text
-    );
--- error: range_empty_frac range_length_hist null mismatch
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'arange'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    range_length_histogram => '{399,499,Infinity}'::text
-    );
--- error: range_empty_frac range_length_hist null mismatch part 2
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'arange'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    range_empty_frac => 0.5::real
-    );
--- ok: range_empty_frac + range_length_hist
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'arange'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    range_empty_frac => 0.5::real,
-    range_length_histogram => '{399,499,Infinity}'::text
-    );
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'arange';
-
--- error: scalars can't have range stats
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
-    );
--- ok: range_bounds_histogram
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'arange'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
-    );
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'arange';
-
--- error: cannot set most_common_elems for range type
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'arange'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_vals => '{"[2,3)","[1,2)","[3,4)"}'::text,
-    most_common_freqs => '{0.3,0.25,0.05}'::real[],
-    histogram_bounds => '{"[1,2)","[2,3)","[3,4)","[4,5)"}'::text,
-    correlation => 1.1::real,
-    most_common_elems => '{3,1}'::text,
-    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[],
-    range_empty_frac => -0.5::real,
-    range_length_histogram => '{399,499,Infinity}'::text,
-    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
-    );
-
---
--- Clear attribute stats to try again with restore functions
--- (relation stats were already cleared).
---
-SELECT
-  pg_catalog.pg_clear_attribute_stats(
-        'stats_import.test'::regclass,
-        s.attname,
-        s.inherited)
-FROM pg_catalog.pg_stats AS s
-WHERE s.schemaname = 'stats_import'
-AND s.tablename = 'test'
-ORDER BY s.attname, s.inherited;
-
 -- reject: argument name is NULL
 SELECT pg_restore_relation_stats(
         'relation', '0'::oid::regclass,
@@ -1105,173 +658,6 @@ CREATE TABLE stats_import.test_clone ( LIKE stats_import.test )
 
 CREATE INDEX is_odd_clone ON stats_import.test_clone(((comp).a % 2 = 1));
 
---
--- Copy stats from test to test_clone, and is_odd to is_odd_clone
---
-SELECT s.schemaname, s.tablename, s.attname, s.inherited
-FROM pg_catalog.pg_stats AS s
-CROSS JOIN LATERAL
-    pg_catalog.pg_set_attribute_stats(
-        relation => ('stats_import.' || s.tablename || '_clone')::regclass::oid,
-        attname => s.attname,
-        inherited => s.inherited,
-        null_frac => s.null_frac,
-        avg_width => s.avg_width,
-        n_distinct => s.n_distinct,
-        most_common_vals => s.most_common_vals::text,
-        most_common_freqs => s.most_common_freqs,
-        histogram_bounds => s.histogram_bounds::text,
-        correlation => s.correlation,
-        most_common_elems => s.most_common_elems::text,
-        most_common_elem_freqs => s.most_common_elem_freqs,
-        elem_count_histogram => s.elem_count_histogram,
-        range_bounds_histogram => s.range_bounds_histogram::text,
-        range_empty_frac => s.range_empty_frac,
-        range_length_histogram => s.range_length_histogram::text) AS r
-WHERE s.schemaname = 'stats_import'
-AND s.tablename IN ('test', 'is_odd')
-ORDER BY s.tablename, s.attname, s.inherited;
-
-SELECT c.relname, COUNT(*) AS num_stats
-FROM pg_class AS c
-JOIN pg_statistic s ON s.starelid = c.oid
-WHERE c.relnamespace = 'stats_import'::regnamespace
-AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
-GROUP BY c.relname
-ORDER BY c.relname;
-
--- check test minus test_clone
-SELECT
-    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
-    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
-    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
-    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
-    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
-    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
-    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
-    s.stavalues5::text AS sv5, 'test' AS direction
-FROM pg_statistic s
-JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
-WHERE s.starelid = 'stats_import.test'::regclass
-EXCEPT
-SELECT
-    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
-    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
-    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
-    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
-    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
-    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
-    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
-    s.stavalues5::text AS sv5, 'test' AS direction
-FROM pg_statistic s
-JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
-WHERE s.starelid = 'stats_import.test_clone'::regclass;
-
--- check test_clone minus test
-SELECT
-    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
-    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
-    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
-    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
-    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
-    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
-    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
-    s.stavalues5::text AS sv5, 'test_clone' AS direction
-FROM pg_statistic s
-JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
-WHERE s.starelid = 'stats_import.test_clone'::regclass
-EXCEPT
-SELECT
-    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
-    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
-    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
-    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
-    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
-    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
-    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
-    s.stavalues5::text AS sv5, 'test_clone' AS direction
-FROM pg_statistic s
-JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
-WHERE s.starelid = 'stats_import.test'::regclass;
-
--- check is_odd minus is_odd_clone
-SELECT
-    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
-    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
-    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
-    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
-    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
-    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
-    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
-    s.stavalues5::text AS sv5, 'is_odd' AS direction
-FROM pg_statistic s
-JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
-WHERE s.starelid = 'stats_import.is_odd'::regclass
-EXCEPT
-SELECT
-    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
-    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
-    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
-    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
-    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
-    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
-    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
-    s.stavalues5::text AS sv5, 'is_odd' AS direction
-FROM pg_statistic s
-JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
-WHERE s.starelid = 'stats_import.is_odd_clone'::regclass;
-
--- check is_odd_clone minus is_odd
-SELECT
-    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
-    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
-    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
-    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
-    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
-    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
-    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
-    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
-FROM pg_statistic s
-JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
-WHERE s.starelid = 'stats_import.is_odd_clone'::regclass
-EXCEPT
-SELECT
-    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
-    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
-    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
-    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
-    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
-    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
-    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
-    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
-FROM pg_statistic s
-JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
-WHERE s.starelid = 'stats_import.is_odd'::regclass;
-
---
-SELECT relpages, reltuples, relallvisible
-FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
-
---
--- Clear clone stats to try again with pg_restore_attribute_stats
---
-SELECT
-  pg_catalog.pg_clear_attribute_stats(
-        ('stats_import.' || s.tablename)::regclass,
-        s.attname,
-        s.inherited)
-FROM pg_catalog.pg_stats AS s
-WHERE s.schemaname = 'stats_import'
-AND s.tablename IN ('test_clone', 'is_odd_clone')
-ORDER BY s.tablename, s.attname, s.inherited;
-SELECT
-
-SELECT COUNT(*)
-FROM pg_catalog.pg_stats AS s
-WHERE s.schemaname = 'stats_import'
-AND s.tablename IN ('test_clone', 'is_odd_clone');
-
 --
 -- Copy stats from test to test_clone, and is_odd to is_odd_clone
 --
-- 
2.34.1

#366Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#365)
Re: Statistics Import and Export

On Tue, Feb 25, 2025 at 1:22 PM Jeff Davis <pgsql@j-davis.com> wrote:

On Mon, 2025-02-24 at 12:50 -0500, Tom Lane wrote:

Also, while working on the attached, I couldn't help forming the
opinion that we'd be better off to nuke pg_set_attribute_stats()
from orbit and require people to use pg_restore_attribute_stats().

Attached a patch to do so. The docs and tests required substantial
rework, but I think it's for the better now that we aren't trying to do
in-place updates.

Regards,
Jeff Davis

All the C code changes make sense to me. Though as an aside, we're going to
run into the parameter-ordering problem when it comes to
pg_clear_attribute_stats, but that's a (read: my) problem for a later patch.

Documentation:

+         The currently-supported relation statistics are
+         <literal>relpages</literal> with a value of type
+         <type>integer</type>, <literal>reltuples</literal> with a value of
+         type <type>real</type>, and <literal>relallvisible</literal> with
a
+         value of type <type>integer</type>.

Could we make this a bullet-list? Same for the required attribute stats and
optional attribute stats. I think it would be more eye-catching and useful
to people skimming to recall the name of a parameter, which is probably
what most people will do after they've read it once to get the core
concepts.

Question:

Do we want to re-compact the oids we consumed in pg_proc.dat?

Test cases:

We're ripping out a lot of regression tests here. Some of them obviously
have no possible pg_restore_* analogs, such as explicitly set NULL values
vs omitting the param entirely, but some others may not, especially the
ones that test required arg-pairs.

Specifically missing are:

* regclass not found
* attribute is system column
* scalars can't have mcelem
* mcelem / mcelem freqs mismatch (parts 1 and 2)
* scalars can't have elem_count_histogram
* cannot set most_common_elems for range type

I'm less worried about all the tests of successful import calls, as the
pg_upgrade TAP tests kick those tires pretty well.

I'm also ok with losing the copies from test to test_clone, those are also
covered well by the TAP tests.

I'd feel better if we adapted the above tests from set-tests to
restore-tests, as the TAP suite doesn't really cover intentionally bad
stats.

#367Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#366)
3 attachment(s)
Re: Statistics Import and Export

Specifically missing are:

* regclass not found
* attribute is system column
* scalars can't have mcelem
* mcelem / mcelem freqs mismatch (parts 1 and 2)
* scalars can't have elem_count_histogram
* cannot set most_common_elems for range type

This patchset is as follows:

0001 - Jeff's patch from earlier today
0002 - Changing the parameter lists to <itemizedlist> to aid skim-ability
0003 - converting some of the deleted pg_set* tests into pg_restore* tests
to keep the error coverage that they had.

Next I'm going to incorporate Tom's attnum change, the locale set, and the
set-index-by-attnum changes.

Attachments:

vAdios-Set-0001-Remove-redundant-pg_set_-_stats-variants.patchtext/x-patch; charset=US-ASCII; name=vAdios-Set-0001-Remove-redundant-pg_set_-_stats-variants.patchDownload
From 30e9010a54d43376af2b33281974c1ebb0ea6382 Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Mon, 24 Feb 2025 17:24:05 -0800
Subject: [PATCH vAdios-Set 1/3] Remove redundant pg_set_*_stats() variants.

After commit f3dae2ae58, the primary purpose of separating the
pg_set_*_stats() from the pg_restore_*_stats() variants was
eliminated.

Leave pg_restore_relation_stats() and pg_restore_attribute_stats(),
which satisfy both purposes, and remove pg_set_relation_stats() and
pg_set_attribute_stats().

Discussion: https://postgr.es/m/1457469.1740419458@sss.pgh.pa.us
---
 src/include/catalog/pg_proc.dat            |  14 -
 src/include/statistics/stat_utils.h        |   8 +-
 src/backend/catalog/system_functions.sql   |  32 -
 src/backend/statistics/attribute_stats.c   |  98 +--
 src/backend/statistics/relation_stats.c    |  24 +-
 src/backend/statistics/stat_utils.c        |  30 +-
 src/test/regress/expected/stats_import.out | 832 +--------------------
 src/test/regress/sql/stats_import.sql      | 648 +---------------
 doc/src/sgml/func.sgml                     | 260 +++----
 9 files changed, 212 insertions(+), 1734 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index af9546de23d..9f0c676e22d 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12429,13 +12429,6 @@
   proargnames => '{kwargs}',
   proargmodes => '{v}',
   prosrc => 'pg_restore_attribute_stats' },
-{ oid => '9162',
-  descr => 'set statistics on attribute',
-  proname => 'pg_set_attribute_stats', provolatile => 'v', proisstrict => 'f',
-  proparallel => 'u', prorettype => 'void',
-  proargtypes => 'regclass name bool float4 int4 float4 text _float4 text float4 text _float4 _float4 text float4 text',
-  proargnames => '{relation,attname,inherited,null_frac,avg_width,n_distinct,most_common_vals,most_common_freqs,histogram_bounds,correlation,most_common_elems,most_common_elem_freqs,elem_count_histogram,range_length_histogram,range_empty_frac,range_bounds_histogram}',
-  prosrc => 'pg_set_attribute_stats' },
 { oid => '9163',
   descr => 'clear statistics on attribute',
   proname => 'pg_clear_attribute_stats', provolatile => 'v', proisstrict => 'f',
@@ -12443,13 +12436,6 @@
   proargtypes => 'regclass name bool',
   proargnames => '{relation,attname,inherited}',
   prosrc => 'pg_clear_attribute_stats' },
-{ oid => '9944',
-  descr => 'set statistics on relation',
-  proname => 'pg_set_relation_stats', provolatile => 'v', proisstrict => 'f',
-  proparallel => 'u', prorettype => 'void',
-  proargtypes => 'regclass int4 float4 int4',
-  proargnames => '{relation,relpages,reltuples,relallvisible}',
-  prosrc => 'pg_set_relation_stats' },
 { oid => '9945',
   descr => 'clear statistics on relation',
   proname => 'pg_clear_relation_stats', provolatile => 'v', proisstrict => 'f',
diff --git a/src/include/statistics/stat_utils.h b/src/include/statistics/stat_utils.h
index 6edb5ea0321..0eb4decfcac 100644
--- a/src/include/statistics/stat_utils.h
+++ b/src/include/statistics/stat_utils.h
@@ -25,17 +25,15 @@ extern void stats_check_required_arg(FunctionCallInfo fcinfo,
 									 struct StatsArgInfo *arginfo,
 									 int argnum);
 extern bool stats_check_arg_array(FunctionCallInfo fcinfo,
-								  struct StatsArgInfo *arginfo, int argnum,
-								  int elevel);
+								  struct StatsArgInfo *arginfo, int argnum);
 extern bool stats_check_arg_pair(FunctionCallInfo fcinfo,
 								 struct StatsArgInfo *arginfo,
-								 int argnum1, int argnum2, int elevel);
+								 int argnum1, int argnum2);
 
 extern void stats_lock_check_privileges(Oid reloid);
 
 extern bool stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
 											 FunctionCallInfo positional_fcinfo,
-											 struct StatsArgInfo *arginfo,
-											 int elevel);
+											 struct StatsArgInfo *arginfo);
 
 #endif							/* STATS_UTILS_H */
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 591157b1d1b..86888cd3201 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -636,38 +636,6 @@ LANGUAGE INTERNAL
 CALLED ON NULL INPUT VOLATILE PARALLEL SAFE
 AS 'pg_stat_reset_slru';
 
-CREATE OR REPLACE FUNCTION
-  pg_set_relation_stats(relation regclass,
-                        relpages integer DEFAULT NULL,
-                        reltuples real DEFAULT NULL,
-                        relallvisible integer DEFAULT NULL)
-RETURNS void
-LANGUAGE INTERNAL
-CALLED ON NULL INPUT VOLATILE
-AS 'pg_set_relation_stats';
-
-CREATE OR REPLACE FUNCTION
-  pg_set_attribute_stats(relation regclass,
-                         attname name,
-                         inherited bool,
-                         null_frac real DEFAULT NULL,
-                         avg_width integer DEFAULT NULL,
-                         n_distinct real DEFAULT NULL,
-                         most_common_vals text DEFAULT NULL,
-                         most_common_freqs real[] DEFAULT NULL,
-                         histogram_bounds text DEFAULT NULL,
-                         correlation real DEFAULT NULL,
-                         most_common_elems text DEFAULT NULL,
-                         most_common_elem_freqs real[] DEFAULT NULL,
-                         elem_count_histogram real[] DEFAULT NULL,
-                         range_length_histogram text DEFAULT NULL,
-                         range_empty_frac real DEFAULT NULL,
-                         range_bounds_histogram text DEFAULT NULL)
-RETURNS void
-LANGUAGE INTERNAL
-CALLED ON NULL INPUT VOLATILE
-AS 'pg_set_attribute_stats';
-
 --
 -- The default permissions for functions mean that anyone can execute them.
 -- A number of functions shouldn't be executable by just anyone, but rather
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index c0c398a4bb2..66a5676c810 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -76,16 +76,16 @@ static struct StatsArgInfo attarginfo[] =
 	[NUM_ATTRIBUTE_STATS_ARGS] = {0}
 };
 
-static bool attribute_statistics_update(FunctionCallInfo fcinfo, int elevel);
+static bool attribute_statistics_update(FunctionCallInfo fcinfo);
 static Node *get_attr_expr(Relation rel, int attnum);
-static void get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
+static void get_attr_stat_type(Oid reloid, AttrNumber attnum,
 							   Oid *atttypid, int32 *atttypmod,
 							   char *atttyptype, Oid *atttypcoll,
 							   Oid *eq_opr, Oid *lt_opr);
-static bool get_elem_stat_type(Oid atttypid, char atttyptype, int elevel,
+static bool get_elem_stat_type(Oid atttypid, char atttyptype,
 							   Oid *elemtypid, Oid *elem_eq_opr);
 static Datum text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d,
-							   Oid typid, int32 typmod, int elevel, bool *ok);
+							   Oid typid, int32 typmod, bool *ok);
 static void set_stats_slot(Datum *values, bool *nulls, bool *replaces,
 						   int16 stakind, Oid staop, Oid stacoll,
 						   Datum stanumbers, bool stanumbers_isnull,
@@ -109,11 +109,11 @@ static void init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
  *
  * Major errors, such as the table not existing, the attribute not existing,
  * or a permissions failure are always reported at ERROR. Other errors, such
- * as a conversion failure on one statistic kind, are reported at 'elevel',
+ * as a conversion failure on one statistic kind, are reported as a WARNING
  * and other statistic kinds may still be updated.
  */
 static bool
-attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
+attribute_statistics_update(FunctionCallInfo fcinfo)
 {
 	Oid			reloid;
 	Name		attname;
@@ -184,33 +184,29 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	inherited = PG_GETARG_BOOL(INHERITED_ARG);
 
 	/*
-	 * Check argument sanity. If some arguments are unusable, emit at elevel
+	 * Check argument sanity. If some arguments are unusable, emit a WARNING
 	 * and set the corresponding argument to NULL in fcinfo.
 	 */
 
-	if (!stats_check_arg_array(fcinfo, attarginfo, MOST_COMMON_FREQS_ARG,
-							   elevel))
+	if (!stats_check_arg_array(fcinfo, attarginfo, MOST_COMMON_FREQS_ARG))
 	{
 		do_mcv = false;
 		result = false;
 	}
 
-	if (!stats_check_arg_array(fcinfo, attarginfo, MOST_COMMON_ELEM_FREQS_ARG,
-							   elevel))
+	if (!stats_check_arg_array(fcinfo, attarginfo, MOST_COMMON_ELEM_FREQS_ARG))
 	{
 		do_mcelem = false;
 		result = false;
 	}
-	if (!stats_check_arg_array(fcinfo, attarginfo, ELEM_COUNT_HISTOGRAM_ARG,
-							   elevel))
+	if (!stats_check_arg_array(fcinfo, attarginfo, ELEM_COUNT_HISTOGRAM_ARG))
 	{
 		do_dechist = false;
 		result = false;
 	}
 
 	if (!stats_check_arg_pair(fcinfo, attarginfo,
-							  MOST_COMMON_VALS_ARG, MOST_COMMON_FREQS_ARG,
-							  elevel))
+							  MOST_COMMON_VALS_ARG, MOST_COMMON_FREQS_ARG))
 	{
 		do_mcv = false;
 		result = false;
@@ -218,7 +214,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 
 	if (!stats_check_arg_pair(fcinfo, attarginfo,
 							  MOST_COMMON_ELEMS_ARG,
-							  MOST_COMMON_ELEM_FREQS_ARG, elevel))
+							  MOST_COMMON_ELEM_FREQS_ARG))
 	{
 		do_mcelem = false;
 		result = false;
@@ -226,14 +222,14 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 
 	if (!stats_check_arg_pair(fcinfo, attarginfo,
 							  RANGE_LENGTH_HISTOGRAM_ARG,
-							  RANGE_EMPTY_FRAC_ARG, elevel))
+							  RANGE_EMPTY_FRAC_ARG))
 	{
 		do_range_length_histogram = false;
 		result = false;
 	}
 
 	/* derive information from attribute */
-	get_attr_stat_type(reloid, attnum, elevel,
+	get_attr_stat_type(reloid, attnum,
 					   &atttypid, &atttypmod,
 					   &atttyptype, &atttypcoll,
 					   &eq_opr, &lt_opr);
@@ -241,10 +237,10 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	/* if needed, derive element type */
 	if (do_mcelem || do_dechist)
 	{
-		if (!get_elem_stat_type(atttypid, atttyptype, elevel,
+		if (!get_elem_stat_type(atttypid, atttyptype,
 								&elemtypid, &elem_eq_opr))
 		{
-			ereport(elevel,
+			ereport(WARNING,
 					(errmsg("unable to determine element type of attribute \"%s\"", NameStr(*attname)),
 					 errdetail("Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.")));
 			elemtypid = InvalidOid;
@@ -259,7 +255,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	/* histogram and correlation require less-than operator */
 	if ((do_histogram || do_correlation) && !OidIsValid(lt_opr))
 	{
-		ereport(elevel,
+		ereport(WARNING,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("could not determine less-than operator for attribute \"%s\"", NameStr(*attname)),
 				 errdetail("Cannot set STATISTIC_KIND_HISTOGRAM or STATISTIC_KIND_CORRELATION.")));
@@ -273,7 +269,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	if ((do_range_length_histogram || do_bounds_histogram) &&
 		!(atttyptype == TYPTYPE_RANGE || atttyptype == TYPTYPE_MULTIRANGE))
 	{
-		ereport(elevel,
+		ereport(WARNING,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("attribute \"%s\" is not a range type", NameStr(*attname)),
 				 errdetail("Cannot set STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM or STATISTIC_KIND_BOUNDS_HISTOGRAM.")));
@@ -322,7 +318,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 												  &array_in_fn,
 												  PG_GETARG_DATUM(MOST_COMMON_VALS_ARG),
 												  atttypid, atttypmod,
-												  elevel, &converted);
+												  &converted);
 
 		if (converted)
 		{
@@ -344,7 +340,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 		stavalues = text_to_stavalues("histogram_bounds",
 									  &array_in_fn,
 									  PG_GETARG_DATUM(HISTOGRAM_BOUNDS_ARG),
-									  atttypid, atttypmod, elevel,
+									  atttypid, atttypmod,
 									  &converted);
 
 		if (converted)
@@ -382,7 +378,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 									  &array_in_fn,
 									  PG_GETARG_DATUM(MOST_COMMON_ELEMS_ARG),
 									  elemtypid, atttypmod,
-									  elevel, &converted);
+									  &converted);
 
 		if (converted)
 		{
@@ -422,7 +418,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 									  &array_in_fn,
 									  PG_GETARG_DATUM(RANGE_BOUNDS_HISTOGRAM_ARG),
 									  atttypid, atttypmod,
-									  elevel, &converted);
+									  &converted);
 
 		if (converted)
 		{
@@ -449,7 +445,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 		stavalues = text_to_stavalues("range_length_histogram",
 									  &array_in_fn,
 									  PG_GETARG_DATUM(RANGE_LENGTH_HISTOGRAM_ARG),
-									  FLOAT8OID, 0, elevel, &converted);
+									  FLOAT8OID, 0, &converted);
 
 		if (converted)
 		{
@@ -517,7 +513,7 @@ get_attr_expr(Relation rel, int attnum)
  * Derive type information from the attribute.
  */
 static void
-get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
+get_attr_stat_type(Oid reloid, AttrNumber attnum,
 				   Oid *atttypid, int32 *atttypmod,
 				   char *atttyptype, Oid *atttypcoll,
 				   Oid *eq_opr, Oid *lt_opr)
@@ -599,7 +595,7 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
  * Derive element type information from the attribute type.
  */
 static bool
-get_elem_stat_type(Oid atttypid, char atttyptype, int elevel,
+get_elem_stat_type(Oid atttypid, char atttyptype,
 				   Oid *elemtypid, Oid *elem_eq_opr)
 {
 	TypeCacheEntry *elemtypcache;
@@ -634,13 +630,13 @@ get_elem_stat_type(Oid atttypid, char atttyptype, int elevel,
 /*
  * Cast a text datum into an array with element type elemtypid.
  *
- * If an error is encountered, capture it and re-throw at elevel, and set ok
- * to false. If the resulting array contains NULLs, raise an error at elevel
- * and set ok to false. Otherwise, set ok to true.
+ * If an error is encountered, capture it and re-throw a WARNING, and set ok
+ * to false. If the resulting array contains NULLs, raise a WARNING and set ok
+ * to false. Otherwise, set ok to true.
  */
 static Datum
 text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid typid,
-				  int32 typmod, int elevel, bool *ok)
+				  int32 typmod, bool *ok)
 {
 	LOCAL_FCINFO(fcinfo, 8);
 	char	   *s;
@@ -667,8 +663,7 @@ text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid typid,
 
 	if (escontext.error_occurred)
 	{
-		if (elevel != ERROR)
-			escontext.error_data->elevel = elevel;
+		escontext.error_data->elevel = WARNING;
 		ThrowErrorData(escontext.error_data);
 		*ok = false;
 		return (Datum) 0;
@@ -676,7 +671,7 @@ text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid typid,
 
 	if (array_contains_nulls(DatumGetArrayTypeP(result)))
 	{
-		ereport(elevel,
+		ereport(WARNING,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("\"%s\" array cannot contain NULL values", staname)));
 		*ok = false;
@@ -851,33 +846,6 @@ init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
 	}
 }
 
-/*
- * Import statistics for a given relation attribute.
- *
- * Inserts or replaces a row in pg_statistic for the given relation and
- * attribute name. It takes input parameters that correspond to columns in the
- * view pg_stats.
- *
- * Parameters null_frac, avg_width, and n_distinct all correspond to NOT NULL
- * columns in pg_statistic. The remaining parameters all belong to a specific
- * stakind. Some stakinds require multiple parameters, which must be specified
- * together (or neither specified).
- *
- * Parameters are only superficially validated. Omitting a parameter or
- * passing NULL leaves the statistic unchanged.
- *
- * Parameters corresponding to ANYARRAY columns are instead passed in as text
- * values, which is a valid input string for an array of the type or element
- * type of the attribute. Any error generated by the array_in() function will
- * in turn fail the function.
- */
-Datum
-pg_set_attribute_stats(PG_FUNCTION_ARGS)
-{
-	attribute_statistics_update(fcinfo, ERROR);
-	PG_RETURN_VOID();
-}
-
 /*
  * Delete statistics for the given attribute.
  */
@@ -933,10 +901,10 @@ pg_restore_attribute_stats(PG_FUNCTION_ARGS)
 							 InvalidOid, NULL, NULL);
 
 	if (!stats_fill_fcinfo_from_arg_pairs(fcinfo, positional_fcinfo,
-										  attarginfo, WARNING))
+										  attarginfo))
 		result = false;
 
-	if (!attribute_statistics_update(positional_fcinfo, WARNING))
+	if (!attribute_statistics_update(positional_fcinfo))
 		result = false;
 
 	PG_RETURN_BOOL(result);
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index 66731290a3e..11b1ef2dbc2 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -48,13 +48,13 @@ static struct StatsArgInfo relarginfo[] =
 	[NUM_RELATION_STATS_ARGS] = {0}
 };
 
-static bool relation_statistics_update(FunctionCallInfo fcinfo, int elevel);
+static bool relation_statistics_update(FunctionCallInfo fcinfo);
 
 /*
  * Internal function for modifying statistics for a relation.
  */
 static bool
-relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
+relation_statistics_update(FunctionCallInfo fcinfo)
 {
 	bool		result = true;
 	Oid			reloid;
@@ -83,7 +83,7 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 		reltuples = PG_GETARG_FLOAT4(RELTUPLES_ARG);
 		if (reltuples < -1.0)
 		{
-			ereport(elevel,
+			ereport(WARNING,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("reltuples cannot be < -1.0")));
 			result = false;
@@ -118,7 +118,7 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	ctup = SearchSysCache1(RELOID, ObjectIdGetDatum(reloid));
 	if (!HeapTupleIsValid(ctup))
 	{
-		ereport(elevel,
+		ereport(WARNING,
 				(errcode(ERRCODE_OBJECT_IN_USE),
 				 errmsg("pg_class entry for relid %u not found", reloid)));
 		table_close(crel, RowExclusiveLock);
@@ -169,16 +169,6 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	return result;
 }
 
-/*
- * Set statistics for a given pg_class entry.
- */
-Datum
-pg_set_relation_stats(PG_FUNCTION_ARGS)
-{
-	relation_statistics_update(fcinfo, ERROR);
-	PG_RETURN_VOID();
-}
-
 /*
  * Clear statistics for a given pg_class entry; that is, set back to initial
  * stats for a newly-created table.
@@ -199,7 +189,7 @@ pg_clear_relation_stats(PG_FUNCTION_ARGS)
 	newfcinfo->args[3].value = UInt32GetDatum(0);
 	newfcinfo->args[3].isnull = false;
 
-	relation_statistics_update(newfcinfo, ERROR);
+	relation_statistics_update(newfcinfo);
 	PG_RETURN_VOID();
 }
 
@@ -214,10 +204,10 @@ pg_restore_relation_stats(PG_FUNCTION_ARGS)
 							 InvalidOid, NULL, NULL);
 
 	if (!stats_fill_fcinfo_from_arg_pairs(fcinfo, positional_fcinfo,
-										  relarginfo, WARNING))
+										  relarginfo))
 		result = false;
 
-	if (!relation_statistics_update(positional_fcinfo, WARNING))
+	if (!relation_statistics_update(positional_fcinfo))
 		result = false;
 
 	PG_RETURN_BOOL(result);
diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c
index e70ea1ce738..54ead90b5bb 100644
--- a/src/backend/statistics/stat_utils.c
+++ b/src/backend/statistics/stat_utils.c
@@ -48,13 +48,13 @@ stats_check_required_arg(FunctionCallInfo fcinfo,
  * Check that argument is either NULL or a one dimensional array with no
  * NULLs.
  *
- * If a problem is found, emit at elevel, and return false. Otherwise return
+ * If a problem is found, emit a WARNING, and return false. Otherwise return
  * true.
  */
 bool
 stats_check_arg_array(FunctionCallInfo fcinfo,
 					  struct StatsArgInfo *arginfo,
-					  int argnum, int elevel)
+					  int argnum)
 {
 	ArrayType  *arr;
 
@@ -65,7 +65,7 @@ stats_check_arg_array(FunctionCallInfo fcinfo,
 
 	if (ARR_NDIM(arr) != 1)
 	{
-		ereport(elevel,
+		ereport(WARNING,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("\"%s\" cannot be a multidimensional array",
 						arginfo[argnum].argname)));
@@ -74,7 +74,7 @@ stats_check_arg_array(FunctionCallInfo fcinfo,
 
 	if (array_contains_nulls(arr))
 	{
-		ereport(elevel,
+		ereport(WARNING,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("\"%s\" array cannot contain NULL values",
 						arginfo[argnum].argname)));
@@ -89,13 +89,13 @@ stats_check_arg_array(FunctionCallInfo fcinfo,
  * a particular stakind, such as most_common_vals and most_common_freqs for
  * STATISTIC_KIND_MCV.
  *
- * If a problem is found, emit at elevel, and return false. Otherwise return
+ * If a problem is found, emit a WARNING, and return false. Otherwise return
  * true.
  */
 bool
 stats_check_arg_pair(FunctionCallInfo fcinfo,
 					 struct StatsArgInfo *arginfo,
-					 int argnum1, int argnum2, int elevel)
+					 int argnum1, int argnum2)
 {
 	if (PG_ARGISNULL(argnum1) && PG_ARGISNULL(argnum2))
 		return true;
@@ -105,7 +105,7 @@ stats_check_arg_pair(FunctionCallInfo fcinfo,
 		int			nullarg = PG_ARGISNULL(argnum1) ? argnum1 : argnum2;
 		int			otherarg = PG_ARGISNULL(argnum1) ? argnum2 : argnum1;
 
-		ereport(elevel,
+		ereport(WARNING,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("\"%s\" must be specified when \"%s\" is specified",
 						arginfo[nullarg].argname,
@@ -216,7 +216,7 @@ stats_lock_check_privileges(Oid reloid)
  * found.
  */
 static int
-get_arg_by_name(const char *argname, struct StatsArgInfo *arginfo, int elevel)
+get_arg_by_name(const char *argname, struct StatsArgInfo *arginfo)
 {
 	int			argnum;
 
@@ -224,7 +224,7 @@ get_arg_by_name(const char *argname, struct StatsArgInfo *arginfo, int elevel)
 		if (pg_strcasecmp(argname, arginfo[argnum].argname) == 0)
 			return argnum;
 
-	ereport(elevel,
+	ereport(WARNING,
 			(errmsg("unrecognized argument name: \"%s\"", argname)));
 
 	return -1;
@@ -234,11 +234,11 @@ get_arg_by_name(const char *argname, struct StatsArgInfo *arginfo, int elevel)
  * Ensure that a given argument matched the expected type.
  */
 static bool
-stats_check_arg_type(const char *argname, Oid argtype, Oid expectedtype, int elevel)
+stats_check_arg_type(const char *argname, Oid argtype, Oid expectedtype)
 {
 	if (argtype != expectedtype)
 	{
-		ereport(elevel,
+		ereport(WARNING,
 				(errmsg("argument \"%s\" has type \"%s\", expected type \"%s\"",
 						argname, format_type_be(argtype),
 						format_type_be(expectedtype))));
@@ -260,8 +260,7 @@ stats_check_arg_type(const char *argname, Oid argtype, Oid expectedtype, int ele
 bool
 stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
 								 FunctionCallInfo positional_fcinfo,
-								 struct StatsArgInfo *arginfo,
-								 int elevel)
+								 struct StatsArgInfo *arginfo)
 {
 	Datum	   *args;
 	bool	   *argnulls;
@@ -319,11 +318,10 @@ stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
 		if (pg_strcasecmp(argname, "version") == 0)
 			continue;
 
-		argnum = get_arg_by_name(argname, arginfo, elevel);
+		argnum = get_arg_by_name(argname, arginfo);
 
 		if (argnum < 0 || !stats_check_arg_type(argname, types[i + 1],
-												arginfo[argnum].argtype,
-												elevel))
+												arginfo[argnum].argtype))
 		{
 			result = false;
 			continue;
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index d6713eacc2c..7c7784efaf1 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -12,6 +12,7 @@ CREATE TABLE stats_import.test(
     arange int4range,
     tags text[]
 ) WITH (autovacuum_enabled = false);
+CREATE INDEX test_i ON stats_import.test(id);
 -- starting stats
 SELECT relpages, reltuples, relallvisible
 FROM pg_class
@@ -21,80 +22,15 @@ WHERE oid = 'stats_import.test'::regclass;
         0 |        -1 |             0
 (1 row)
 
--- error: regclass not found
-SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 0::Oid,
-        relpages => 17::integer,
-        reltuples => 400.0::real,
-        relallvisible => 4::integer);
-ERROR:  could not open relation with OID 0
--- relpages default
-SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.test'::regclass,
-        relpages => NULL::integer,
-        reltuples => 400.0::real,
-        relallvisible => 4::integer);
- pg_set_relation_stats 
------------------------
- 
-(1 row)
-
--- reltuples default
-SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.test'::regclass,
-        relpages => 17::integer,
-        reltuples => NULL::real,
-        relallvisible => 4::integer);
- pg_set_relation_stats 
------------------------
- 
-(1 row)
-
--- relallvisible default
-SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.test'::regclass,
-        relpages => 17::integer,
-        reltuples => 400.0::real,
-        relallvisible => NULL::integer);
- pg_set_relation_stats 
------------------------
- 
-(1 row)
-
--- named arguments
-SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.test'::regclass,
-        relpages => 17::integer,
-        reltuples => 400.0::real,
-        relallvisible => 4::integer);
- pg_set_relation_stats 
------------------------
- 
-(1 row)
-
-SELECT relpages, reltuples, relallvisible
-FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
- relpages | reltuples | relallvisible 
-----------+-----------+---------------
-       17 |       400 |             4
-(1 row)
-
-CREATE INDEX test_i ON stats_import.test(id);
 BEGIN;
 -- regular indexes have special case locking rules
 SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.test_i'::regclass,
-        relpages => 18::integer);
- pg_set_relation_stats 
------------------------
- 
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.test_i'::regclass,
+        'relpages', 18::integer);
+ pg_restore_relation_stats 
+---------------------------
+ t
 (1 row)
 
 SELECT mode FROM pg_locks
@@ -123,34 +59,6 @@ SELECT
  t
 (1 row)
 
--- positional arguments
-SELECT
-    pg_catalog.pg_set_relation_stats(
-        'stats_import.test'::regclass,
-        18::integer,
-        401.0::real,
-        5::integer);
- pg_set_relation_stats 
------------------------
- 
-(1 row)
-
-SELECT relpages, reltuples, relallvisible
-FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
- relpages | reltuples | relallvisible 
-----------+-----------+---------------
-       18 |       401 |             5
-(1 row)
-
-SELECT relpages, reltuples, relallvisible
-FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
- relpages | reltuples | relallvisible 
-----------+-----------+---------------
-       18 |       401 |             5
-(1 row)
-
 -- clear
 SELECT
     pg_catalog.pg_clear_relation_stats(
@@ -200,21 +108,21 @@ WHERE oid = 'stats_import.part_parent'::regclass;
 -- although partitioned tables have no storage, setting relpages to a
 -- positive value is still allowed
 SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.part_parent_i'::regclass,
-        relpages => 2::integer);
- pg_set_relation_stats 
------------------------
- 
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.part_parent_i'::regclass,
+        'relpages', 2::integer);
+ pg_restore_relation_stats 
+---------------------------
+ t
 (1 row)
 
 SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.part_parent'::regclass,
-        relpages => 2::integer);
- pg_set_relation_stats 
------------------------
- 
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.part_parent'::regclass,
+        'relpages', 2::integer);
+ pg_restore_relation_stats 
+---------------------------
+ t
 (1 row)
 
 --
@@ -225,12 +133,12 @@ SELECT
 --
 BEGIN;
 SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.part_parent_i'::regclass,
-        relpages => 2::integer);
- pg_set_relation_stats 
------------------------
- 
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.part_parent_i'::regclass,
+        'relpages', 2::integer);
+ pg_restore_relation_stats 
+---------------------------
+ t
 (1 row)
 
 SELECT mode FROM pg_locks
@@ -261,56 +169,14 @@ SELECT
 
 -- nothing stops us from setting it to -1
 SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.part_parent'::regclass,
-        relpages => -1::integer);
- pg_set_relation_stats 
------------------------
- 
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.part_parent'::regclass,
+        'relpages', -1::integer);
+ pg_restore_relation_stats 
+---------------------------
+ t
 (1 row)
 
--- error: object doesn't exist
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => '0'::oid,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.1::real,
-    avg_width => 2::integer,
-    n_distinct => 0.3::real);
-ERROR:  could not open relation with OID 0
--- error: object doesn't exist
-SELECT pg_catalog.pg_clear_attribute_stats(
-    relation => '0'::oid,
-    attname => 'id'::name,
-    inherited => false::boolean);
-ERROR:  could not open relation with OID 0
--- error: relation null
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => NULL::oid,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.1::real,
-    avg_width => 2::integer,
-    n_distinct => 0.3::real);
-ERROR:  "relation" cannot be NULL
--- error: attribute is system column
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'xmin'::name,
-    inherited => false::boolean,
-    null_frac => 0.1::real,
-    avg_width => 2::integer,
-    n_distinct => 0.3::real);
-ERROR:  cannot modify statistics on system column "xmin"
--- error: attname doesn't exist
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'nope'::name,
-    inherited => false::boolean,
-    null_frac => 0.1::real,
-    avg_width => 2::integer,
-    n_distinct => 0.3::real);
-ERROR:  column "nope" of relation "test" does not exist
 -- error: attribute is system column
 SELECT pg_catalog.pg_clear_attribute_stats(
     relation => 'stats_import.test'::regclass,
@@ -323,432 +189,6 @@ SELECT pg_catalog.pg_clear_attribute_stats(
     attname => 'nope'::name,
     inherited => false::boolean);
 ERROR:  column "nope" of relation "test" does not exist
--- error: attname null
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => NULL::name,
-    inherited => false::boolean,
-    null_frac => 0.1::real,
-    avg_width => 2::integer,
-    n_distinct => 0.3::real);
-ERROR:  "attname" cannot be NULL
--- error: inherited null
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => NULL::boolean,
-    null_frac => 0.1::real,
-    avg_width => 2::integer,
-    n_distinct => 0.3::real);
-ERROR:  "inherited" cannot be NULL
--- ok: no stakinds
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.1::real,
-    avg_width => 2::integer,
-    n_distinct => 0.3::real);
- pg_set_attribute_stats 
-------------------------
- 
-(1 row)
-
-SELECT stanullfrac, stawidth, stadistinct
-FROM pg_statistic
-WHERE starelid = 'stats_import.test'::regclass;
- stanullfrac | stawidth | stadistinct 
--------------+----------+-------------
-         0.1 |        2 |         0.3
-(1 row)
-
--- error: mcv / mcf null mismatch
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_freqs => '{0.1,0.2,0.3}'::real[]
-    );
-ERROR:  "most_common_vals" must be specified when "most_common_freqs" is specified
--- error: mcv / mcf null mismatch part 2
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_vals => '{1,2,3}'::text
-    );
-ERROR:  "most_common_freqs" must be specified when "most_common_vals" is specified
--- error: mcv / mcf type mismatch
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_vals => '{2023-09-30,2024-10-31,3}'::text,
-    most_common_freqs => '{0.2,0.1}'::real[]
-    );
-ERROR:  invalid input syntax for type integer: "2023-09-30"
--- error: mcv cast failure
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_vals => '{2,four,3}'::text,
-    most_common_freqs => '{0.3,0.25,0.05}'::real[]
-    );
-ERROR:  invalid input syntax for type integer: "four"
--- ok: mcv+mcf
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_vals => '{2,1,3}'::text,
-    most_common_freqs => '{0.3,0.25,0.05}'::real[]
-    );
- pg_set_attribute_stats 
-------------------------
- 
-(1 row)
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'id';
-  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
---------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
-(1 row)
-
--- error: histogram elements null value
--- this generates no warnings, but perhaps it should
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    histogram_bounds => '{1,NULL,3,4}'::text
-    );
-ERROR:  "histogram_bounds" array cannot contain NULL values
--- ok: histogram_bounds
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    histogram_bounds => '{1,2,3,4}'::text
-    );
- pg_set_attribute_stats 
-------------------------
- 
-(1 row)
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'id';
-  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
---------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
-(1 row)
-
--- ok: correlation
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    correlation => 0.5::real);
- pg_set_attribute_stats 
-------------------------
- 
-(1 row)
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'id';
-  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
---------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |         0.5 |                   |                        |                      |                        |                  | 
-(1 row)
-
--- error: scalars can't have mcelem
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_elems => '{1,3}'::text,
-    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
-    );
-ERROR:  unable to determine element type of attribute "id"
-DETAIL:  Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.
--- error: mcelem / mcelem mismatch
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'tags'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_elems => '{one,two}'::text
-    );
-ERROR:  "most_common_elem_freqs" must be specified when "most_common_elems" is specified
--- error: mcelem / mcelem null mismatch part 2
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'tags'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
-    );
-ERROR:  "most_common_elems" must be specified when "most_common_elem_freqs" is specified
--- ok: mcelem
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'tags'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_elems => '{one,three}'::text,
-    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
-    );
- pg_set_attribute_stats 
-------------------------
- 
-(1 row)
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'tags';
-  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
---------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             | {one,three}       | {0.3,0.2,0.2,0.3,0}    |                      |                        |                  | 
-(1 row)
-
--- error: scalars can't have elem_count_histogram
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
-    );
-ERROR:  unable to determine element type of attribute "id"
-DETAIL:  Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.
--- error: elem_count_histogram null element
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'tags'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    elem_count_histogram => '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
-    );
-ERROR:  "elem_count_histogram" array cannot contain NULL values
--- ok: elem_count_histogram
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'tags'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
-    );
- pg_set_attribute_stats 
-------------------------
- 
-(1 row)
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'tags';
-  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
---------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
- stats_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             | {one,three}       | {0.3,0.2,0.2,0.3,0}    | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
-(1 row)
-
--- error: scalars can't have range stats
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    range_empty_frac => 0.5::real,
-    range_length_histogram => '{399,499,Infinity}'::text
-    );
-ERROR:  attribute "id" is not a range type
-DETAIL:  Cannot set STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM or STATISTIC_KIND_BOUNDS_HISTOGRAM.
--- error: range_empty_frac range_length_hist null mismatch
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'arange'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    range_length_histogram => '{399,499,Infinity}'::text
-    );
-ERROR:  "range_empty_frac" must be specified when "range_length_histogram" is specified
--- error: range_empty_frac range_length_hist null mismatch part 2
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'arange'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    range_empty_frac => 0.5::real
-    );
-ERROR:  "range_length_histogram" must be specified when "range_empty_frac" is specified
--- ok: range_empty_frac + range_length_hist
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'arange'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    range_empty_frac => 0.5::real,
-    range_length_histogram => '{399,499,Infinity}'::text
-    );
- pg_set_attribute_stats 
-------------------------
- 
-(1 row)
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'arange';
-  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
---------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
-(1 row)
-
--- error: scalars can't have range stats
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
-    );
-ERROR:  attribute "id" is not a range type
-DETAIL:  Cannot set STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM or STATISTIC_KIND_BOUNDS_HISTOGRAM.
--- ok: range_bounds_histogram
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'arange'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
-    );
- pg_set_attribute_stats 
-------------------------
- 
-(1 row)
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'arange';
-  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
---------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
- stats_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
-(1 row)
-
--- error: cannot set most_common_elems for range type
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'arange'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_vals => '{"[2,3)","[1,2)","[3,4)"}'::text,
-    most_common_freqs => '{0.3,0.25,0.05}'::real[],
-    histogram_bounds => '{"[1,2)","[2,3)","[3,4)","[4,5)"}'::text,
-    correlation => 1.1::real,
-    most_common_elems => '{3,1}'::text,
-    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[],
-    range_empty_frac => -0.5::real,
-    range_length_histogram => '{399,499,Infinity}'::text,
-    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
-    );
-ERROR:  unable to determine element type of attribute "arange"
-DETAIL:  Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.
---
--- Clear attribute stats to try again with restore functions
--- (relation stats were already cleared).
---
-SELECT
-  pg_catalog.pg_clear_attribute_stats(
-        'stats_import.test'::regclass,
-        s.attname,
-        s.inherited)
-FROM pg_catalog.pg_stats AS s
-WHERE s.schemaname = 'stats_import'
-AND s.tablename = 'test'
-ORDER BY s.attname, s.inherited;
- pg_clear_attribute_stats 
---------------------------
- 
- 
- 
-(3 rows)
-
 -- reject: argument name is NULL
 SELECT pg_restore_relation_stats(
         'relation', '0'::oid::regclass,
@@ -1472,216 +912,6 @@ CREATE INDEX is_odd_clone ON stats_import.test_clone(((comp).a % 2 = 1));
 --
 -- Copy stats from test to test_clone, and is_odd to is_odd_clone
 --
-SELECT s.schemaname, s.tablename, s.attname, s.inherited
-FROM pg_catalog.pg_stats AS s
-CROSS JOIN LATERAL
-    pg_catalog.pg_set_attribute_stats(
-        relation => ('stats_import.' || s.tablename || '_clone')::regclass::oid,
-        attname => s.attname,
-        inherited => s.inherited,
-        null_frac => s.null_frac,
-        avg_width => s.avg_width,
-        n_distinct => s.n_distinct,
-        most_common_vals => s.most_common_vals::text,
-        most_common_freqs => s.most_common_freqs,
-        histogram_bounds => s.histogram_bounds::text,
-        correlation => s.correlation,
-        most_common_elems => s.most_common_elems::text,
-        most_common_elem_freqs => s.most_common_elem_freqs,
-        elem_count_histogram => s.elem_count_histogram,
-        range_bounds_histogram => s.range_bounds_histogram::text,
-        range_empty_frac => s.range_empty_frac,
-        range_length_histogram => s.range_length_histogram::text) AS r
-WHERE s.schemaname = 'stats_import'
-AND s.tablename IN ('test', 'is_odd')
-ORDER BY s.tablename, s.attname, s.inherited;
-  schemaname  | tablename | attname | inherited 
---------------+-----------+---------+-----------
- stats_import | is_odd    | expr    | f
- stats_import | test      | arange  | f
- stats_import | test      | comp    | f
- stats_import | test      | id      | f
- stats_import | test      | name    | f
- stats_import | test      | tags    | f
-(6 rows)
-
-SELECT c.relname, COUNT(*) AS num_stats
-FROM pg_class AS c
-JOIN pg_statistic s ON s.starelid = c.oid
-WHERE c.relnamespace = 'stats_import'::regnamespace
-AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
-GROUP BY c.relname
-ORDER BY c.relname;
-   relname    | num_stats 
---------------+-----------
- is_odd       |         1
- is_odd_clone |         1
- test         |         5
- test_clone   |         5
-(4 rows)
-
--- check test minus test_clone
-SELECT
-    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
-    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
-    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
-    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
-    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
-    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
-    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
-    s.stavalues5::text AS sv5, 'test' AS direction
-FROM pg_statistic s
-JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
-WHERE s.starelid = 'stats_import.test'::regclass
-EXCEPT
-SELECT
-    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
-    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
-    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
-    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
-    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
-    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
-    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
-    s.stavalues5::text AS sv5, 'test' AS direction
-FROM pg_statistic s
-JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
-WHERE s.starelid = 'stats_import.test_clone'::regclass;
- attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
----------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
-(0 rows)
-
--- check test_clone minus test
-SELECT
-    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
-    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
-    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
-    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
-    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
-    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
-    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
-    s.stavalues5::text AS sv5, 'test_clone' AS direction
-FROM pg_statistic s
-JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
-WHERE s.starelid = 'stats_import.test_clone'::regclass
-EXCEPT
-SELECT
-    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
-    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
-    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
-    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
-    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
-    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
-    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
-    s.stavalues5::text AS sv5, 'test_clone' AS direction
-FROM pg_statistic s
-JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
-WHERE s.starelid = 'stats_import.test'::regclass;
- attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
----------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
-(0 rows)
-
--- check is_odd minus is_odd_clone
-SELECT
-    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
-    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
-    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
-    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
-    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
-    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
-    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
-    s.stavalues5::text AS sv5, 'is_odd' AS direction
-FROM pg_statistic s
-JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
-WHERE s.starelid = 'stats_import.is_odd'::regclass
-EXCEPT
-SELECT
-    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
-    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
-    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
-    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
-    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
-    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
-    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
-    s.stavalues5::text AS sv5, 'is_odd' AS direction
-FROM pg_statistic s
-JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
-WHERE s.starelid = 'stats_import.is_odd_clone'::regclass;
- attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
----------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
-(0 rows)
-
--- check is_odd_clone minus is_odd
-SELECT
-    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
-    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
-    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
-    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
-    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
-    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
-    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
-    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
-FROM pg_statistic s
-JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
-WHERE s.starelid = 'stats_import.is_odd_clone'::regclass
-EXCEPT
-SELECT
-    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
-    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
-    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
-    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
-    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
-    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
-    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
-    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
-FROM pg_statistic s
-JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
-WHERE s.starelid = 'stats_import.is_odd'::regclass;
- attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction 
----------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
-(0 rows)
-
---
-SELECT relpages, reltuples, relallvisible
-FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
- relpages | reltuples | relallvisible 
-----------+-----------+---------------
-        1 |         4 |             0
-(1 row)
-
---
--- Clear clone stats to try again with pg_restore_attribute_stats
---
-SELECT
-  pg_catalog.pg_clear_attribute_stats(
-        ('stats_import.' || s.tablename)::regclass,
-        s.attname,
-        s.inherited)
-FROM pg_catalog.pg_stats AS s
-WHERE s.schemaname = 'stats_import'
-AND s.tablename IN ('test_clone', 'is_odd_clone')
-ORDER BY s.tablename, s.attname, s.inherited;
- pg_clear_attribute_stats 
---------------------------
- 
- 
- 
- 
- 
- 
-(6 rows)
-
-SELECT
-SELECT COUNT(*)
-FROM pg_catalog.pg_stats AS s
-WHERE s.schemaname = 'stats_import'
-AND s.tablename IN ('test_clone', 'is_odd_clone');
-ERROR:  syntax error at or near "SELECT"
-LINE 2: SELECT COUNT(*)
-        ^
---
--- Copy stats from test to test_clone, and is_odd to is_odd_clone
---
 SELECT s.schemaname, s.tablename, s.attname, s.inherited, r.*
 FROM pg_catalog.pg_stats AS s
 CROSS JOIN LATERAL
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index 9740ab3ff02..f26f7857748 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -15,63 +15,19 @@ CREATE TABLE stats_import.test(
     tags text[]
 ) WITH (autovacuum_enabled = false);
 
+CREATE INDEX test_i ON stats_import.test(id);
+
 -- starting stats
 SELECT relpages, reltuples, relallvisible
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
--- error: regclass not found
-SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 0::Oid,
-        relpages => 17::integer,
-        reltuples => 400.0::real,
-        relallvisible => 4::integer);
-
--- relpages default
-SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.test'::regclass,
-        relpages => NULL::integer,
-        reltuples => 400.0::real,
-        relallvisible => 4::integer);
-
--- reltuples default
-SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.test'::regclass,
-        relpages => 17::integer,
-        reltuples => NULL::real,
-        relallvisible => 4::integer);
-
--- relallvisible default
-SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.test'::regclass,
-        relpages => 17::integer,
-        reltuples => 400.0::real,
-        relallvisible => NULL::integer);
-
--- named arguments
-SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.test'::regclass,
-        relpages => 17::integer,
-        reltuples => 400.0::real,
-        relallvisible => 4::integer);
-
-SELECT relpages, reltuples, relallvisible
-FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
-
-CREATE INDEX test_i ON stats_import.test(id);
-
 BEGIN;
 -- regular indexes have special case locking rules
 SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.test_i'::regclass,
-        relpages => 18::integer);
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.test_i'::regclass,
+        'relpages', 18::integer);
 
 SELECT mode FROM pg_locks
 WHERE relation = 'stats_import.test'::regclass AND
@@ -88,22 +44,6 @@ SELECT
         'relation', 'stats_import.test_i'::regclass,
         'relpages', 19::integer );
 
--- positional arguments
-SELECT
-    pg_catalog.pg_set_relation_stats(
-        'stats_import.test'::regclass,
-        18::integer,
-        401.0::real,
-        5::integer);
-
-SELECT relpages, reltuples, relallvisible
-FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
-
-SELECT relpages, reltuples, relallvisible
-FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
-
 -- clear
 SELECT
     pg_catalog.pg_clear_relation_stats(
@@ -141,14 +81,14 @@ WHERE oid = 'stats_import.part_parent'::regclass;
 -- although partitioned tables have no storage, setting relpages to a
 -- positive value is still allowed
 SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.part_parent_i'::regclass,
-        relpages => 2::integer);
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.part_parent_i'::regclass,
+        'relpages', 2::integer);
 
 SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.part_parent'::regclass,
-        relpages => 2::integer);
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.part_parent'::regclass,
+        'relpages', 2::integer);
 
 --
 -- Partitioned indexes aren't analyzed but it is possible to set
@@ -159,9 +99,9 @@ SELECT
 BEGIN;
 
 SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.part_parent_i'::regclass,
-        relpages => 2::integer);
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.part_parent_i'::regclass,
+        'relpages', 2::integer);
 
 SELECT mode FROM pg_locks
 WHERE relation = 'stats_import.part_parent'::regclass AND
@@ -180,51 +120,9 @@ SELECT
 
 -- nothing stops us from setting it to -1
 SELECT
-    pg_catalog.pg_set_relation_stats(
-        relation => 'stats_import.part_parent'::regclass,
-        relpages => -1::integer);
-
--- error: object doesn't exist
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => '0'::oid,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.1::real,
-    avg_width => 2::integer,
-    n_distinct => 0.3::real);
-
--- error: object doesn't exist
-SELECT pg_catalog.pg_clear_attribute_stats(
-    relation => '0'::oid,
-    attname => 'id'::name,
-    inherited => false::boolean);
-
--- error: relation null
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => NULL::oid,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.1::real,
-    avg_width => 2::integer,
-    n_distinct => 0.3::real);
-
--- error: attribute is system column
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'xmin'::name,
-    inherited => false::boolean,
-    null_frac => 0.1::real,
-    avg_width => 2::integer,
-    n_distinct => 0.3::real);
-
--- error: attname doesn't exist
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'nope'::name,
-    inherited => false::boolean,
-    null_frac => 0.1::real,
-    avg_width => 2::integer,
-    n_distinct => 0.3::real);
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.part_parent'::regclass,
+        'relpages', -1::integer);
 
 -- error: attribute is system column
 SELECT pg_catalog.pg_clear_attribute_stats(
@@ -238,351 +136,6 @@ SELECT pg_catalog.pg_clear_attribute_stats(
     attname => 'nope'::name,
     inherited => false::boolean);
 
--- error: attname null
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => NULL::name,
-    inherited => false::boolean,
-    null_frac => 0.1::real,
-    avg_width => 2::integer,
-    n_distinct => 0.3::real);
-
--- error: inherited null
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => NULL::boolean,
-    null_frac => 0.1::real,
-    avg_width => 2::integer,
-    n_distinct => 0.3::real);
-
--- ok: no stakinds
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.1::real,
-    avg_width => 2::integer,
-    n_distinct => 0.3::real);
-
-SELECT stanullfrac, stawidth, stadistinct
-FROM pg_statistic
-WHERE starelid = 'stats_import.test'::regclass;
-
--- error: mcv / mcf null mismatch
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_freqs => '{0.1,0.2,0.3}'::real[]
-    );
-
--- error: mcv / mcf null mismatch part 2
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_vals => '{1,2,3}'::text
-    );
-
--- error: mcv / mcf type mismatch
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_vals => '{2023-09-30,2024-10-31,3}'::text,
-    most_common_freqs => '{0.2,0.1}'::real[]
-    );
-
--- error: mcv cast failure
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_vals => '{2,four,3}'::text,
-    most_common_freqs => '{0.3,0.25,0.05}'::real[]
-    );
-
--- ok: mcv+mcf
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_vals => '{2,1,3}'::text,
-    most_common_freqs => '{0.3,0.25,0.05}'::real[]
-    );
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'id';
-
--- error: histogram elements null value
--- this generates no warnings, but perhaps it should
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    histogram_bounds => '{1,NULL,3,4}'::text
-    );
-
--- ok: histogram_bounds
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    histogram_bounds => '{1,2,3,4}'::text
-    );
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'id';
-
--- ok: correlation
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    correlation => 0.5::real);
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'id';
-
--- error: scalars can't have mcelem
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_elems => '{1,3}'::text,
-    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
-    );
-
--- error: mcelem / mcelem mismatch
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'tags'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_elems => '{one,two}'::text
-    );
-
--- error: mcelem / mcelem null mismatch part 2
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'tags'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
-    );
-
--- ok: mcelem
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'tags'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_elems => '{one,three}'::text,
-    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
-    );
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'tags';
-
--- error: scalars can't have elem_count_histogram
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
-    );
--- error: elem_count_histogram null element
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'tags'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    elem_count_histogram => '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
-    );
--- ok: elem_count_histogram
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'tags'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
-    );
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'tags';
-
--- error: scalars can't have range stats
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    range_empty_frac => 0.5::real,
-    range_length_histogram => '{399,499,Infinity}'::text
-    );
--- error: range_empty_frac range_length_hist null mismatch
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'arange'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    range_length_histogram => '{399,499,Infinity}'::text
-    );
--- error: range_empty_frac range_length_hist null mismatch part 2
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'arange'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    range_empty_frac => 0.5::real
-    );
--- ok: range_empty_frac + range_length_hist
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'arange'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    range_empty_frac => 0.5::real,
-    range_length_histogram => '{399,499,Infinity}'::text
-    );
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'arange';
-
--- error: scalars can't have range stats
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'id'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
-    );
--- ok: range_bounds_histogram
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'arange'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
-    );
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'arange';
-
--- error: cannot set most_common_elems for range type
-SELECT pg_catalog.pg_set_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'arange'::name,
-    inherited => false::boolean,
-    null_frac => 0.5::real,
-    avg_width => 2::integer,
-    n_distinct => -0.1::real,
-    most_common_vals => '{"[2,3)","[1,2)","[3,4)"}'::text,
-    most_common_freqs => '{0.3,0.25,0.05}'::real[],
-    histogram_bounds => '{"[1,2)","[2,3)","[3,4)","[4,5)"}'::text,
-    correlation => 1.1::real,
-    most_common_elems => '{3,1}'::text,
-    most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[],
-    range_empty_frac => -0.5::real,
-    range_length_histogram => '{399,499,Infinity}'::text,
-    range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
-    );
-
---
--- Clear attribute stats to try again with restore functions
--- (relation stats were already cleared).
---
-SELECT
-  pg_catalog.pg_clear_attribute_stats(
-        'stats_import.test'::regclass,
-        s.attname,
-        s.inherited)
-FROM pg_catalog.pg_stats AS s
-WHERE s.schemaname = 'stats_import'
-AND s.tablename = 'test'
-ORDER BY s.attname, s.inherited;
-
 -- reject: argument name is NULL
 SELECT pg_restore_relation_stats(
         'relation', '0'::oid::regclass,
@@ -1105,173 +658,6 @@ CREATE TABLE stats_import.test_clone ( LIKE stats_import.test )
 
 CREATE INDEX is_odd_clone ON stats_import.test_clone(((comp).a % 2 = 1));
 
---
--- Copy stats from test to test_clone, and is_odd to is_odd_clone
---
-SELECT s.schemaname, s.tablename, s.attname, s.inherited
-FROM pg_catalog.pg_stats AS s
-CROSS JOIN LATERAL
-    pg_catalog.pg_set_attribute_stats(
-        relation => ('stats_import.' || s.tablename || '_clone')::regclass::oid,
-        attname => s.attname,
-        inherited => s.inherited,
-        null_frac => s.null_frac,
-        avg_width => s.avg_width,
-        n_distinct => s.n_distinct,
-        most_common_vals => s.most_common_vals::text,
-        most_common_freqs => s.most_common_freqs,
-        histogram_bounds => s.histogram_bounds::text,
-        correlation => s.correlation,
-        most_common_elems => s.most_common_elems::text,
-        most_common_elem_freqs => s.most_common_elem_freqs,
-        elem_count_histogram => s.elem_count_histogram,
-        range_bounds_histogram => s.range_bounds_histogram::text,
-        range_empty_frac => s.range_empty_frac,
-        range_length_histogram => s.range_length_histogram::text) AS r
-WHERE s.schemaname = 'stats_import'
-AND s.tablename IN ('test', 'is_odd')
-ORDER BY s.tablename, s.attname, s.inherited;
-
-SELECT c.relname, COUNT(*) AS num_stats
-FROM pg_class AS c
-JOIN pg_statistic s ON s.starelid = c.oid
-WHERE c.relnamespace = 'stats_import'::regnamespace
-AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
-GROUP BY c.relname
-ORDER BY c.relname;
-
--- check test minus test_clone
-SELECT
-    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
-    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
-    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
-    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
-    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
-    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
-    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
-    s.stavalues5::text AS sv5, 'test' AS direction
-FROM pg_statistic s
-JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
-WHERE s.starelid = 'stats_import.test'::regclass
-EXCEPT
-SELECT
-    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
-    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
-    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
-    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
-    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
-    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
-    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
-    s.stavalues5::text AS sv5, 'test' AS direction
-FROM pg_statistic s
-JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
-WHERE s.starelid = 'stats_import.test_clone'::regclass;
-
--- check test_clone minus test
-SELECT
-    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
-    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
-    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
-    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
-    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
-    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
-    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
-    s.stavalues5::text AS sv5, 'test_clone' AS direction
-FROM pg_statistic s
-JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
-WHERE s.starelid = 'stats_import.test_clone'::regclass
-EXCEPT
-SELECT
-    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
-    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
-    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
-    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
-    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
-    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
-    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
-    s.stavalues5::text AS sv5, 'test_clone' AS direction
-FROM pg_statistic s
-JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
-WHERE s.starelid = 'stats_import.test'::regclass;
-
--- check is_odd minus is_odd_clone
-SELECT
-    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
-    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
-    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
-    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
-    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
-    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
-    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
-    s.stavalues5::text AS sv5, 'is_odd' AS direction
-FROM pg_statistic s
-JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
-WHERE s.starelid = 'stats_import.is_odd'::regclass
-EXCEPT
-SELECT
-    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
-    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
-    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
-    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
-    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
-    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
-    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
-    s.stavalues5::text AS sv5, 'is_odd' AS direction
-FROM pg_statistic s
-JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
-WHERE s.starelid = 'stats_import.is_odd_clone'::regclass;
-
--- check is_odd_clone minus is_odd
-SELECT
-    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
-    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
-    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
-    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
-    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
-    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
-    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
-    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
-FROM pg_statistic s
-JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
-WHERE s.starelid = 'stats_import.is_odd_clone'::regclass
-EXCEPT
-SELECT
-    a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
-    s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
-    s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
-    s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
-    s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
-    s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
-    s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
-    s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
-FROM pg_statistic s
-JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
-WHERE s.starelid = 'stats_import.is_odd'::regclass;
-
---
-SELECT relpages, reltuples, relallvisible
-FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
-
---
--- Clear clone stats to try again with pg_restore_attribute_stats
---
-SELECT
-  pg_catalog.pg_clear_attribute_stats(
-        ('stats_import.' || s.tablename)::regclass,
-        s.attname,
-        s.inherited)
-FROM pg_catalog.pg_stats AS s
-WHERE s.schemaname = 'stats_import'
-AND s.tablename IN ('test_clone', 'is_odd_clone')
-ORDER BY s.tablename, s.attname, s.inherited;
-SELECT
-
-SELECT COUNT(*)
-FROM pg_catalog.pg_stats AS s
-WHERE s.schemaname = 'stats_import'
-AND s.tablename IN ('test_clone', 'is_odd_clone');
-
 --
 -- Copy stats from test to test_clone, and is_odd to is_odd_clone
 --
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index f0ccb751106..12206e0cfc6 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30181,41 +30181,72 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
 
      <tbody>
       <row>
-       <entry role="func_table_entry">
-        <para role="func_signature">
-         <indexterm>
-          <primary>pg_set_relation_stats</primary>
-         </indexterm>
-         <function>pg_set_relation_stats</function> (
-         <parameter>relation</parameter> <type>regclass</type>
-         <optional>, <parameter>relpages</parameter> <type>integer</type></optional>
-         <optional>, <parameter>reltuples</parameter> <type>real</type></optional>
-         <optional>, <parameter>relallvisible</parameter> <type>integer</type></optional> )
-         <returnvalue>void</returnvalue>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_restore_relation_stats</primary>
+        </indexterm>
+        <function>pg_restore_relation_stats</function> (
+        <literal>VARIADIC</literal> <parameter>kwargs</parameter> <type>"any"</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+        <para>
+         Updates table-level statistics.  Ordinarily, these statistics are
+         collected automatically or updated as a part of <xref
+         linkend="sql-vacuum"/> or <xref linkend="sql-analyze"/>, so it's not
+         necessary to call this function.  However, it is useful after a
+         restore to enable the optimizer to choose better plans if
+         <command>ANALYZE</command> has not been run yet.
         </para>
         <para>
-         Updates relation-level statistics for the given relation to the
-         specified values. The parameters correspond to columns in <link
-         linkend="catalog-pg-class"><structname>pg_class</structname></link>. Unspecified
-         or <literal>NULL</literal> values leave the setting unchanged.
+         The tracked statistics may change from version to version, so
+         arguments are passed as pairs of <replaceable>argname</replaceable>
+         and <replaceable>argvalue</replaceable> in the form:
+<programlisting>
+ SELECT pg_restore_relation_stats(
+    '<replaceable>arg1name</replaceable>', '<replaceable>arg1value</replaceable>'::<replaceable>arg1type</replaceable>,
+    '<replaceable>arg2name</replaceable>', '<replaceable>arg2value</replaceable>'::<replaceable>arg2type</replaceable>,
+    '<replaceable>arg3name</replaceable>', '<replaceable>arg3value</replaceable>'::<replaceable>arg3type</replaceable>);
+</programlisting>
         </para>
         <para>
-         Ordinarily, these statistics are collected automatically or updated
-         as a part of <xref linkend="sql-vacuum"/> or <xref
-         linkend="sql-analyze"/>, so it's not necessary to call this
-         function. However, it may be useful when testing the effects of
-         statistics on the planner to understand or anticipate plan changes.
+         For example, to set the <structname>relpages</structname> and
+         <structname>reltuples</structname> of the table
+         <structname>mytable</structname>:
+<programlisting>
+ SELECT pg_restore_relation_stats(
+    'relation',  'mytable'::regclass,
+    'relpages',  173::integer,
+    'reltuples', 10000::real);
+</programlisting>
         </para>
         <para>
-         The caller must have the <literal>MAINTAIN</literal> privilege on
-         the table or be the owner of the database.
+         The argument <literal>relation</literal> with a value of type
+         <type>regclass</type> is required, and specifies the table. Other
+         arguments are the names of statistics corresponding to certain
+         columns in <link
+         linkend="catalog-pg-class"><structname>pg_class</structname></link>.
+         The currently-supported relation statistics are
+         <literal>relpages</literal> with a value of type
+         <type>integer</type>, <literal>reltuples</literal> with a value of
+         type <type>real</type>, and <literal>relallvisible</literal> with a
+         value of type <type>integer</type>.
         </para>
         <para>
-         The value of <structfield>relpages</structfield> must be greater than
-         or equal to <literal>-1</literal>,
-         <structfield>reltuples</structfield> must be greater than or equal to
-         <literal>-1.0</literal>, and <structfield>relallvisible</structfield>
-         must be greater than or equal to <literal>0</literal>.
+         Additionally, this function supports argument name
+         <literal>version</literal> of type <type>integer</type>, which
+         specifies the version from which the statistics originated, improving
+         interpretation of statistics from older versions of
+         <productname>PostgreSQL</productname>.
+        </para>
+        <para>
+         Minor errors are reported as a <literal>WARNING</literal> and
+         ignored, and remaining statistics will still be restored. If all
+         specified statistics are successfully restored, return
+         <literal>true</literal>, otherwise <literal>false</literal>.
+        </para>
+        <para>
+         The caller must have the <literal>MAINTAIN</literal> privilege on the
+         table or be the owner of the database.
         </para>
        </entry>
       </row>
@@ -30234,8 +30265,8 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          table was newly created.
         </para>
         <para>
-         The caller must have the <literal>MAINTAIN</literal> privilege on
-         the table or be the owner of the database.
+         The caller must have the <literal>MAINTAIN</literal> privilege on the
+         table or be the owner of the database.
         </para>
        </entry>
       </row>
@@ -30243,42 +30274,61 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
-         <primary>pg_restore_relation_stats</primary>
+         <primary>pg_restore_attribute_stats</primary>
         </indexterm>
-        <function>pg_restore_relation_stats</function> (
+        <function>pg_restore_attribute_stats</function> (
         <literal>VARIADIC</literal> <parameter>kwargs</parameter> <type>"any"</type> )
         <returnvalue>boolean</returnvalue>
+       </para>
+        <para>
+         Create or update column-level statistics.  Ordinarily, these
+         statistics are collected automatically or updated as a part of <xref
+         linkend="sql-vacuum"/> or <xref linkend="sql-analyze"/>, so it's not
+         necessary to call this function.  However, it is useful after a
+         restore to enable the optimizer to choose better plans if
+         <command>ANALYZE</command> has not been run yet.
         </para>
         <para>
-         Similar to <function>pg_set_relation_stats()</function>, but intended
-         for bulk restore of relation statistics. The tracked statistics may
-         change from version to version, so the primary purpose of this
-         function is to maintain a consistent function signature to avoid
-         errors when restoring statistics from previous versions.
+         The tracked statistics may change from version to version, so
+         arguments are passed as pairs of <replaceable>argname</replaceable>
+         and <replaceable>argvalue</replaceable> in the form:
+<programlisting>
+ SELECT pg_restore_attribute_stats(
+    '<replaceable>arg1name</replaceable>', '<replaceable>arg1value</replaceable>'::<replaceable>arg1type</replaceable>,
+    '<replaceable>arg2name</replaceable>', '<replaceable>arg2value</replaceable>'::<replaceable>arg2type</replaceable>,
+    '<replaceable>arg3name</replaceable>', '<replaceable>arg3value</replaceable>'::<replaceable>arg3type</replaceable>);
+</programlisting>
         </para>
         <para>
-         Arguments are passed as pairs of <replaceable>argname</replaceable>
-         and <replaceable>argvalue</replaceable>, where
-         <replaceable>argname</replaceable> corresponds to a named argument in
-         <function>pg_set_relation_stats()</function> and
-         <replaceable>argvalue</replaceable> is of the corresponding type.
+         For example, to set the <structname>avg_width</structname> and
+         <structname>null_frac</structname> for the attribute
+         <structname>col1</structname> of the table
+         <structname>mytable</structname>:
+<programlisting>
+ SELECT pg_restore_attribute_stats(
+    'relation',    'mytable'::regclass,
+    'attname',     'col1'::name,
+    'inherited',   false,
+    'avg_width',   125::integer,
+    'null_frac',   0.5::real);
+</programlisting>
+        </para>
+        <para>
+         The required arguments are <literal>relation</literal> with a value
+         of type <type>regclass</type>, which specifies the table;
+         <literal>attname</literal> with a value of type <type>name</type>,
+         which specifies the column; and <literal>inherited</literal>, which
+         specifies whether the statistics includes values from child tables.
+         Other arguments are the names of statistics corresponding to columns
+         in <link
+         linkend="view-pg-stats"><structname>pg_stats</structname></link>.
         </para>
         <para>
          Additionally, this function supports argument name
          <literal>version</literal> of type <type>integer</type>, which
          specifies the version from which the statistics originated, improving
-         interpretation of older statistics.
-        </para>
-        <para>
-         For example, to set the <structname>relpages</structname> and
-         <structname>reltuples</structname> of the table
-         <structname>mytable</structname>:
-<programlisting>
- SELECT pg_restore_relation_stats(
-    'relation',  'mytable'::regclass,
-    'relpages',  173::integer,
-    'reltuples', 10000::float4);
-</programlisting>
+         interpretation of statistics from older versions of
+         <productname>PostgreSQL</productname>.
         </para>
         <para>
          Minor errors are reported as a <literal>WARNING</literal> and
@@ -30286,53 +30336,9 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          specified statistics are successfully restored, return
          <literal>true</literal>, otherwise <literal>false</literal>.
         </para>
-       </entry>
-      </row>
-
-      <row>
-       <entry role="func_table_entry">
-        <para role="func_signature">
-         <indexterm>
-          <primary>pg_set_attribute_stats</primary>
-         </indexterm>
-         <function>pg_set_attribute_stats</function> (
-         <parameter>relation</parameter> <type>regclass</type>,
-         <parameter>attname</parameter> <type>name</type>,
-         <parameter>inherited</parameter> <type>boolean</type>
-         <optional>, <parameter>null_frac</parameter> <type>real</type></optional>
-         <optional>, <parameter>avg_width</parameter> <type>integer</type></optional>
-         <optional>, <parameter>n_distinct</parameter> <type>real</type></optional>
-         <optional>, <parameter>most_common_vals</parameter> <type>text</type>, <parameter>most_common_freqs</parameter> <type>real[]</type> </optional>
-         <optional>, <parameter>histogram_bounds</parameter> <type>text</type> </optional>
-         <optional>, <parameter>correlation</parameter> <type>real</type> </optional>
-         <optional>, <parameter>most_common_elems</parameter> <type>text</type>, <parameter>most_common_elem_freqs</parameter> <type>real[]</type> </optional>
-         <optional>, <parameter>elem_count_histogram</parameter> <type>real[]</type> </optional>
-         <optional>, <parameter>range_length_histogram</parameter> <type>text</type> </optional>
-         <optional>, <parameter>range_empty_frac</parameter> <type>real</type> </optional>
-         <optional>, <parameter>range_bounds_histogram</parameter> <type>text</type> </optional> )
-         <returnvalue>void</returnvalue>
-        </para>
         <para>
-         Creates or updates attribute-level statistics for the given relation
-         and attribute name to the specified values. The parameters correspond
-         to attributes of the same name found in the <link
-         linkend="view-pg-stats"><structname>pg_stats</structname></link>
-         view.
-        </para>
-        <para>
-         Optional parameters default to <literal>NULL</literal>, which leave
-         the corresponding statistic unchanged.
-        </para>
-        <para>
-         Ordinarily, these statistics are collected automatically or updated
-         as a part of <xref linkend="sql-vacuum"/> or <xref
-         linkend="sql-analyze"/>, so it's not necessary to call this
-         function. However, it may be useful when testing the effects of
-         statistics on the planner to understand or anticipate plan changes.
-        </para>
-        <para>
-         The caller must have the <literal>MAINTAIN</literal> privilege on
-         the table or be the owner of the database.
+         The caller must have the <literal>MAINTAIN</literal> privilege on the
+         table or be the owner of the database.
         </para>
        </entry>
       </row>
@@ -30350,8 +30356,8 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          <returnvalue>void</returnvalue>
         </para>
         <para>
-         Clears table-level statistics for the given relation attribute, as
-         though the table was newly created.
+         Clears column-level statistics for the given relation and
+         attribute, as though the table was newly created.
         </para>
         <para>
          The caller must have the <literal>MAINTAIN</literal> privilege on
@@ -30359,58 +30365,6 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
         </para>
        </entry>
       </row>
-
-      <row>
-       <entry role="func_table_entry"><para role="func_signature">
-        <indexterm>
-         <primary>pg_restore_attribute_stats</primary>
-        </indexterm>
-        <function>pg_restore_attribute_stats</function> (
-        <literal>VARIADIC</literal> <parameter>kwargs</parameter> <type>"any"</type> )
-        <returnvalue>boolean</returnvalue>
-        </para>
-        <para>
-         Similar to <function>pg_set_attribute_stats()</function>, but
-         intended for bulk restore of attribute statistics. The tracked
-         statistics may change from version to version, so the primary purpose
-         of this function is to maintain a consistent function signature to
-         avoid errors when restoring statistics from previous versions.
-        </para>
-        <para>
-         Arguments are passed as pairs of <replaceable>argname</replaceable>
-         and <replaceable>argvalue</replaceable>, where
-         <replaceable>argname</replaceable> corresponds to a named argument in
-         <function>pg_set_attribute_stats()</function> and
-         <replaceable>argvalue</replaceable> is of the corresponding type.
-        </para>
-        <para>
-         Additionally, this function supports argument name
-         <literal>version</literal> of type <type>integer</type>, which
-         specifies the version from which the statistics originated, improving
-         interpretation of older statistics.
-        </para>
-        <para>
-         For example, to set the <structname>avg_width</structname> and
-         <structname>null_frac</structname> for the attribute
-         <structname>col1</structname> of the table
-         <structname>mytable</structname>:
-<programlisting>
- SELECT pg_restore_attribute_stats(
-    'relation',    'mytable'::regclass,
-    'attname',     'col1'::name,
-    'inherited',   false,
-    'avg_width',   125::integer,
-    'null_frac',   0.5::real);
-</programlisting>
-        </para>
-        <para>
-         Minor errors are reported as a <literal>WARNING</literal> and
-         ignored, and remaining statistics will still be restored. If all
-         specified statistics are successfully restored, return
-         <literal>true</literal>, otherwise <literal>false</literal>.
-        </para>
-       </entry>
-      </row>
      </tbody>
     </tgroup>
    </table>

base-commit: ecbff4378beecb0b1d12fc758538005a69821db1
-- 
2.48.1

vAdios-Set-0002-Change-variable-lists-to-itemizedlists.patchtext/x-patch; charset=US-ASCII; name=vAdios-Set-0002-Change-variable-lists-to-itemizedlists.patchDownload
From 925176e7524291dea63d69e0893d236491894663 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Tue, 25 Feb 2025 18:32:13 -0500
Subject: [PATCH vAdios-Set 2/3] Change variable lists to itemizedlists

---
 doc/src/sgml/func.sgml | 47 ++++++++++++++++++++++++++++++++++--------
 1 file changed, 38 insertions(+), 9 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 12206e0cfc6..4b3a84c6766 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30226,10 +30226,23 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          columns in <link
          linkend="catalog-pg-class"><structname>pg_class</structname></link>.
          The currently-supported relation statistics are
-         <literal>relpages</literal> with a value of type
-         <type>integer</type>, <literal>reltuples</literal> with a value of
-         type <type>real</type>, and <literal>relallvisible</literal> with a
-         value of type <type>integer</type>.
+         <itemizedlist>
+           <listitem>
+             <para>
+               <literal>relpages</literal> with a value of type <type>integer</type>
+             </para>
+           </listitem>
+           <listitem>
+             <para>
+               <literal>reltuples</literal> with a value of type <type>real</type>
+             </para>
+           </listitem>
+           <listitem>
+             <para>
+               <literal>relallvisible</literal> with a value of type <type>integer</type>
+             </para>
+           </listitem>
+         </itemizedlist>
         </para>
         <para>
          Additionally, this function supports argument name
@@ -30314,11 +30327,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
 </programlisting>
         </para>
         <para>
-         The required arguments are <literal>relation</literal> with a value
-         of type <type>regclass</type>, which specifies the table;
-         <literal>attname</literal> with a value of type <type>name</type>,
-         which specifies the column; and <literal>inherited</literal>, which
-         specifies whether the statistics includes values from child tables.
+         The required arguments are
+         <itemizedlist>
+           <listitem>
+             <para>
+               <literal>relation</literal> with a value of type
+               <type>regclass</type>, which specifies the table
+             </para>
+           </listitem>
+           <listitem>
+             <para>
+               <literal>attname</literal> with a value of type
+               <type>name</type>, which specifies the column
+             </para>
+           </listitem>
+           <listitem>
+             <para>
+               <literal>inherited</literal>, which specifies whether the
+               statistics includes values from child tables.
+             </para>
+           </listitem>
+         </itemizedlist>
          Other arguments are the names of statistics corresponding to columns
          in <link
          linkend="view-pg-stats"><structname>pg_stats</structname></link>.
-- 
2.48.1

vAdios-Set-0003-Adapt-some-pg_set_-_stats-calls-to-pg_res.patchtext/x-patch; charset=US-ASCII; name=vAdios-Set-0003-Adapt-some-pg_set_-_stats-calls-to-pg_res.patchDownload
From b23e5bae0c0e650ec76cffa7343cb17b0295e4d5 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Tue, 25 Feb 2025 18:32:55 -0500
Subject: [PATCH vAdios-Set 3/3] Adapt some pg_set_*_stats() calls to
 pg_restore_*_stats() to preserve coverage

---
 src/test/regress/expected/stats_import.out | 119 +++++++++++++++++++--
 src/test/regress/sql/stats_import.sql      |  79 ++++++++++++--
 2 files changed, 182 insertions(+), 16 deletions(-)

diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index 7c7784efaf1..e907d76b1c1 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -125,6 +125,13 @@ SELECT
  t
 (1 row)
 
+-- error: regclass not found
+SELECT pg_catalog.pg_restore_relation_stats(
+    'relation', 0::Oid::regclass,
+    'relpages', '17'::integer,
+    'reltuples', '400.0'::real,
+    'relallvisible', '4'::integer);
+ERROR:  could not open relation with OID 0
 --
 -- Partitioned indexes aren't analyzed but it is possible to set
 -- stats. The locking rules are different from normal indexes due to
@@ -177,7 +184,12 @@ SELECT
  t
 (1 row)
 
--- error: attribute is system column
+-- error: relation null
+SELECT
+    pg_catalog.pg_restore_relation_stats(
+        'relation', NULL::regclass,
+        'relpages', -1::integer);
+ERROR:  "relation" cannot be NULL
 SELECT pg_catalog.pg_clear_attribute_stats(
     relation => 'stats_import.test'::regclass,
     attname => 'ctid'::name,
@@ -191,7 +203,7 @@ SELECT pg_catalog.pg_clear_attribute_stats(
 ERROR:  column "nope" of relation "test" does not exist
 -- reject: argument name is NULL
 SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
+        'relation', 'stats_import.test'::regclass,
         'version', 150000::integer,
         NULL, '17'::integer,
         'reltuples', 400::real,
@@ -199,7 +211,7 @@ SELECT pg_restore_relation_stats(
 ERROR:  name at variadic position 5 is NULL
 -- reject: argument name is an integer
 SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
+        'relation', 'stats_import.test'::regclass,
         'version', 150000::integer,
         17, '17'::integer,
         'reltuples', 400::real,
@@ -207,7 +219,7 @@ SELECT pg_restore_relation_stats(
 ERROR:  name at variadic position 5 has type "integer", expected type "text"
 -- reject: odd number of variadic arguments cannot be pairs
 SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
+        'relation', 'stats_import.test'::regclass,
         'version', 150000::integer,
         'relpages', '17'::integer,
         'reltuples', 400::real,
@@ -298,32 +310,46 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- warn and error: unrecognized argument name
 SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
+        'relation', 'stats_import.test'::regclass,
         'version', 150000::integer,
         'relpages', '17'::integer,
         'reltuples', 400::real,
         'nope', 4::integer);
 WARNING:  unrecognized argument name: "nope"
-ERROR:  could not open relation with OID 0
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+-- but relpages, reltuples got set anyway
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       17 |       400 |             5
+(1 row)
+
 -- warn: bad relpages type
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
         'version', 150000::integer,
         'relpages', 'nope'::text,
-        'reltuples', 400.0::real,
-        'relallvisible', 4::integer);
+        'reltuples', 401.0::real,
+        'relallvisible', 7::integer);
 WARNING:  argument "relpages" has type "text", expected type "integer"
  pg_restore_relation_stats 
 ---------------------------
  f
 (1 row)
 
+-- but reltuples, relallvisible got set anyway
 SELECT relpages, reltuples, relallvisible
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
  relpages | reltuples | relallvisible 
 ----------+-----------+---------------
-       16 |       400 |             4
+       17 |       401 |             7
 (1 row)
 
 -- error: object does not exist
@@ -366,6 +392,12 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'avg_width', 2::integer,
     'n_distinct', 0.3::real);
 ERROR:  column "nope" of relation "test" does not exist
+-- error: attribute is system column
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'ctid'::name,
+    'inherited', 'false'::boolean);
+ERROR:  cannot modify statistics on system column "ctid"
 -- error: inherited null
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
@@ -483,6 +515,75 @@ AND attname = 'id';
  stats_import | test      | id      | f         |       0.7 |         8 |       -0.8 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
+-- scalars can't have mcelem
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'most_common_elems', '{1,3}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+WARNING:  unable to determine element type of attribute "id"
+DETAIL:  Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- mcelem / mcelem freqs mismatch (each way)
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'tags'::name,
+    'inherited', false::boolean,
+    'most_common_elems', '{one,two}'::text
+    );
+WARNING:  "most_common_elem_freqs" must be specified when "most_common_elems" is specified
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'tags'::name,
+    'inherited', false::boolean,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
+    );
+WARNING:  "most_common_elems" must be specified when "most_common_elem_freqs" is specified
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+WARNING:  unable to determine element type of attribute "id"
+DETAIL:  Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- range types can't have most_common_elems
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'arange'::name,
+    'inherited', false::boolean,
+    'most_common_elems', '{3,1}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+WARNING:  unable to determine element type of attribute "arange"
+DETAIL:  Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
 -- warn: mcv / mcf type mismatch
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index f26f7857748..6098fb49b85 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -90,6 +90,13 @@ SELECT
         'relation', 'stats_import.part_parent'::regclass,
         'relpages', 2::integer);
 
+-- error: regclass not found
+SELECT pg_catalog.pg_restore_relation_stats(
+    'relation', 0::Oid::regclass,
+    'relpages', '17'::integer,
+    'reltuples', '400.0'::real,
+    'relallvisible', '4'::integer);
+
 --
 -- Partitioned indexes aren't analyzed but it is possible to set
 -- stats. The locking rules are different from normal indexes due to
@@ -124,7 +131,12 @@ SELECT
         'relation', 'stats_import.part_parent'::regclass,
         'relpages', -1::integer);
 
--- error: attribute is system column
+-- error: relation null
+SELECT
+    pg_catalog.pg_restore_relation_stats(
+        'relation', NULL::regclass,
+        'relpages', -1::integer);
+
 SELECT pg_catalog.pg_clear_attribute_stats(
     relation => 'stats_import.test'::regclass,
     attname => 'ctid'::name,
@@ -138,7 +150,7 @@ SELECT pg_catalog.pg_clear_attribute_stats(
 
 -- reject: argument name is NULL
 SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
+        'relation', 'stats_import.test'::regclass,
         'version', 150000::integer,
         NULL, '17'::integer,
         'reltuples', 400::real,
@@ -146,7 +158,7 @@ SELECT pg_restore_relation_stats(
 
 -- reject: argument name is an integer
 SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
+        'relation', 'stats_import.test'::regclass,
         'version', 150000::integer,
         17, '17'::integer,
         'reltuples', 400::real,
@@ -154,7 +166,7 @@ SELECT pg_restore_relation_stats(
 
 -- reject: odd number of variadic arguments cannot be pairs
 SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
+        'relation', 'stats_import.test'::regclass,
         'version', 150000::integer,
         'relpages', '17'::integer,
         'reltuples', 400::real,
@@ -212,20 +224,26 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- warn and error: unrecognized argument name
 SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
+        'relation', 'stats_import.test'::regclass,
         'version', 150000::integer,
         'relpages', '17'::integer,
         'reltuples', 400::real,
         'nope', 4::integer);
 
+-- but relpages, reltuples got set anyway
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
 -- warn: bad relpages type
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
         'version', 150000::integer,
         'relpages', 'nope'::text,
-        'reltuples', 400.0::real,
-        'relallvisible', 4::integer);
+        'reltuples', 401.0::real,
+        'relallvisible', 7::integer);
 
+-- but reltuples, relallvisible got set anyway
 SELECT relpages, reltuples, relallvisible
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
@@ -270,6 +288,12 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'avg_width', 2::integer,
     'n_distinct', 0.3::real);
 
+-- error: attribute is system column
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'ctid'::name,
+    'inherited', 'false'::boolean);
+
 -- error: inherited null
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
@@ -352,6 +376,47 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
+-- scalars can't have mcelem
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'most_common_elems', '{1,3}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+-- mcelem / mcelem freqs mismatch (each way)
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'tags'::name,
+    'inherited', false::boolean,
+    'most_common_elems', '{one,two}'::text
+    );
+
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'tags'::name,
+    'inherited', false::boolean,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
+    );
+
+-- scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+
+-- range types can't have most_common_elems
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'arange'::name,
+    'inherited', false::boolean,
+    'most_common_elems', '{3,1}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
 -- warn: mcv / mcf type mismatch
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
-- 
2.48.1

#368Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#366)
Re: Statistics Import and Export

On Tue, 2025-02-25 at 15:31 -0500, Corey Huinker wrote:

Documentation:

+         The currently-supported relation statistics are
+         <literal>relpages</literal> with a value of type
+         <type>integer</type>, <literal>reltuples</literal> with a
value of
+         type <type>real</type>, and
<literal>relallvisible</literal> with a
+         value of type <type>integer</type>.

Could we make this a bullet-list? Same for the required attribute
stats and optional attribute stats. I think it would be more eye-
catching and useful to people skimming to recall the name of a
parameter, which is probably what most people will do after they've
read it once to get the core concepts.

I couldn't make that look quite right. These functions are mostly for
use by pg_dump, and while documentation is necessary, I don't think we
should go so far as to make it "eye-catching". At least not until
things settle a bit.

Question:

Do we want to re-compact the oids we consumed in pg_proc.dat?

Done.

Specifically missing are:

* regclass not found
* attribute is system column
* scalars can't have mcelem
* mcelem / mcelem freqs mismatch (parts 1 and 2)
* scalars can't have elem_count_histogram
* cannot set most_common_elems for range type

Done.

And committed.

Regards,
Jeff Davis

#369Jeff Davis
pgsql@j-davis.com
In reply to: Andres Freund (#330)
2 attachment(s)
Re: Statistics Import and Export

On Mon, 2025-02-24 at 09:54 -0500, Andres Freund wrote:

Have you compared performance of with/without stats after these
optimizations?

On unoptimized build with asserts enabled, dumping the regression
database:

--no-statistics: 1.0s
master: 3.6s
v3j-0001: 3.0s
v3j-0002: 1.7s

I plan to commit the patches soon.

Regards,
Jeff Davis

Attachments:

v3j-0001-Avoid-unnecessary-relation-stats-query-in-pg_dum.patchtext/x-patch; charset=UTF-8; name=v3j-0001-Avoid-unnecessary-relation-stats-query-in-pg_dum.patchDownload
From d617fb142158e0ca964e5bc8bb3351d993de6062 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Fri, 21 Feb 2025 23:31:04 -0500
Subject: [PATCH v3j 1/2] Avoid unnecessary relation stats query in pg_dump.

The few fields we need can be easily collected in getTables() and
getIndexes() and stored in RelStatsInfo.

Co-authored-by: Corey Huinker <corey.huinker@gmail.com>
Co-authored-by: Jeff Davis <pgsql@j-davis.com>
Discussion: https://postgr.es/m/CADkLM=f0a43aTd88xW4xCFayEF25g-7hTrHX_WhV40HyocsUGg@mail.gmail.com
---
 src/bin/pg_dump/pg_dump.c | 145 ++++++++++++++++----------------------
 src/bin/pg_dump/pg_dump.h |   5 +-
 2 files changed, 64 insertions(+), 86 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index afd79287177..a1823914656 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -56,6 +56,7 @@
 #include "common/connect.h"
 #include "common/int.h"
 #include "common/relpath.h"
+#include "common/shortest_dec.h"
 #include "compress_io.h"
 #include "dumputils.h"
 #include "fe_utils/option_utils.h"
@@ -524,6 +525,9 @@ main(int argc, char **argv)
 	pg_logging_set_level(PG_LOG_WARNING);
 	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_dump"));
 
+	/* ensure that locale does not affect floating point interpretation */
+	setlocale(LC_NUMERIC, "C");
+
 	/*
 	 * Initialize what we need for parallel execution, especially for thread
 	 * support on Windows.
@@ -6814,7 +6818,8 @@ getFuncs(Archive *fout)
  *
  */
 static RelStatsInfo *
-getRelationStatistics(Archive *fout, DumpableObject *rel, char relkind)
+getRelationStatistics(Archive *fout, DumpableObject *rel, int32 relpages,
+					  float reltuples, int32 relallvisible, char relkind)
 {
 	if (!fout->dopt->dumpStatistics)
 		return NULL;
@@ -6839,6 +6844,9 @@ getRelationStatistics(Archive *fout, DumpableObject *rel, char relkind)
 		dobj->components |= DUMP_COMPONENT_STATISTICS;
 		dobj->name = pg_strdup(rel->name);
 		dobj->namespace = rel->namespace;
+		info->relpages = relpages;
+		info->reltuples = reltuples;
+		info->relallvisible = relallvisible;
 		info->relkind = relkind;
 		info->postponed_def = false;
 
@@ -6874,6 +6882,8 @@ getTables(Archive *fout, int *numTables)
 	int			i_relhasindex;
 	int			i_relhasrules;
 	int			i_relpages;
+	int			i_reltuples;
+	int			i_relallvisible;
 	int			i_toastpages;
 	int			i_owning_tab;
 	int			i_owning_col;
@@ -6924,7 +6934,7 @@ getTables(Archive *fout, int *numTables)
 						 "c.relowner, "
 						 "c.relchecks, "
 						 "c.relhasindex, c.relhasrules, c.relpages, "
-						 "c.relhastriggers, "
+						 "c.reltuples, c.relallvisible, c.relhastriggers, "
 						 "c.relpersistence, "
 						 "c.reloftype, "
 						 "c.relacl, "
@@ -7088,6 +7098,8 @@ getTables(Archive *fout, int *numTables)
 	i_relhasindex = PQfnumber(res, "relhasindex");
 	i_relhasrules = PQfnumber(res, "relhasrules");
 	i_relpages = PQfnumber(res, "relpages");
+	i_reltuples = PQfnumber(res, "reltuples");
+	i_relallvisible = PQfnumber(res, "relallvisible");
 	i_toastpages = PQfnumber(res, "toastpages");
 	i_owning_tab = PQfnumber(res, "owning_tab");
 	i_owning_col = PQfnumber(res, "owning_col");
@@ -7134,6 +7146,9 @@ getTables(Archive *fout, int *numTables)
 
 	for (i = 0; i < ntups; i++)
 	{
+		float		reltuples = strtof(PQgetvalue(res, i, i_reltuples), NULL);
+		int32		relallvisible = atoi(PQgetvalue(res, i, i_relallvisible));
+
 		tblinfo[i].dobj.objType = DO_TABLE;
 		tblinfo[i].dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_reltableoid));
 		tblinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_reloid));
@@ -7233,7 +7248,8 @@ getTables(Archive *fout, int *numTables)
 
 		/* Add statistics */
 		if (tblinfo[i].interesting)
-			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relkind);
+			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relpages,
+								  reltuples, relallvisible, tblinfo[i].relkind);
 
 		/*
 		 * Read-lock target tables to make sure they aren't DROPPED or altered
@@ -7499,6 +7515,9 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				i_oid,
 				i_indrelid,
 				i_indexname,
+				i_relpages,
+				i_reltuples,
+				i_relallvisible,
 				i_parentidx,
 				i_indexdef,
 				i_indnkeyatts,
@@ -7552,6 +7571,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 	appendPQExpBufferStr(query,
 						 "SELECT t.tableoid, t.oid, i.indrelid, "
 						 "t.relname AS indexname, "
+						 "t.relpages, t.reltuples, t.relallvisible, "
 						 "pg_catalog.pg_get_indexdef(i.indexrelid) AS indexdef, "
 						 "i.indkey, i.indisclustered, "
 						 "c.contype, c.conname, "
@@ -7659,6 +7679,9 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_oid = PQfnumber(res, "oid");
 	i_indrelid = PQfnumber(res, "indrelid");
 	i_indexname = PQfnumber(res, "indexname");
+	i_relpages = PQfnumber(res, "relpages");
+	i_reltuples = PQfnumber(res, "reltuples");
+	i_relallvisible = PQfnumber(res, "relallvisible");
 	i_parentidx = PQfnumber(res, "parentidx");
 	i_indexdef = PQfnumber(res, "indexdef");
 	i_indnkeyatts = PQfnumber(res, "indnkeyatts");
@@ -7725,6 +7748,9 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			char		contype;
 			char		indexkind;
 			RelStatsInfo *relstats;
+			int32		relpages = atoi(PQgetvalue(res, j, i_relpages));
+			float		reltuples = strtof(PQgetvalue(res, j, i_reltuples), NULL);
+			int32		relallvisible = atoi(PQgetvalue(res, j, i_relallvisible));
 
 			indxinfo[j].dobj.objType = DO_INDEX;
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
@@ -7759,7 +7785,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				indexkind = RELKIND_PARTITIONED_INDEX;
 
 			contype = *(PQgetvalue(res, j, i_contype));
-			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, indexkind);
+			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, relpages,
+											 reltuples, relallvisible, indexkind);
 
 			if (contype == 'p' || contype == 'u' || contype == 'x')
 			{
@@ -10383,18 +10410,6 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
-/*
- * Tabular description of the parameters to pg_restore_relation_stats()
- * param_name, param_type
- */
-static const char *rel_stats_arginfo[][2] = {
-	{"relation", "regclass"},
-	{"version", "integer"},
-	{"relpages", "integer"},
-	{"reltuples", "real"},
-	{"relallvisible", "integer"},
-};
-
 /*
  * Tabular description of the parameters to pg_restore_attribute_stats()
  * param_name, param_type
@@ -10419,30 +10434,6 @@ static const char *att_stats_arginfo[][2] = {
 	{"range_bounds_histogram", "text"},
 };
 
-/*
- * getRelStatsExportQuery --
- *
- * Generate a query that will fetch all relation (e.g. pg_class)
- * stats for a given relation.
- */
-static void
-getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
-					   const char *schemaname, const char *relname)
-{
-	resetPQExpBuffer(query);
-	appendPQExpBufferStr(query,
-						 "SELECT c.oid::regclass AS relation, "
-						 "current_setting('server_version_num') AS version, "
-						 "c.relpages, c.reltuples, c.relallvisible "
-						 "FROM pg_class c "
-						 "JOIN pg_namespace n "
-						 "ON n.oid = c.relnamespace "
-						 "WHERE n.nspname = ");
-	appendStringLiteralAH(query, schemaname, fout);
-	appendPQExpBufferStr(query, " AND c.relname = ");
-	appendStringLiteralAH(query, relname, fout);
-}
-
 /*
  * getAttStatsExportQuery --
  *
@@ -10454,21 +10445,22 @@ getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
 					   const char *schemaname, const char *relname)
 {
 	resetPQExpBuffer(query);
-	appendPQExpBufferStr(query,
-						 "SELECT c.oid::regclass AS relation, "
-						 "s.attname,"
-						 "s.inherited,"
-						 "current_setting('server_version_num') AS version, "
-						 "s.null_frac,"
-						 "s.avg_width,"
-						 "s.n_distinct,"
-						 "s.most_common_vals,"
-						 "s.most_common_freqs,"
-						 "s.histogram_bounds,"
-						 "s.correlation,"
-						 "s.most_common_elems,"
-						 "s.most_common_elem_freqs,"
-						 "s.elem_count_histogram,");
+	appendPQExpBuffer(query,
+					  "SELECT c.oid::regclass AS relation, "
+					  "s.attname,"
+					  "s.inherited,"
+					  "'%u'::integer AS version, "
+					  "s.null_frac,"
+					  "s.avg_width,"
+					  "s.n_distinct,"
+					  "s.most_common_vals,"
+					  "s.most_common_freqs,"
+					  "s.histogram_bounds,"
+					  "s.correlation,"
+					  "s.most_common_elems,"
+					  "s.most_common_elem_freqs,"
+					  "s.elem_count_histogram,",
+					  fout->remoteVersion);
 
 	if (fout->remoteVersion >= 170000)
 		appendPQExpBufferStr(query,
@@ -10521,34 +10513,21 @@ appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
  * Append a formatted pg_restore_relation_stats statement.
  */
 static void
-appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+appendRelStatsImport(PQExpBuffer out, Archive *fout, const RelStatsInfo *rsinfo)
 {
-	const char *sep = "";
+	const char *qualname = fmtQualifiedId(rsinfo->dobj.namespace->dobj.name, rsinfo->dobj.name);
+	char		reltuples_str[FLOAT_SHORTEST_DECIMAL_LEN];
 
-	if (PQntuples(res) == 0)
-		return;
+	float_to_shortest_decimal_buf(rsinfo->reltuples, reltuples_str);
 
 	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
-
-	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
-	{
-		const char *argname = rel_stats_arginfo[argno][0];
-		const char *argtype = rel_stats_arginfo[argno][1];
-		int			fieldno = PQfnumber(res, argname);
-
-		if (fieldno < 0)
-			pg_fatal("relation stats export query missing field '%s'",
-					 argname);
-
-		if (PQgetisnull(res, 0, fieldno))
-			continue;
-
-		appendPQExpBufferStr(out, sep);
-		appendNamedArgument(out, fout, argname, PQgetvalue(res, 0, fieldno), argtype);
-
-		sep = ",\n";
-	}
-	appendPQExpBufferStr(out, "\n);\n");
+	appendPQExpBuffer(out, "\t'relation', '%s'::regclass,\n", qualname);
+	appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
+					  fout->remoteVersion);
+	appendPQExpBuffer(out, "\t'relpages', '%d'::integer,\n", rsinfo->relpages);
+	appendPQExpBuffer(out, "\t'reltuples', '%s'::real,\n", reltuples_str);
+	appendPQExpBuffer(out, "\t'relallvisible', '%d'::integer\n);\n",
+					  rsinfo->relallvisible);
 }
 
 /*
@@ -10643,15 +10622,11 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 	tag = createPQExpBuffer();
 	appendPQExpBufferStr(tag, fmtId(dobj->name));
 
-	query = createPQExpBuffer();
 	out = createPQExpBuffer();
 
-	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
-						   dobj->name);
-	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
-	appendRelStatsImport(out, fout, res);
-	PQclear(res);
+	appendRelStatsImport(out, fout, rsinfo);
 
+	query = createPQExpBuffer();
 	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
 						   dobj->name);
 	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index f08f5905aa3..9d6a4857c4b 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -328,7 +328,7 @@ typedef struct _tableInfo
 	Oid			owning_tab;		/* OID of table owning sequence */
 	int			owning_col;		/* attr # of column owning sequence */
 	bool		is_identity_sequence;
-	int			relpages;		/* table's size in pages (from pg_class) */
+	int32		relpages;		/* table's size in pages (from pg_class) */
 	int			toastpages;		/* toast table's size in pages, if any */
 
 	bool		interesting;	/* true if need to collect more data */
@@ -438,6 +438,9 @@ typedef struct _indexAttachInfo
 typedef struct _relStatsInfo
 {
 	DumpableObject dobj;
+	int32		relpages;
+	float		reltuples;
+	int32		relallvisible;
 	char		relkind;		/* 'r', 'm', 'i', etc */
 	bool		postponed_def;	/* stats must be postponed into post-data */
 } RelStatsInfo;
-- 
2.34.1

v3j-0002-pg_dump-prepare-attribute-stats-query.patchtext/x-patch; charset=UTF-8; name=v3j-0002-pg_dump-prepare-attribute-stats-query.patchDownload
From 33cde5fdbb207a99bce678015e1e45df0210bb56 Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Tue, 25 Feb 2025 17:15:47 -0800
Subject: [PATCH v3j 2/2] pg_dump: prepare attribute stats query.

Follow precedent in pg_dump for preparing queries to improve
performance. Also, simplify the query by removing unnecessary joins.

Reported-by: Andres Freund <andres@anarazel.de>
Co-authored-by: Corey Huinker <corey.huinker@gmail.com>
Co-authored-by: Jeff Davis <pgsql@j-davis.com>
Discussion: https://postgr.es/m/CADkLM=dRMC6t8gp9GVf6y6E_r5EChQjMAAh_vPyih_zMiq0zvA@mail.gmail.com
---
 src/bin/pg_dump/pg_backup.h |   1 +
 src/bin/pg_dump/pg_dump.c   | 123 +++++++++++++++++-------------------
 2 files changed, 58 insertions(+), 66 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 350cf659c41..e783cc68d89 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -72,6 +72,7 @@ enum _dumpPreparedQueries
 	PREPQUERY_DUMPOPR,
 	PREPQUERY_DUMPRANGETYPE,
 	PREPQUERY_DUMPTABLEATTACH,
+	PREPQUERY_GETATTRIBUTESTATS,
 	PREPQUERY_GETCOLUMNACLS,
 	PREPQUERY_GETDOMAINCONSTRAINTS,
 };
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a1823914656..0de6c959bb0 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -10415,10 +10415,8 @@ dumpComment(Archive *fout, const char *type,
  * param_name, param_type
  */
 static const char *att_stats_arginfo[][2] = {
-	{"relation", "regclass"},
 	{"attname", "name"},
 	{"inherited", "boolean"},
-	{"version", "integer"},
 	{"null_frac", "float4"},
 	{"avg_width", "integer"},
 	{"n_distinct", "float4"},
@@ -10434,60 +10432,6 @@ static const char *att_stats_arginfo[][2] = {
 	{"range_bounds_histogram", "text"},
 };
 
-/*
- * getAttStatsExportQuery --
- *
- * Generate a query that will fetch all attribute (e.g. pg_statistic)
- * stats for a given relation.
- */
-static void
-getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
-					   const char *schemaname, const char *relname)
-{
-	resetPQExpBuffer(query);
-	appendPQExpBuffer(query,
-					  "SELECT c.oid::regclass AS relation, "
-					  "s.attname,"
-					  "s.inherited,"
-					  "'%u'::integer AS version, "
-					  "s.null_frac,"
-					  "s.avg_width,"
-					  "s.n_distinct,"
-					  "s.most_common_vals,"
-					  "s.most_common_freqs,"
-					  "s.histogram_bounds,"
-					  "s.correlation,"
-					  "s.most_common_elems,"
-					  "s.most_common_elem_freqs,"
-					  "s.elem_count_histogram,",
-					  fout->remoteVersion);
-
-	if (fout->remoteVersion >= 170000)
-		appendPQExpBufferStr(query,
-							 "s.range_length_histogram,"
-							 "s.range_empty_frac,"
-							 "s.range_bounds_histogram ");
-	else
-		appendPQExpBufferStr(query,
-							 "NULL AS range_length_histogram,"
-							 "NULL AS range_empty_frac,"
-							 "NULL AS range_bounds_histogram ");
-
-	appendPQExpBufferStr(query,
-						 "FROM pg_stats s "
-						 "JOIN pg_namespace n "
-						 "ON n.nspname = s.schemaname "
-						 "JOIN pg_class c "
-						 "ON c.relname = s.tablename "
-						 "AND c.relnamespace = n.oid "
-						 "WHERE s.schemaname = ");
-	appendStringLiteralAH(query, schemaname, fout);
-	appendPQExpBufferStr(query, " AND s.tablename = ");
-	appendStringLiteralAH(query, relname, fout);
-	appendPQExpBufferStr(query, " ORDER BY s.attname, s.inherited");
-}
-
-
 /*
  * appendNamedArgument --
  *
@@ -10513,17 +10457,17 @@ appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
  * Append a formatted pg_restore_relation_stats statement.
  */
 static void
-appendRelStatsImport(PQExpBuffer out, Archive *fout, const RelStatsInfo *rsinfo)
+appendRelStatsImport(PQExpBuffer out, Archive *fout, const RelStatsInfo *rsinfo,
+					 const char *qualified_name)
 {
-	const char *qualname = fmtQualifiedId(rsinfo->dobj.namespace->dobj.name, rsinfo->dobj.name);
 	char		reltuples_str[FLOAT_SHORTEST_DECIMAL_LEN];
 
 	float_to_shortest_decimal_buf(rsinfo->reltuples, reltuples_str);
 
 	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
-	appendPQExpBuffer(out, "\t'relation', '%s'::regclass,\n", qualname);
 	appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
 					  fout->remoteVersion);
+	appendPQExpBuffer(out, "\t'relation', '%s'::regclass,\n", qualified_name);
 	appendPQExpBuffer(out, "\t'relpages', '%d'::integer,\n", rsinfo->relpages);
 	appendPQExpBuffer(out, "\t'reltuples', '%s'::real,\n", reltuples_str);
 	appendPQExpBuffer(out, "\t'relallvisible', '%d'::integer\n);\n",
@@ -10536,13 +10480,18 @@ appendRelStatsImport(PQExpBuffer out, Archive *fout, const RelStatsInfo *rsinfo)
  * Append a series of formatted pg_restore_attribute_stats statements.
  */
 static void
-appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res,
+					 const char *qualified_name)
 {
 	for (int rownum = 0; rownum < PQntuples(res); rownum++)
 	{
 		const char *sep = "";
 
 		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+		appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
+						  fout->remoteVersion);
+		appendPQExpBuffer(out, "\t'relation', '%s'::regclass,\n",
+						  qualified_name);
 		for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
 		{
 			const char *argname = att_stats_arginfo[argno][0];
@@ -10607,6 +10556,7 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 	DumpableObject *dobj = (DumpableObject *) &rsinfo->dobj;
 	DumpId	   *deps = NULL;
 	int			ndeps = 0;
+	const char *qualified_name;
 
 	/* nothing to do if we are not dumping statistics */
 	if (!fout->dopt->dumpStatistics)
@@ -10622,15 +10572,56 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 	tag = createPQExpBuffer();
 	appendPQExpBufferStr(tag, fmtId(dobj->name));
 
-	out = createPQExpBuffer();
+	query = createPQExpBuffer();
+	if (!fout->is_prepared[PREPQUERY_GETATTRIBUTESTATS])
+	{
+		appendPQExpBufferStr(query,
+							 "PREPARE getAttributeStats(pg_catalog.name, pg_catalog.name) AS\n"
+							 "SELECT s.attname, s.inherited, "
+							 "s.null_frac, s.avg_width, s.n_distinct, "
+							 "s.most_common_vals, s.most_common_freqs, "
+							 "s.histogram_bounds, s.correlation, "
+							 "s.most_common_elems, s.most_common_elem_freqs, "
+							 "s.elem_count_histogram, ");
+
+		if (fout->remoteVersion >= 170000)
+			appendPQExpBufferStr(query,
+								 "s.range_length_histogram, s.range_empty_frac, "
+								 "s.range_bounds_histogram ");
+		else
+			appendPQExpBufferStr(query,
+								 "NULL AS range_length_histogram,"
+								 "NULL AS range_empty_frac,"
+								 "NULL AS range_bounds_histogram ");
 
-	appendRelStatsImport(out, fout, rsinfo);
+		appendPQExpBufferStr(query,
+							 "FROM pg_stats s "
+							 "WHERE s.schemaname = $1 "
+							 "AND s.tablename = $2 "
+							 "ORDER BY s.attname, s.inherited");
+
+		ExecuteSqlStatement(fout, query->data);
+
+		fout->is_prepared[PREPQUERY_GETATTRIBUTESTATS] = true;
+		resetPQExpBuffer(query);
+	}
+
+	appendPQExpBufferStr(query, "EXECUTE getAttributeStats(");
+	appendStringLiteralAH(query, dobj->namespace->dobj.name, fout);
+	appendPQExpBufferStr(query, ", ");
+	appendStringLiteralAH(query, dobj->name, fout);
+	appendPQExpBufferStr(query, "); ");
 
-	query = createPQExpBuffer();
-	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
-						   dobj->name);
 	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
-	appendAttStatsImport(out, fout, res);
+
+	out = createPQExpBuffer();
+
+	qualified_name = fmtQualifiedId(rsinfo->dobj.namespace->dobj.name,
+									rsinfo->dobj.name);
+
+	appendRelStatsImport(out, fout, rsinfo, qualified_name);
+	appendAttStatsImport(out, fout, res, qualified_name);
+
 	PQclear(res);
 
 	ArchiveEntry(fout, nilCatalogId, createDumpId(),
-- 
2.34.1

#370Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#369)
Re: Statistics Import and Export

On Tue, Feb 25, 2025 at 9:00 PM Jeff Davis <pgsql@j-davis.com> wrote:

On Mon, 2025-02-24 at 09:54 -0500, Andres Freund wrote:

Have you compared performance of with/without stats after these
optimizations?

On unoptimized build with asserts enabled, dumping the regression
database:

--no-statistics: 1.0s
master: 3.6s
v3j-0001: 3.0s
v3j-0002: 1.7s

I plan to commit the patches soon.

Regards,
Jeff Davis

+1 from me

We can still convert the "EXECUTE getAttributeStats" call to a Params call,
but that involves creating an ExecuteSqlQueryParams(), which starts to
snowball in the changes required.

#371Tom Lane
tgl@sss.pgh.pa.us
In reply to: Corey Huinker (#370)
Re: Statistics Import and Export

Corey Huinker <corey.huinker@gmail.com> writes:

We can still convert the "EXECUTE getAttributeStats" call to a Params call,
but that involves creating an ExecuteSqlQueryParams(), which starts to
snowball in the changes required.

Yeah, let's leave that for some other day. It's not really apparent
that it'd buy us much performance-wise, though maybe the code would
net out cleaner.

To my mind the next task is to get the buildfarm green again by
fixing the expression-index-stats problem. I can have a go at
that once Jeff pushes these patches, unless one of you are already
on it?

regards, tom lane

#372Jeff Davis
pgsql@j-davis.com
In reply to: Tom Lane (#371)
Re: Statistics Import and Export

On Tue, 2025-02-25 at 22:40 -0500, Tom Lane wrote:

To my mind the next task is to get the buildfarm green again by
fixing the expression-index-stats problem.  I can have a go at
that once Jeff pushes these patches, unless one of you are already
on it?

I just committed them.

Regards,
Jeff Davis

#373Corey Huinker
corey.huinker@gmail.com
In reply to: Tom Lane (#371)
Re: Statistics Import and Export

To my mind the next task is to get the buildfarm green again by
fixing the expression-index-stats problem. I can have a go at
that once Jeff pushes these patches, unless one of you are already
on it?

Already on it, but I can step aside if you've got a clearer vision of how
to solve it.

My solution so far is to take allo the v11+ (SELECT array_agg...) functions
and put them into a LATERAL, two of them filtered by attstattarget > 0 and
a new one aggregating attnames with no filter.

An alternative would be a new subselect for array_agg(attname) WHERE
in.indexprs IS NOT NULL, thus removing the extra compute for the indexes
that lack an index expression (i.e. most of them), and thus lack settable
stats (at least for now) and wouldn't be affected by the name-jitter issue
anyway.

I'm on the fence about how to handle pg_clear_attribute_stats(), leaning
toward overloaded functions.

#374Tom Lane
tgl@sss.pgh.pa.us
In reply to: Corey Huinker (#373)
Re: Statistics Import and Export

Corey Huinker <corey.huinker@gmail.com> writes:

My solution so far is to take allo the v11+ (SELECT array_agg...) functions
and put them into a LATERAL, two of them filtered by attstattarget > 0 and
a new one aggregating attnames with no filter.

An alternative would be a new subselect for array_agg(attname) WHERE
in.indexprs IS NOT NULL, thus removing the extra compute for the indexes
that lack an index expression (i.e. most of them), and thus lack settable
stats (at least for now) and wouldn't be affected by the name-jitter issue
anyway.

Yeah, I've been thinking about that. I think that the idea of the
current design is that relatively few indexes will have explicit stats
targets set on them, so most of the time the sub-SELECTs produce no
data. (Which is not to say that they're cheap to execute.) If we
pull all the column names for all indexes then we'll likely bloat
pg_dump's working storage quite a bit. Pulling them only for indexes
with expression columns should fix that, and as you say we don't need
the names otherwise.

I still fear that those sub-selects are pretty expensive in aggregate
-- they are basically forcing a nestloop join -- and maybe we need to
rethink that whole idea.

BTW, just as a point of order: it is not the case that non-expression
indexes are free of name-jitter problems. That's because we don't
bother to rename index columns when the underlying table column is
renamed, thus:

regression=# create table t1 (id int primary key);
CREATE TABLE
regression=# \d t1_pkey
Index "public.t1_pkey"
Column | Type | Key? | Definition
--------+---------+------+------------
id | integer | yes | id
primary key, btree, for table "public.t1"

regression=# alter table t1 rename column id to xx;
ALTER TABLE
regression=# \d t1_pkey
Index "public.t1_pkey"
Column | Type | Key? | Definition
--------+---------+------+------------
id | integer | yes | xx
primary key, btree, for table "public.t1"

After dump-n-reload, this index's column will be named "xx".
That's not relevant to our current problem as long as we
don't store stats on such index columns, but it's plenty
relevant to the ALTER INDEX ... SET STATISTICS code.

I'm on the fence about how to handle pg_clear_attribute_stats(), leaning
toward overloaded functions.

I kinda felt that we didn't need to bother with an attnum-based
variant of pg_clear_attribute_stats(), since pg_dump has no
use for that. I won't stand in the way if you're desperate to
do it, though.

regards, tom lane

#375Corey Huinker
corey.huinker@gmail.com
In reply to: Tom Lane (#374)
Re: Statistics Import and Export

On Tue, Feb 25, 2025 at 11:36 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:

Corey Huinker <corey.huinker@gmail.com> writes:

My solution so far is to take allo the v11+ (SELECT array_agg...)

functions

and put them into a LATERAL, two of them filtered by attstattarget > 0

and

a new one aggregating attnames with no filter.

An alternative would be a new subselect for array_agg(attname) WHERE
in.indexprs IS NOT NULL, thus removing the extra compute for the indexes
that lack an index expression (i.e. most of them), and thus lack settable
stats (at least for now) and wouldn't be affected by the name-jitter

issue

anyway.

Yeah, I've been thinking about that. I think that the idea of the
current design is that relatively few indexes will have explicit stats
targets set on them, so most of the time the sub-SELECTs produce no
data. (Which is not to say that they're cheap to execute.) If we
pull all the column names for all indexes then we'll likely bloat
pg_dump's working storage quite a bit. Pulling them only for indexes
with expression columns should fix that, and as you say we don't need
the names otherwise.

I still fear that those sub-selects are pretty expensive in aggregate
-- they are basically forcing a nestloop join -- and maybe we need to
rethink that whole idea.

BTW, just as a point of order: it is not the case that non-expression
indexes are free of name-jitter problems. That's because we don't
bother to rename index columns when the underlying table column is
renamed, thus:

Ouch.

After dump-n-reload, this index's column will be named "xx".
That's not relevant to our current problem as long as we
don't store stats on such index columns, but it's plenty
relevant to the ALTER INDEX ... SET STATISTICS code.

The only way I can imagine those columns getting their own stats is if we
start adding stats for columns of partial indexes, in which case we'd just
bump the predicate to WHERE (i.indexprs IS NOT NULL OR i.indpred IS NOT
NULL)

Just to confirm, we ARE able to assume dense packing of attributes in an
index, and thus we can infer the attnum from the position of the attname in
the aggregated array, and there's no need to do a parallel array_agg of
attnums, yes?

I'm on the fence about how to handle pg_clear_attribute_stats(), leaning
toward overloaded functions.

I kinda felt that we didn't need to bother with an attnum-based
variant of pg_clear_attribute_stats(), since pg_dump has no
use for that. I won't stand in the way if you're desperate to
do it, though.

I'm not desperate to slow this thread down, no. We'll stick with
attname-only.

#376Tom Lane
tgl@sss.pgh.pa.us
In reply to: Corey Huinker (#375)
Re: Statistics Import and Export

Corey Huinker <corey.huinker@gmail.com> writes:

Just to confirm, we ARE able to assume dense packing of attributes in an
index, and thus we can infer the attnum from the position of the attname in
the aggregated array, and there's no need to do a parallel array_agg of
attnums, yes?

Yes, absolutely, there are no dropped columns in indexes. See
upthread discussion.

We could have avoided two sub-selects for attstattarget too,
on the same principle: just collect them all and sort it out
later. That'd risk bloating pg_dump's storage, although maybe
we could have handled that by doing additional processing
while inspecting the results of getIndexes' query, so as not
to store anything in the common case.

regards, tom lane

#377Corey Huinker
corey.huinker@gmail.com
In reply to: Tom Lane (#376)
2 attachment(s)
Re: Statistics Import and Export

On Wed, Feb 26, 2025 at 12:05 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:

Corey Huinker <corey.huinker@gmail.com> writes:

Just to confirm, we ARE able to assume dense packing of attributes in an
index, and thus we can infer the attnum from the position of the attname

in

the aggregated array, and there's no need to do a parallel array_agg of
attnums, yes?

Yes, absolutely, there are no dropped columns in indexes. See
upthread discussion.

We could have avoided two sub-selects for attstattarget too,
on the same principle: just collect them all and sort it out
later. That'd risk bloating pg_dump's storage, although maybe
we could have handled that by doing additional processing
while inspecting the results of getIndexes' query, so as not
to store anything in the common case.

regards, tom lane

0001 - Add attnum support to attribute_statistics_update

* Basically what Tom posted earlier, minus the pg_set_attribute_stats
stuff, obviously.

0002 - Add attnum support to pg_dump.

* Removed att_stats_arginfo
* Folds appendRelStatsImport and appendAttStatsImport
into dumpRelationStats

Attachments:

vViaDellaAttnums-0001-Add-ability-to-reference-columns-by.patchtext/x-patch; charset=US-ASCII; name=vViaDellaAttnums-0001-Add-ability-to-reference-columns-by.patchDownload
From 9dd7129318804ede9e412d886f3124e6d452a2d9 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 26 Feb 2025 00:39:17 -0500
Subject: [PATCH vViaDellaAttnums 1/2] Add ability to reference columns by
 attnum to pg_restore_attribute_stats().

We need this because pg_dump needs to address index expression column
statistics by attnum because the attribute name is not stable across
upgrades.
---
 src/backend/statistics/attribute_stats.c   | 84 +++++++++++++++++-----
 src/test/regress/expected/stats_import.out |  2 +-
 2 files changed, 67 insertions(+), 19 deletions(-)

diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index 66a5676c810..71a7a175d1f 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -38,6 +38,7 @@ enum attribute_stats_argnum
 {
 	ATTRELATION_ARG = 0,
 	ATTNAME_ARG,
+	ATTNUM_ARG,
 	INHERITED_ARG,
 	NULL_FRAC_ARG,
 	AVG_WIDTH_ARG,
@@ -59,6 +60,7 @@ static struct StatsArgInfo attarginfo[] =
 {
 	[ATTRELATION_ARG] = {"relation", REGCLASSOID},
 	[ATTNAME_ARG] = {"attname", NAMEOID},
+	[ATTNUM_ARG] = {"attnum", INT2OID},
 	[INHERITED_ARG] = {"inherited", BOOLOID},
 	[NULL_FRAC_ARG] = {"null_frac", FLOAT4OID},
 	[AVG_WIDTH_ARG] = {"avg_width", INT4OID},
@@ -76,6 +78,22 @@ static struct StatsArgInfo attarginfo[] =
 	[NUM_ATTRIBUTE_STATS_ARGS] = {0}
 };
 
+enum clear_attribute_stats_argnum
+{
+	C_ATTRELATION_ARG = 0,
+	C_ATTNAME_ARG,
+	C_INHERITED_ARG,
+	C_NUM_ATTRIBUTE_STATS_ARGS
+};
+
+static struct StatsArgInfo cleararginfo[] =
+{
+	[C_ATTRELATION_ARG] = {"relation", REGCLASSOID},
+	[C_ATTNAME_ARG] = {"attname", NAMEOID},
+	[C_INHERITED_ARG] = {"inherited", BOOLOID},
+	[C_NUM_ATTRIBUTE_STATS_ARGS] = {0}
+};
+
 static bool attribute_statistics_update(FunctionCallInfo fcinfo);
 static Node *get_attr_expr(Relation rel, int attnum);
 static void get_attr_stat_type(Oid reloid, AttrNumber attnum,
@@ -116,9 +134,9 @@ static bool
 attribute_statistics_update(FunctionCallInfo fcinfo)
 {
 	Oid			reloid;
-	Name		attname;
-	bool		inherited;
+	char	   *attname;
 	AttrNumber	attnum;
+	bool		inherited;
 
 	Relation	starel;
 	HeapTuple	statup;
@@ -164,21 +182,51 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	/* lock before looking up attribute */
 	stats_lock_check_privileges(reloid);
 
-	stats_check_required_arg(fcinfo, attarginfo, ATTNAME_ARG);
-	attname = PG_GETARG_NAME(ATTNAME_ARG);
-	attnum = get_attnum(reloid, NameStr(*attname));
+	/* user can specify either attname or attnum, but not both */
+	if (!PG_ARGISNULL(ATTNAME_ARG))
+	{
+		Name		attnamename;
+
+		if (!PG_ARGISNULL(ATTNUM_ARG))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("must specify one of attname and attnum")));
+		attnamename = PG_GETARG_NAME(ATTNAME_ARG);
+		attname = NameStr(*attnamename);
+		attnum = get_attnum(reloid, attname);
+		/* note that this test covers attisdropped cases too: */
+		if (attnum == InvalidAttrNumber)
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_COLUMN),
+					 errmsg("column \"%s\" of relation \"%s\" does not exist",
+							attname, get_rel_name(reloid))));
+	}
+	else if (!PG_ARGISNULL(ATTNUM_ARG))
+	{
+		attnum = PG_GETARG_INT16(ATTNUM_ARG);
+		attname = get_attname(reloid, attnum, true);
+		/* Annoyingly, get_attname doesn't check attisdropped */
+		if (attname == NULL ||
+			!SearchSysCacheExistsAttName(reloid, attname))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_COLUMN),
+					 errmsg("column %d of relation \"%s\" does not exist",
+							attnum, get_rel_name(reloid))));
+	}
+	else
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("must specify one of attname and attnum")));
+		attname = NULL;			/* keep compiler quiet */
+		attnum = 0;
+	}
 
 	if (attnum < 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot modify statistics on system column \"%s\"",
-						NameStr(*attname))));
-
-	if (attnum == InvalidAttrNumber)
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_COLUMN),
-				 errmsg("column \"%s\" of relation \"%s\" does not exist",
-						NameStr(*attname), get_rel_name(reloid))));
+						attname)));
 
 	stats_check_required_arg(fcinfo, attarginfo, INHERITED_ARG);
 	inherited = PG_GETARG_BOOL(INHERITED_ARG);
@@ -241,7 +289,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 								&elemtypid, &elem_eq_opr))
 		{
 			ereport(WARNING,
-					(errmsg("unable to determine element type of attribute \"%s\"", NameStr(*attname)),
+					(errmsg("unable to determine element type of attribute \"%s\"", attname),
 					 errdetail("Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.")));
 			elemtypid = InvalidOid;
 			elem_eq_opr = InvalidOid;
@@ -257,7 +305,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	{
 		ereport(WARNING,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				 errmsg("could not determine less-than operator for attribute \"%s\"", NameStr(*attname)),
+				 errmsg("could not determine less-than operator for attribute \"%s\"", attname),
 				 errdetail("Cannot set STATISTIC_KIND_HISTOGRAM or STATISTIC_KIND_CORRELATION.")));
 
 		do_histogram = false;
@@ -271,7 +319,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	{
 		ereport(WARNING,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				 errmsg("attribute \"%s\" is not a range type", NameStr(*attname)),
+				 errmsg("attribute \"%s\" is not a range type", attname),
 				 errdetail("Cannot set STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM or STATISTIC_KIND_BOUNDS_HISTOGRAM.")));
 
 		do_bounds_histogram = false;
@@ -857,7 +905,7 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 	AttrNumber	attnum;
 	bool		inherited;
 
-	stats_check_required_arg(fcinfo, attarginfo, ATTRELATION_ARG);
+	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELATION_ARG);
 	reloid = PG_GETARG_OID(ATTRELATION_ARG);
 
 	if (RecoveryInProgress())
@@ -868,7 +916,7 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 
 	stats_lock_check_privileges(reloid);
 
-	stats_check_required_arg(fcinfo, attarginfo, ATTNAME_ARG);
+	stats_check_required_arg(fcinfo, cleararginfo, C_ATTNAME_ARG);
 	attname = PG_GETARG_NAME(ATTNAME_ARG);
 	attnum = get_attnum(reloid, NameStr(*attname));
 
@@ -884,7 +932,7 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 				 errmsg("column \"%s\" of relation \"%s\" does not exist",
 						NameStr(*attname), get_rel_name(reloid))));
 
-	stats_check_required_arg(fcinfo, attarginfo, INHERITED_ARG);
+	stats_check_required_arg(fcinfo, attarginfo, C_INHERITED_ARG);
 	inherited = PG_GETARG_BOOL(INHERITED_ARG);
 
 	delete_pg_statistic(reloid, attnum, inherited);
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index 7e8b7f429c9..b8fc2fcce46 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -1250,7 +1250,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'null_frac', 0.1::real,
     'avg_width', 2::integer,
     'n_distinct', 0.3::real);
-ERROR:  "attname" cannot be NULL
+ERROR:  must specify one of attname and attnum
 -- error: attname doesn't exist
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,

base-commit: 0e42d31b0b2273c376ce9de946b59d155fac589c
-- 
2.48.1

vViaDellaAttnums-0002-Dump-expression-index-stats-by-attn.patchtext/x-patch; charset=US-ASCII; name=vViaDellaAttnums-0002-Dump-expression-index-stats-by-attn.patchDownload
From 92f75fc9835e7982216d32329caa34b26347a6b3 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 26 Feb 2025 04:08:31 -0500
Subject: [PATCH vViaDellaAttnums 2/2] Dump expression index stats by attnum,
 not attname.

Names of columns in index expressions are not stable across major
versions, so we are forced to dump those by attnum instead.
---
 src/bin/pg_dump/pg_dump.c | 219 +++++++++++++++++++++-----------------
 src/bin/pg_dump/pg_dump.h |   2 +
 2 files changed, 125 insertions(+), 96 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 0de6c959bb0..65413e07899 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -6819,7 +6819,8 @@ getFuncs(Archive *fout)
  */
 static RelStatsInfo *
 getRelationStatistics(Archive *fout, DumpableObject *rel, int32 relpages,
-					  float reltuples, int32 relallvisible, char relkind)
+					  float reltuples, int32 relallvisible, char relkind,
+					  char **indexprattnames, int nindexprattnames)
 {
 	if (!fout->dopt->dumpStatistics)
 		return NULL;
@@ -6844,6 +6845,8 @@ getRelationStatistics(Archive *fout, DumpableObject *rel, int32 relpages,
 		dobj->components |= DUMP_COMPONENT_STATISTICS;
 		dobj->name = pg_strdup(rel->name);
 		dobj->namespace = rel->namespace;
+		info->indexprattnames = indexprattnames;
+		info->nindexprattnames = nindexprattnames;
 		info->relpages = relpages;
 		info->reltuples = reltuples;
 		info->relallvisible = relallvisible;
@@ -7249,7 +7252,8 @@ getTables(Archive *fout, int *numTables)
 		/* Add statistics */
 		if (tblinfo[i].interesting)
 			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relpages,
-								  reltuples, relallvisible, tblinfo[i].relkind);
+								  reltuples, relallvisible, tblinfo[i].relkind,
+								  NULL, 0);
 
 		/*
 		 * Read-lock target tables to make sure they aren't DROPPED or altered
@@ -7537,7 +7541,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				i_tablespace,
 				i_indreloptions,
 				i_indstatcols,
-				i_indstatvals;
+				i_indstatvals,
+				i_indexprattnames;
 
 	/*
 	 * We want to perform just one query against pg_index.  However, we
@@ -7579,6 +7584,10 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 						 "c.tableoid AS contableoid, "
 						 "c.oid AS conoid, "
 						 "pg_catalog.pg_get_constraintdef(c.oid, false) AS condef, "
+						 "(SELECT pg_catalog.array_agg(attname ORDER BY attnum) "
+						 "  FROM pg_catalog.pg_attribute "
+						 "  WHERE attrelid = i.indexrelid AND "
+						 "    i.indexprs IS NOT NULL) AS indexprattnames, "
 						 "(SELECT spcname FROM pg_catalog.pg_tablespace s WHERE s.oid = t.reltablespace) AS tablespace, "
 						 "t.reloptions AS indreloptions, ");
 
@@ -7702,6 +7711,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_indreloptions = PQfnumber(res, "indreloptions");
 	i_indstatcols = PQfnumber(res, "indstatcols");
 	i_indstatvals = PQfnumber(res, "indstatvals");
+	i_indexprattnames = PQfnumber(res, "indexprattnames");
 
 	indxinfo = (IndxInfo *) pg_malloc(ntups * sizeof(IndxInfo));
 
@@ -7715,6 +7725,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 		Oid			indrelid = atooid(PQgetvalue(res, j, i_indrelid));
 		TableInfo  *tbinfo = NULL;
 		int			numinds;
+		char	  **indexprattnames = NULL;	/* attnames for expression indexes only */
+		int			nindexprattnames = 0;	/* number of attnames for expression indexes only */
 
 		/* Count rows for this table */
 		for (numinds = 1; numinds < ntups - j; numinds++)
@@ -7784,9 +7796,16 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			else
 				indexkind = RELKIND_PARTITIONED_INDEX;
 
-			contype = *(PQgetvalue(res, j, i_contype));
+			if (!PQgetisnull(res, j, i_indexprattnames))
+				if (!parsePGArray(PQgetvalue(res, j, i_indexprattnames),
+								   &indexprattnames, &nindexprattnames))
+				pg_fatal("could not parse %s array", "indattnames");
+
 			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, relpages,
-											 reltuples, relallvisible, indexkind);
+											 reltuples, relallvisible, indexkind,
+											 indexprattnames, nindexprattnames);
+
+			contype = *(PQgetvalue(res, j, i_contype));
 
 			if (contype == 'p' || contype == 'u' || contype == 'x')
 			{
@@ -10410,28 +10429,6 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
-/*
- * Tabular description of the parameters to pg_restore_attribute_stats()
- * param_name, param_type
- */
-static const char *att_stats_arginfo[][2] = {
-	{"attname", "name"},
-	{"inherited", "boolean"},
-	{"null_frac", "float4"},
-	{"avg_width", "integer"},
-	{"n_distinct", "float4"},
-	{"most_common_vals", "text"},
-	{"most_common_freqs", "float4[]"},
-	{"histogram_bounds", "text"},
-	{"correlation", "float4"},
-	{"most_common_elems", "text"},
-	{"most_common_elem_freqs", "float4[]"},
-	{"elem_count_histogram", "float4[]"},
-	{"range_length_histogram", "text"},
-	{"range_empty_frac", "float4"},
-	{"range_bounds_histogram", "text"},
-};
-
 /*
  * appendNamedArgument --
  *
@@ -10440,9 +10437,9 @@ static const char *att_stats_arginfo[][2] = {
  */
 static void
 appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
-					const char *argval, const char *argtype)
+					const char *argtype, const char *argval)
 {
-	appendPQExpBufferStr(out, "\t");
+	appendPQExpBufferStr(out, ",\n\t");
 
 	appendStringLiteralAH(out, argname, fout);
 	appendPQExpBufferStr(out, ", ");
@@ -10451,68 +10448,6 @@ appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
 	appendPQExpBuffer(out, "::%s", argtype);
 }
 
-/*
- * appendRelStatsImport --
- *
- * Append a formatted pg_restore_relation_stats statement.
- */
-static void
-appendRelStatsImport(PQExpBuffer out, Archive *fout, const RelStatsInfo *rsinfo,
-					 const char *qualified_name)
-{
-	char		reltuples_str[FLOAT_SHORTEST_DECIMAL_LEN];
-
-	float_to_shortest_decimal_buf(rsinfo->reltuples, reltuples_str);
-
-	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
-	appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
-					  fout->remoteVersion);
-	appendPQExpBuffer(out, "\t'relation', '%s'::regclass,\n", qualified_name);
-	appendPQExpBuffer(out, "\t'relpages', '%d'::integer,\n", rsinfo->relpages);
-	appendPQExpBuffer(out, "\t'reltuples', '%s'::real,\n", reltuples_str);
-	appendPQExpBuffer(out, "\t'relallvisible', '%d'::integer\n);\n",
-					  rsinfo->relallvisible);
-}
-
-/*
- * appendAttStatsImport --
- *
- * Append a series of formatted pg_restore_attribute_stats statements.
- */
-static void
-appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res,
-					 const char *qualified_name)
-{
-	for (int rownum = 0; rownum < PQntuples(res); rownum++)
-	{
-		const char *sep = "";
-
-		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
-		appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
-						  fout->remoteVersion);
-		appendPQExpBuffer(out, "\t'relation', '%s'::regclass,\n",
-						  qualified_name);
-		for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
-		{
-			const char *argname = att_stats_arginfo[argno][0];
-			const char *argtype = att_stats_arginfo[argno][1];
-			int			fieldno = PQfnumber(res, argname);
-
-			if (fieldno < 0)
-				pg_fatal("attribute stats export query missing field '%s'",
-						 argname);
-
-			if (PQgetisnull(res, rownum, fieldno))
-				continue;
-
-			appendPQExpBufferStr(out, sep);
-			appendNamedArgument(out, fout, argname, PQgetvalue(res, rownum, fieldno), argtype);
-			sep = ",\n";
-		}
-		appendPQExpBufferStr(out, "\n);\n");
-	}
-}
-
 /*
  * Decide which section to use based on the relkind of the parent object.
  *
@@ -10557,6 +10492,7 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 	DumpId	   *deps = NULL;
 	int			ndeps = 0;
 	const char *qualified_name;
+	char		reltuples_str[FLOAT_SHORTEST_DECIMAL_LEN];
 
 	/* nothing to do if we are not dumping statistics */
 	if (!fout->dopt->dumpStatistics)
@@ -10606,6 +10542,22 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 		resetPQExpBuffer(query);
 	}
 
+	out = createPQExpBuffer();
+
+	qualified_name = fmtQualifiedId(rsinfo->dobj.namespace->dobj.name,
+									rsinfo->dobj.name);
+
+	/* restore relation stats */
+	float_to_shortest_decimal_buf(rsinfo->reltuples, reltuples_str);
+	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
+	appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
+					  fout->remoteVersion);
+	appendPQExpBuffer(out, "\t'relation', '%s'::regclass,\n", qualified_name);
+	appendPQExpBuffer(out, "\t'relpages', '%d'::integer,\n", rsinfo->relpages);
+	appendPQExpBuffer(out, "\t'reltuples', '%s'::real,\n", reltuples_str);
+	appendPQExpBuffer(out, "\t'relallvisible', '%d'::integer\n);\n",
+					  rsinfo->relallvisible);
+
 	appendPQExpBufferStr(query, "EXECUTE getAttributeStats(");
 	appendStringLiteralAH(query, dobj->namespace->dobj.name, fout);
 	appendPQExpBufferStr(query, ", ");
@@ -10614,13 +10566,88 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 
 	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
 
-	out = createPQExpBuffer();
+	/* restore attribute stats */
+	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	{
+		const char *attname;
 
-	qualified_name = fmtQualifiedId(rsinfo->dobj.namespace->dobj.name,
-									rsinfo->dobj.name);
+		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+		appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
+						  fout->remoteVersion);
+		appendPQExpBuffer(out, "\t'relation', '%s'::regclass",
+						  qualified_name);
 
-	appendRelStatsImport(out, fout, rsinfo, qualified_name);
-	appendAttStatsImport(out, fout, res, qualified_name);
+
+		if (PQgetisnull(res, rownum, 0))
+			pg_fatal("attname cannot be NULL");
+		attname = PQgetvalue(res, rownum, 0);
+
+		/*
+		 * Expression indexes look up attname in attnames to derive attnum,
+		 * all others use attname directly.
+		 */
+		if (rsinfo->nindexprattnames == 0)
+			appendNamedArgument(out, fout, "attname", "name", attname);
+		else
+		{
+			bool found = false;
+
+			for (int i = 0; i < rsinfo->nindexprattnames; i++)
+				if (strcmp(attname, rsinfo->indexprattnames[i]) == 0)
+				{
+					appendPQExpBuffer(out, ",\n\t'attnum', '%d'::smallint", (AttrNumber) i + 1);
+					found = true;
+					break;
+				}
+
+			if (!found)
+				pg_fatal("unable to find attname '%s'", attname);
+		}
+
+		if (!PQgetisnull(res, rownum, 1))
+			appendNamedArgument(out, fout, "inherited", "boolean",
+								PQgetvalue(res, rownum, 1));
+		if (!PQgetisnull(res, rownum, 2))
+			appendNamedArgument(out, fout, "null_frac", "float4",
+								PQgetvalue(res, rownum, 2));
+		if (!PQgetisnull(res, rownum, 3))
+			appendNamedArgument(out, fout, "avg_width", "integer",
+								PQgetvalue(res, rownum, 3));
+		if (!PQgetisnull(res, rownum, 4))
+			appendNamedArgument(out, fout, "n_distinct", "float4",
+								PQgetvalue(res, rownum, 4));
+		if (!PQgetisnull(res, rownum, 5))
+			appendNamedArgument(out, fout, "most_common_vals", "text",
+								PQgetvalue(res, rownum, 5));
+		if (!PQgetisnull(res, rownum, 6))
+			appendNamedArgument(out, fout, "most_common_freqs", "float4[]",
+								PQgetvalue(res, rownum, 6));
+		if (!PQgetisnull(res, rownum, 7))
+			appendNamedArgument(out, fout, "histogram_bounds", "text",
+								PQgetvalue(res, rownum, 7));
+		if (!PQgetisnull(res, rownum, 8))
+			appendNamedArgument(out, fout, "correlation", "float4",
+								PQgetvalue(res, rownum, 8));
+		if (!PQgetisnull(res, rownum, 9))
+			appendNamedArgument(out, fout, "most_common_elems", "text",
+								PQgetvalue(res, rownum, 9));
+		if (!PQgetisnull(res, rownum, 10))
+			appendNamedArgument(out, fout, "most_common_elem_freqs", "float4[]",
+								PQgetvalue(res, rownum, 10));
+		if (!PQgetisnull(res, rownum, 11))
+			appendNamedArgument(out, fout, "elem_count_histogram", "float4[]",
+								PQgetvalue(res, rownum, 11));
+		if (!PQgetisnull(res, rownum, 12))
+			appendNamedArgument(out, fout, "range_length_histogram", "text",
+								PQgetvalue(res, rownum, 12));
+		if (!PQgetisnull(res, rownum, 13))
+			appendNamedArgument(out, fout, "range_empty_frac", "float4",
+								PQgetvalue(res, rownum, 13));
+		if (!PQgetisnull(res, rownum, 14))
+			appendNamedArgument(out, fout, "range_bounds_histogram", "text",
+								PQgetvalue(res, rownum, 14));
+		appendPQExpBufferStr(out, "\n);\n");
+	}
 
 	PQclear(res);
 
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 9d6a4857c4b..0584b1c7abb 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -438,6 +438,8 @@ typedef struct _indexAttachInfo
 typedef struct _relStatsInfo
 {
 	DumpableObject dobj;
+	char	  **indexprattnames;	/* attnames in an expression index */
+	int32		nindexprattnames;	/* number of attnames for expression indexes, else 0 */
 	int32		relpages;
 	float		reltuples;
 	int32		relallvisible;
-- 
2.48.1

#378Tom Lane
tgl@sss.pgh.pa.us
In reply to: Corey Huinker (#377)
Re: Statistics Import and Export

Corey Huinker <corey.huinker@gmail.com> writes:

0001 - Add attnum support to attribute_statistics_update
* Basically what Tom posted earlier, minus the pg_set_attribute_stats
stuff, obviously.
0002 - Add attnum support to pg_dump.
* Removed att_stats_arginfo
* Folds appendRelStatsImport and appendAttStatsImport
into dumpRelationStats

Cool. Jeff, are you taking these, or shall I?

regards, tom lane

#379Jeff Davis
pgsql@j-davis.com
In reply to: Tom Lane (#378)
Re: Statistics Import and Export

On Wed, 2025-02-26 at 11:13 -0500, Tom Lane wrote:

Cool.  Jeff, are you taking these, or shall I?

Please go ahead.

I think you had mentioned upthread something about getting rid of the
table-driven logic, which is fine with me. Did you mean for that to
happen in this patch as well?

Regards,
Jeff Davis

#380Tom Lane
tgl@sss.pgh.pa.us
In reply to: Jeff Davis (#379)
Re: Statistics Import and Export

Jeff Davis <pgsql@j-davis.com> writes:

I think you had mentioned upthread something about getting rid of the
table-driven logic, which is fine with me. Did you mean for that to
happen in this patch as well?

Per Corey's description of the patch (I didn't read it yet), some
of that already happened. I want to get to buildfarm-green ASAP,
so I'm content to leave other cosmetic changes for later.

BTW, one cosmetic change that I'd like to see is that any tables that
don't go away get marked "const". I tried to make that happen with
attribute_stats.c's tables in my WIP patch upthread, but found that
the need for const-ness would propagate to some utility functions and
such, so I put the idea on the back burner.

regards, tom lane

#381Corey Huinker
corey.huinker@gmail.com
In reply to: Tom Lane (#380)
Re: Statistics Import and Export

On Wed, Feb 26, 2025 at 11:23 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:

Jeff Davis <pgsql@j-davis.com> writes:

I think you had mentioned upthread something about getting rid of the
table-driven logic, which is fine with me. Did you mean for that to
happen in this patch as well?

Per Corey's description of the patch (I didn't read it yet), some
of that already happened. I want to get to buildfarm-green ASAP,
so I'm content to leave other cosmetic changes for later.

The structs attarginfo and cleararginfo remain, which is notable but not
quite no-table.

BTW, one cosmetic change that I'd like to see is that any tables that
don't go away get marked "const". I tried to make that happen with
attribute_stats.c's tables in my WIP patch upthread, but found that
the need for const-ness would propagate to some utility functions and
such, so I put the idea on the back burner.

I didn't even think to try const-ing those structs.

#382Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#377)
Re: Statistics Import and Export

On Wed, 2025-02-26 at 04:25 -0500, Corey Huinker wrote:

0001 - Add attnum support to attribute_statistics_update

* Basically what Tom posted earlier, minus the pg_set_attribute_stats
stuff, obviously.

Should have a couple simple tests.

And I would use two different error message wordings:

"must specify either attname or attnum"
"cannot specify both attname and attnum"

(or similar)

The "one of attname and attnum" is a bit awkward.

The new struct for pg_clear_attribute_stats() isn't great, but as
discussed we can get rid of that in a subsequent commit.

Otherwise LGTM.

0002 - Add attnum support to pg_dump.

* Removed att_stats_arginfo
* Folds appendRelStatsImport and appendAttStatsImport
into dumpRelationStats 

Can we add a test here, too, to check that tables dump the attname and
indexes dump the attnum?

Everything else in the file uses i_fieldname = PQfnumber(), but in this
patch you're just using raw numbers.

Some of the fields from pg_stats are NOT NULL, so we could consider
issuing a warning in that case rather than just skipping it.

And it could use a pgindent.

I ran a quick measurement and it appears within the noise of the
numbers I posted here:

/messages/by-id/6af48508a32499a8be3398cafffd29fb6188c44b.camel@j-davis.com

Regards,
Jeff Davis

#383Tom Lane
tgl@sss.pgh.pa.us
In reply to: Jeff Davis (#382)
Re: Statistics Import and Export

Jeff Davis <pgsql@j-davis.com> writes:

I ran a quick measurement and it appears within the noise of the
numbers I posted here:
/messages/by-id/6af48508a32499a8be3398cafffd29fb6188c44b.camel@j-davis.com

Thanks for doing that. I agree with your other comments and
will incorporate them.

regards, tom lane

#384Jeff Davis
pgsql@j-davis.com
In reply to: Tom Lane (#383)
Re: Statistics Import and Export

On Wed, 2025-02-26 at 13:06 -0500, Tom Lane wrote:

Jeff Davis <pgsql@j-davis.com> writes:

I ran a quick measurement and it appears within the noise of the
numbers I posted here:
/messages/by-id/6af48508a32499a8be3398cafffd29fb6188c44b.camel@j-davis.com

Thanks for doing that.  I agree with your other comments and
will incorporate them.

Also, here are the numbers with an optimized build:

--no-statistics: 0.21 s
pre-optimization (6c349d83b6): 0.75
v3j-0001 (8f427187db): 0.65
v3j-0002 (6ee3b91bad): 0.27
new patch 0001+0002: 0.26

Regards,
Jeff Davis

#385Melanie Plageman
melanieplageman@gmail.com
In reply to: Corey Huinker (#367)
Re: Statistics Import and Export

On Tue, Feb 25, 2025 at 6:41 PM Corey Huinker <corey.huinker@gmail.com> wrote:

0003 - converting some of the deleted pg_set* tests into pg_restore* tests to keep the error coverage that they had.

I haven't really followed this thread and am not sure where the right
place is to drop this question, so I'll just do it here.

I have a patch that is getting thwacked around by the churn in
stats_import.sql, and it occurred to me that I don't see why all the
negative tests for pg_restore_relation_stats() need to have all the
parameters provided. For example, in both of these tests, you are
testing the relation parameter but including all these other fields.
It's fine if there is a reason to do that, but otherwise, it makes the
test file longer and makes the test case less clear IMO.

-- error: argument name is NULL
SELECT pg_restore_relation_stats(
'relation', '0'::oid::regclass,
'version', 150000::integer,
NULL, '17'::integer,
'reltuples', 400::real,
'relallvisible', 4::integer);

-- error: argument name is an integer
SELECT pg_restore_relation_stats(
'relation', '0'::oid::regclass,
'version', 150000::integer,
17, '17'::integer,
'reltuples', 400::real,
'relallvisible', 4::integer);

- Melanie

#386Robert Haas
robertmhaas@gmail.com
In reply to: Andres Freund (#343)
Re: Statistics Import and Export

On Mon, Feb 24, 2025 at 3:36 PM Andres Freund <andres@anarazel.de> wrote:

I suspect that this is a *really* bad idea. It's very very hard to get inplace
updates right. We have several unfixed correctness bugs that are related to
the use of inplace updates. I really don't think it's wise to add additional
interfaces that can reach inplace updates unless there's really no other
alternative (like not being able to assign an xid in VACUUM to be able to deal
with anti-xid-wraparound-shutdown systems).

I strongly agree. I think shipping this feature in any form that uses
in-place updates is a bad idea. I believe that the chances that we
will regret that decision are high. I take Corey's point that bloating
pg_class isn't great either ... but if that's a big problem, I believe
the solution is to find a way to get the right values into those rows
when they are first created, not to use in-place updates after the
fact.

Honestly, I'd go further than Andres did: even when there's really no
alternative, that doesn't mean in-place updates are a good idea. It
just means they're the best thing we've been able to come up with so
far. I think we're going to keep finding bugs until we remove every
last in-place update in the system.

--
Robert Haas
EDB: http://www.enterprisedb.com

#387Jeff Davis
pgsql@j-davis.com
In reply to: Robert Haas (#386)
Re: Statistics Import and Export

On Wed, 2025-02-26 at 15:15 -0500, Robert Haas wrote:

I strongly agree. I think shipping this feature in any form that uses
in-place updates is a bad idea.

Removed already in commit f3dae2ae58.

The reason they were added was mostly for consistency with ANALYZE, and
(at least for me) secondarily about churn on pg_class. The bloat was
never terrible.

With that in mind, should we remove the in-place updates from ANALYZE
as well?

Regards,
Jeff Davis

#388Robert Haas
robertmhaas@gmail.com
In reply to: Jeff Davis (#387)
Re: Statistics Import and Export

On Wed, Feb 26, 2025 at 3:37 PM Jeff Davis <pgsql@j-davis.com> wrote:

On Wed, 2025-02-26 at 15:15 -0500, Robert Haas wrote:

I strongly agree. I think shipping this feature in any form that uses
in-place updates is a bad idea.

Removed already in commit f3dae2ae58.

Cool.

The reason they were added was mostly for consistency with ANALYZE, and
(at least for me) secondarily about churn on pg_class. The bloat was
never terrible.

With that in mind, should we remove the in-place updates from ANALYZE
as well?

While I generally think fewer in-place updates are better than more,
I'm not sure what code we're talking about here and I definitely
haven't studied it, so I don't want to make excessively strong
statements. If you feel it can be done without breaking anything else,
or you have a way to repair the breakage, I'd definitely be interested
in hearing more about that.

--
Robert Haas
EDB: http://www.enterprisedb.com

#389Jeff Davis
pgsql@j-davis.com
In reply to: Robert Haas (#388)
Re: Statistics Import and Export

On Wed, 2025-02-26 at 15:57 -0500, Robert Haas wrote:

If you feel it can be done without breaking anything else,
or you have a way to repair the breakage, I'd definitely be
interested
in hearing more about that.

That would be a separate thread, but it's good to know that there is a
general consensus that we don't want to use in-place updates for non-
critical things like stats (and perhaps eliminate them entirely). In
other words, the inconcistency likely won't last forever.

Regards,
Jeff Davis

#390Tom Lane
tgl@sss.pgh.pa.us
In reply to: Jeff Davis (#389)
Re: Statistics Import and Export

Jeff Davis <pgsql@j-davis.com> writes:

That would be a separate thread, but it's good to know that there is a
general consensus that we don't want to use in-place updates for non-
critical things like stats (and perhaps eliminate them entirely). In
other words, the inconcistency likely won't last forever.

I'm quite sure that the original argument for using in-place updates
for this was not wanting a full-database VACUUM or ANALYZE to update
every tuple in pg_class. At the time that definitely did lead to
more-or-less 2x bloat. The new information we have now is that that's
no longer the case, and thus the decision can and should be revisited.

regards, tom lane

#391Tom Lane
tgl@sss.pgh.pa.us
In reply to: Melanie Plageman (#385)
Re: Statistics Import and Export

Melanie Plageman <melanieplageman@gmail.com> writes:

I have a patch that is getting thwacked around by the churn in
stats_import.sql, and it occurred to me that I don't see why all the
negative tests for pg_restore_relation_stats() need to have all the
parameters provided. For example, in both of these tests, you are
testing the relation parameter but including all these other fields.
It's fine if there is a reason to do that, but otherwise, it makes the
test file longer and makes the test case less clear IMO.

+1, let's shorten those queries. The coast is probably pretty
clear now if you want to go do that.

regards, tom lane

#392Corey Huinker
corey.huinker@gmail.com
In reply to: Melanie Plageman (#385)
Re: Statistics Import and Export

I have a patch that is getting thwacked around by the churn in
stats_import.sql, and it occurred to me that I don't see why all the
negative tests for pg_restore_relation_stats() need to have all the
parameters provided. For example, in both of these tests, you are
testing the relation parameter but including all these other fields.
It's fine if there is a reason to do that, but otherwise, it makes the
test file longer and makes the test case less clear IMO.

It's a known issue, and I intend to do a culling. Things have been
changing a lot with the pg_set* functions going away, and some of the tests
were covered by set* functions but not restore* functions. I'll give it a
pass once the buildfarm goes green again, and then I'm immediately shifting
gears to your patchset so that the additional tests you'll require are
smooth and minimal.

#393Corey Huinker
corey.huinker@gmail.com
In reply to: Tom Lane (#391)
Re: Statistics Import and Export

On Wed, Feb 26, 2025 at 4:46 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:

Melanie Plageman <melanieplageman@gmail.com> writes:

I have a patch that is getting thwacked around by the churn in
stats_import.sql, and it occurred to me that I don't see why all the
negative tests for pg_restore_relation_stats() need to have all the
parameters provided. For example, in both of these tests, you are
testing the relation parameter but including all these other fields.
It's fine if there is a reason to do that, but otherwise, it makes the
test file longer and makes the test case less clear IMO.

+1, let's shorten those queries. The coast is probably pretty
clear now if you want to go do that.

On it.

#394Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#393)
1 attachment(s)
Re: Statistics Import and Export

+1, let's shorten those queries. The coast is probably pretty

clear now if you want to go do that.

On it.

The earlier conversion of pg_set_attribute_stats (which once had many
not-null params) to pg_restore_* tests (where only the columns that
identify the stat row are actually required) meant that a lot of parameters
which were previously required were now inconsequential to the test.
However, it is important to demonstrate cases where the rest of the restore
operation completed after given bad statistic was encountered, but that can
be adequately done by one "bystander" parameter rather than the whole fleet.

Other notes:
* organized the tests into roughly three groups: relation tests, attribute
tests, and set-difference tests.
* tests that raise an error bubble up to the top of their respective groups
* tests that would have multiple warnings are reduced to having just one
wherever possible
* each test gets a comment about what is to be demonstrated
* attention paid to parameter values to avoid coincidental values that
could mislead someone into thinking the value was written somewhere when
that just happened to be what was already there, etc.
* the set difference tests remain, as they proved extremely useful in
detecting undesirable side-effects during development

Attachments:

vTestReorg-0001-Organize-and-deduplicate-statistics-impor.patchtext/x-patch; charset=US-ASCII; name=vTestReorg-0001-Organize-and-deduplicate-statistics-impor.patchDownload
From f3087b04784d6853970bf41eb619281d72ce94bd Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 26 Feb 2025 21:02:44 -0500
Subject: [PATCH vTestReorg] Organize and deduplicate statistics import tests.

Many changes, refactorings, and rebasings have taken their toll on the
statistics import tests. Now that things appear more stable and the
pg_set_* functions are gone in favor of using pg_restore_* in all cases,
it's safe to remove duplicates, combine tests where possible, and make
the test descriptions a bit more descriptive and uniform.
---
 src/test/regress/expected/stats_import.out | 672 +++++++++------------
 src/test/regress/sql/stats_import.sql      | 546 +++++++----------
 2 files changed, 500 insertions(+), 718 deletions(-)

diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index 1f150f7b08d..eaa3fe0f812 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -13,17 +13,58 @@ CREATE TABLE stats_import.test(
     tags text[]
 ) WITH (autovacuum_enabled = false);
 CREATE INDEX test_i ON stats_import.test(id);
+--
+-- relstats tests
+--
+--- error: relation is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 0::oid,
+        'relpages', 17::integer,
+        'reltuples', 400.0::real,
+        'relallvisible', 4::integer);
+WARNING:  argument "relation" has type "oid", expected type "regclass"
+ERROR:  "relation" cannot be NULL
+-- error: relation not found
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 0::oid::regclass,
+        'relpages', 17::integer,
+        'reltuples', 400.0::real,
+        'relallvisible', 4::integer);
+ERROR:  could not open relation with OID 0
+-- error: odd number of variadic arguments cannot be pairs
+SELECT pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'version', 150000::integer,
+        'relpages', '17'::integer,
+        'reltuples', 400::real,
+        'relallvisible');
+ERROR:  variadic arguments must be name/value pairs
+HINT:  Provide an even number of variadic arguments that can be divided into pairs.
+-- error: argument name is NULL
+SELECT pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'version', 150000::integer,
+        NULL, '17'::integer,
+        'relallvisible', 14::integer);
+ERROR:  name at variadic position 5 is NULL
+-- error: argument name is not a text type
+SELECT pg_restore_relation_stats(
+        'relation', '0'::oid::regclass,
+        'version', 150000::integer,
+        17, '17'::integer,
+        'relallvisible', 44::integer);
+ERROR:  name at variadic position 5 has type "integer", expected type "text"
 -- starting stats
 SELECT relpages, reltuples, relallvisible
 FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
+WHERE oid = 'stats_import.test_i'::regclass;
  relpages | reltuples | relallvisible 
 ----------+-----------+---------------
-        0 |        -1 |             0
+        1 |         0 |             0
 (1 row)
 
-BEGIN;
 -- regular indexes have special case locking rules
+BEGIN;
 SELECT
     pg_catalog.pg_restore_relation_stats(
         'relation', 'stats_import.test_i'::regclass,
@@ -50,32 +91,6 @@ WHERE relation = 'stats_import.test_i'::regclass AND
 (1 row)
 
 COMMIT;
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.test_i'::regclass,
-        'relpages', 19::integer );
- pg_restore_relation_stats 
----------------------------
- t
-(1 row)
-
--- clear
-SELECT
-    pg_catalog.pg_clear_relation_stats(
-        'stats_import.test'::regclass);
- pg_clear_relation_stats 
--------------------------
- 
-(1 row)
-
-SELECT relpages, reltuples, relallvisible
-FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
- relpages | reltuples | relallvisible 
-----------+-----------+---------------
-        0 |        -1 |             0
-(1 row)
-
 --  relpages may be -1 for partitioned tables
 CREATE TABLE stats_import.part_parent ( i integer ) PARTITION BY RANGE(i);
 CREATE TABLE stats_import.part_child_1
@@ -92,26 +107,6 @@ WHERE oid = 'stats_import.part_parent'::regclass;
        -1
 (1 row)
 
--- although partitioned tables have no storage, setting relpages to a
--- positive value is still allowed
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent_i'::regclass,
-        'relpages', 2::integer);
- pg_restore_relation_stats 
----------------------------
- t
-(1 row)
-
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent'::regclass,
-        'relpages', 2::integer);
- pg_restore_relation_stats 
----------------------------
- t
-(1 row)
-
 --
 -- Partitioned indexes aren't analyzed but it is possible to set
 -- stats. The locking rules are different from normal indexes due to
@@ -145,30 +140,19 @@ WHERE relation = 'stats_import.part_parent_i'::regclass AND
 (1 row)
 
 COMMIT;
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent_i'::regclass,
-        'relpages', 2::integer);
- pg_restore_relation_stats 
----------------------------
- t
+SELECT relpages
+FROM pg_class
+WHERE oid = 'stats_import.part_parent_i'::regclass;
+ relpages 
+----------
+        2
 (1 row)
 
--- nothing stops us from setting it to -1
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent'::regclass,
-        'relpages', -1::integer);
- pg_restore_relation_stats 
----------------------------
- t
-(1 row)
-
--- ok: set all stats
+-- ok: set all stats, no bounds checking
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
         'version', 150000::integer,
-        'relpages', '17'::integer,
+        'relpages', '-17'::integer,
         'reltuples', 400::real,
         'relallvisible', 4::integer);
  pg_restore_relation_stats 
@@ -181,10 +165,10 @@ FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
  relpages | reltuples | relallvisible 
 ----------+-----------+---------------
-       17 |       400 |             4
+      -17 |       400 |             4
 (1 row)
 
--- ok: just relpages
+-- ok: set just relpages, rest stay same
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
         'version', 150000::integer,
@@ -202,7 +186,7 @@ WHERE oid = 'stats_import.test'::regclass;
        16 |       400 |             4
 (1 row)
 
--- ok: just reltuples
+-- ok: set just reltuples, rest stay same
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
         'version', 150000::integer,
@@ -220,7 +204,7 @@ WHERE oid = 'stats_import.test'::regclass;
        16 |       500 |             4
 (1 row)
 
--- ok: just relallvisible
+-- ok: set just relallvisible, rest stay same
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
         'version', 150000::integer,
@@ -238,7 +222,7 @@ WHERE oid = 'stats_import.test'::regclass;
        16 |       500 |             5
 (1 row)
 
--- warn: bad relpages type
+-- warn: bad relpages type, rest updated
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
         'version', 150000::integer,
@@ -259,20 +243,136 @@ WHERE oid = 'stats_import.test'::regclass;
        16 |       400 |             4
 (1 row)
 
+-- unrecognized argument name, rest ok
+SELECT pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'version', 150000::integer,
+        'relpages', '171'::integer,
+        'nope', 10::integer);
+WARNING:  unrecognized argument name: "nope"
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+      171 |       400 |             4
+(1 row)
+
+-- ok: clear stats
+SELECT pg_catalog.pg_clear_relation_stats(
+    relation => 'stats_import.test'::regclass);
+ pg_clear_relation_stats 
+-------------------------
+ 
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+        0 |        -1 |             0
+(1 row)
+
 -- invalid relkinds for statistics
 CREATE SEQUENCE stats_import.testseq;
-CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
-SELECT
-    pg_catalog.pg_clear_relation_stats(
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.testseq'::regclass);
+ERROR:  cannot modify statistics for relation "testseq"
+DETAIL:  This operation is not supported for sequences.
+SELECT pg_catalog.pg_clear_relation_stats(
         'stats_import.testseq'::regclass);
 ERROR:  cannot modify statistics for relation "testseq"
 DETAIL:  This operation is not supported for sequences.
-SELECT
-    pg_catalog.pg_clear_relation_stats(
+CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.testview'::regclass);
+ERROR:  cannot modify statistics for relation "testview"
+DETAIL:  This operation is not supported for views.
+SELECT pg_catalog.pg_clear_relation_stats(
         'stats_import.testview'::regclass);
 ERROR:  cannot modify statistics for relation "testview"
 DETAIL:  This operation is not supported for views.
--- ok: no stakinds
+--
+-- attribute stats
+--
+-- error: object does not exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', '0'::oid::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real);
+ERROR:  could not open relation with OID 0
+-- error: relation null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', NULL::oid::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real);
+ERROR:  "relation" cannot be NULL
+-- error: NULL attname
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', NULL::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real);
+ERROR:  must specify either attname or attnum
+-- error: attname doesn't exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'nope'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ERROR:  column "nope" of relation "test" does not exist
+-- error: both attname and attnum
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'attnum', 1::smallint,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real);
+ERROR:  cannot specify both attname and attnum
+-- error: neither attname nor attnum
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real);
+ERROR:  must specify either attname or attnum
+-- error: attribute is system column
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'xmin'::name,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+ERROR:  cannot modify statistics on system column "xmin"
+-- error: inherited null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', NULL::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real);
+ERROR:  "inherited" cannot be NULL
+-- error: attribute is system column
+SELECT pg_catalog.pg_clear_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'ctid'::name,
+    inherited => false::boolean);
+ERROR:  cannot clear statistics on system column "ctid"
+-- ok: just the fixed values, no stakinds
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
@@ -297,15 +397,17 @@ AND attname = 'id';
  stats_import | test      | id      | f         |       0.2 |         5 |        0.6 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- ok: restore by attnum
+--
+-- ok: restore by attnum, we normally reserve this for
+-- indexes, but there is no reason it shouldn't work
+-- for any stat-having relation.
+--
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attnum', 1::smallint,
     'inherited', false::boolean,
     'version', 150000::integer,
-    'null_frac', 0.4::real,
-    'avg_width', 5::integer,
-    'n_distinct', 0.6::real);
+    'null_frac', 0.4::real);
  pg_restore_attribute_stats 
 ----------------------------
  t
@@ -322,14 +424,13 @@ AND attname = 'id';
  stats_import | test      | id      | f         |       0.4 |         5 |        0.6 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: unrecognized argument name
+-- warn: unrecognized argument name, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
     'null_frac', 0.2::real,
-    'avg_width', NULL::integer,
     'nope', 0.5::real);
 WARNING:  unrecognized argument name: "nope"
  pg_restore_attribute_stats 
@@ -348,15 +449,13 @@ AND attname = 'id';
  stats_import | test      | id      | f         |       0.2 |         5 |        0.6 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: mcv / mcf null mismatch part 1
+-- warn: mcv / mcf null mismatch part 1, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
-    'null_frac', 0.6::real,
-    'avg_width', 7::integer,
-    'n_distinct', -0.7::real,
+    'null_frac', 0.21::real,
     'most_common_freqs', '{0.1,0.2,0.3}'::real[]
     );
 WARNING:  "most_common_vals" must be specified when "most_common_freqs" is specified
@@ -373,18 +472,16 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.6 |         7 |       -0.7 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.21 |         5 |        0.6 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: mcv / mcf null mismatch part 2
+-- warn: mcv / mcf null mismatch part 2, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
-    'null_frac', 0.7::real,
-    'avg_width', 8::integer,
-    'n_distinct', -0.8::real,
+    'null_frac', 0.21::real,
     'most_common_vals', '{1,2,3}'::text
     );
 WARNING:  "most_common_freqs" must be specified when "most_common_vals" is specified
@@ -401,18 +498,16 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.7 |         8 |       -0.8 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.21 |         5 |        0.6 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: mcv / mcf type mismatch
+-- warn: mcf type mismatch, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
-    'null_frac', 0.8::real,
-    'avg_width', 9::integer,
-    'n_distinct', -0.9::real,
+    'null_frac', 0.22::real,
     'most_common_vals', '{2,1,3}'::text,
     'most_common_freqs', '{0.2,0.1}'::double precision[]
     );
@@ -431,18 +526,16 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.8 |         9 |       -0.9 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.22 |         5 |        0.6 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: mcv cast failure
+-- warn: mcv cast failure, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
-    'null_frac', 0.9::real,
-    'avg_width', 10::integer,
-    'n_distinct', -0.4::real,
+    'null_frac', 0.23::real,
     'most_common_vals', '{2,four,3}'::text,
     'most_common_freqs', '{0.3,0.25,0.05}'::real[]
     );
@@ -460,7 +553,7 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.9 |        10 |       -0.4 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.23 |         5 |        0.6 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
 -- ok: mcv+mcf
@@ -469,9 +562,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'attname', 'id'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 1::integer,
-    'n_distinct', -0.1::real,
     'most_common_vals', '{2,1,3}'::text,
     'most_common_freqs', '{0.3,0.25,0.05}'::real[]
     );
@@ -488,18 +578,16 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.1 |         1 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.23 |         5 |        0.6 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: NULL in histogram array
+-- warn: NULL in histogram array, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
-    'null_frac', 0.2::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.2::real,
+    'null_frac', 0.24::real,
     'histogram_bounds', '{1,NULL,3,4}'::text
     );
 WARNING:  "histogram_bounds" array cannot contain NULL values
@@ -516,7 +604,7 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.2 |         2 |       -0.2 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.24 |         5 |        0.6 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
 -- ok: histogram_bounds
@@ -525,10 +613,8 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'attname', 'id'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
-    'null_frac', 0.3::real,
-    'avg_width', 3::integer,
-    'n_distinct', -0.3::real,
-    'histogram_bounds', '{1,2,3,4}'::text );
+    'histogram_bounds', '{1,2,3,4}'::text
+    );
  pg_restore_attribute_stats 
 ----------------------------
  t
@@ -542,19 +628,17 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.3 |         3 |       -0.3 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.24 |         5 |        0.6 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: elem_count_histogram null element
+-- warn: elem_count_histogram null element, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
-    'null_frac', 0.4::real,
-    'avg_width', 5::integer,
-    'n_distinct', -0.4::real,
-    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    'null_frac', 0.25::real,
+    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1}'::real[]
     );
 WARNING:  "elem_count_histogram" array cannot contain NULL values
  pg_restore_attribute_stats 
@@ -570,7 +654,7 @@ AND inherited = false
 AND attname = 'tags';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | tags    | f         |       0.4 |         5 |       -0.4 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | tags    | f         |      0.25 |         0 |          0 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
 -- ok: elem_count_histogram
@@ -579,9 +663,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'attname', 'tags'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
-    'null_frac', 0.5::real,
-    'avg_width', 6::integer,
-    'n_distinct', -0.55::real,
+    'null_frac', 0.26::real,
     'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
     );
  pg_restore_attribute_stats 
@@ -597,18 +679,16 @@ AND inherited = false
 AND attname = 'tags';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
- stats_import | test      | tags    | f         |       0.5 |         6 |      -0.55 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+ stats_import | test      | tags    | f         |      0.26 |         0 |          0 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
 (1 row)
 
--- range stats on a scalar type
+-- warn: range stats on a scalar type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
-    'null_frac', 0.6::real,
-    'avg_width', 7::integer,
-    'n_distinct', -0.15::real,
+    'null_frac', 0.27::real,
     'range_empty_frac', 0.5::real,
     'range_length_histogram', '{399,499,Infinity}'::text
     );
@@ -627,18 +707,16 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.6 |         7 |      -0.15 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.27 |         5 |        0.6 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: range_empty_frac range_length_hist null mismatch
+-- warn: range_empty_frac range_length_hist null mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
-    'null_frac', 0.7::real,
-    'avg_width', 8::integer,
-    'n_distinct', -0.25::real,
+    'null_frac', 0.28::real,
     'range_length_histogram', '{399,499,Infinity}'::text
     );
 WARNING:  "range_empty_frac" must be specified when "range_length_histogram" is specified
@@ -655,18 +733,16 @@ AND inherited = false
 AND attname = 'arange';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | arange  | f         |       0.7 |         8 |      -0.25 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | arange  | f         |      0.28 |         0 |          0 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: range_empty_frac range_length_hist null mismatch part 2
+-- warn: range_empty_frac range_length_hist null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
-    'null_frac', 0.8::real,
-    'avg_width', 9::integer,
-    'n_distinct', -0.35::real,
+    'null_frac', 0.29::real,
     'range_empty_frac', 0.5::real
     );
 WARNING:  "range_length_histogram" must be specified when "range_empty_frac" is specified
@@ -683,7 +759,7 @@ AND inherited = false
 AND attname = 'arange';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | arange  | f         |       0.8 |         9 |      -0.35 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | arange  | f         |      0.29 |         0 |          0 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
 -- ok: range_empty_frac + range_length_hist
@@ -692,9 +768,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'attname', 'arange'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
-    'null_frac', 0.9::real,
-    'avg_width', 1::integer,
-    'n_distinct', -0.19::real,
     'range_empty_frac', 0.5::real,
     'range_length_histogram', '{399,499,Infinity}'::text
     );
@@ -711,18 +784,16 @@ AND inherited = false
 AND attname = 'arange';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | arange  | f         |       0.9 |         1 |      -0.19 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
+ stats_import | test      | arange  | f         |      0.29 |         0 |          0 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
 (1 row)
 
--- warn: range bounds histogram on scalar
+-- warn: range bounds histogram on scalar, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.29::real,
+    'null_frac', 0.31::real,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
     );
 WARNING:  attribute "id" is not a range type
@@ -740,7 +811,7 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.1 |         2 |      -0.29 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.31 |         5 |        0.6 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
 (1 row)
 
 -- ok: range_bounds_histogram
@@ -749,9 +820,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'attname', 'arange'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
-    'null_frac', 0.2::real,
-    'avg_width', 3::integer,
-    'n_distinct', -0.39::real,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
     );
  pg_restore_attribute_stats 
@@ -767,26 +835,17 @@ AND inherited = false
 AND attname = 'arange';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
- stats_import | test      | arange  | f         |       0.2 |         3 |      -0.39 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+ stats_import | test      | arange  | f         |      0.29 |         0 |          0 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
 (1 row)
 
--- warn: cannot set most_common_elems for range type
+-- warn: cannot set most_common_elems for range type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
-    'most_common_vals', '{"[2,3)","[1,2)","[3,4)"}'::text,
-    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
-    'histogram_bounds', '{"[1,2)","[2,3)","[3,4)","[4,5)"}'::text,
-    'correlation', 1.1::real,
+    'null_frac', 0.32::real,
     'most_common_elems', '{3,1}'::text,
-    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
-    'range_empty_frac', -0.5::real,
-    'range_length_histogram', '{399,499,Infinity}'::text,
-    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
     );
 WARNING:  unable to determine element type of attribute "arange"
 DETAIL:  Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.
@@ -801,19 +860,17 @@ WHERE schemaname = 'stats_import'
 AND tablename = 'test'
 AND inherited = false
 AND attname = 'arange';
-  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct |     most_common_vals      | most_common_freqs |         histogram_bounds          | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
---------------+-----------+---------+-----------+-----------+-----------+------------+---------------------------+-------------------+-----------------------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
- stats_import | test      | arange  | f         |       0.5 |         2 |       -0.1 | {"[2,3)","[1,2)","[3,4)"} | {0.3,0.25,0.05}   | {"[1,2)","[2,3)","[3,4)","[4,5)"} |         1.1 |                   |                        |                      | {399,499,Infinity}     |             -0.5 | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
+ stats_import | test      | arange  | f         |      0.32 |         0 |          0 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
 (1 row)
 
--- warn: scalars can't have mcelem
+-- warn: scalars can't have mcelem, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
+    'null_frac', 0.33::real,
     'most_common_elems', '{1,3}'::text,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
     );
@@ -832,17 +889,15 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.33 |         5 |        0.6 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: mcelem / mcelem mismatch
+-- warn: mcelem / mcelem mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
+    'null_frac', 0.34::real,
     'most_common_elems', '{one,two}'::text
     );
 WARNING:  "most_common_elem_freqs" must be specified when "most_common_elems" is specified
@@ -859,17 +914,15 @@ AND inherited = false
 AND attname = 'tags';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
- stats_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+ stats_import | test      | tags    | f         |      0.34 |         0 |          0 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
 (1 row)
 
--- warn: mcelem / mcelem null mismatch part 2
+-- warn: mcelem / mcelem null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
+    'null_frac', 0.35::real,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
     );
 WARNING:  "most_common_elems" must be specified when "most_common_elem_freqs" is specified
@@ -878,14 +931,22 @@ WARNING:  "most_common_elems" must be specified when "most_common_elem_freqs" is
  f
 (1 row)
 
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
+ stats_import | test      | tags    | f         |      0.35 |         0 |          0 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+(1 row)
+
 -- ok: mcelem
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
     'most_common_elems', '{one,three}'::text,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
     );
@@ -902,18 +963,16 @@ AND inherited = false
 AND attname = 'tags';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
- stats_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             | {one,three}       | {0.3,0.2,0.2,0.3,0}    | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+ stats_import | test      | tags    | f         |      0.35 |         0 |          0 |                  |                   |                  |             | {one,three}       | {0.3,0.2,0.2,0.3,0}    | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
 (1 row)
 
--- warn: scalars can't have elem_count_histogram
+-- warn: scalars can't have elem_count_histogram, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
-    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    'null_frac', 0.36::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1}'::real[]
     );
 WARNING:  unable to determine element type of attribute "id"
 DETAIL:  Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.
@@ -930,43 +989,7 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
-(1 row)
-
--- warn: too many stat kinds
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
-    'most_common_vals', '{"[2,3)","[1,3)","[3,9)"}'::text,
-    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
-    'histogram_bounds', '{"[1,2)","[2,3)","[3,4)","[4,)"}'::text,
-    'correlation', 1.1::real,
-    'most_common_elems', '{3,1}'::text,
-    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
-    'range_empty_frac', -0.5::real,
-    'range_length_histogram', '{399,499,Infinity}'::text,
-    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text);
-WARNING:  unable to determine element type of attribute "arange"
-DETAIL:  Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.
- pg_restore_attribute_stats 
-----------------------------
- f
-(1 row)
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'arange';
-  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct |     most_common_vals      | most_common_freqs |         histogram_bounds         | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
---------------+-----------+---------+-----------+-----------+-----------+------------+---------------------------+-------------------+----------------------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
- stats_import | test      | arange  | f         |       0.5 |         2 |       -0.1 | {"[2,3)","[1,3)","[3,9)"} | {0.3,0.25,0.05}   | {"[1,2)","[2,3)","[3,4)","[4,)"} |         1.1 |                   |                        |                      | {399,499,Infinity}     |             -0.5 | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+ stats_import | test      | id      | f         |      0.36 |         5 |        0.6 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
 (1 row)
 
 --
@@ -986,19 +1009,6 @@ SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import.complex_type,
 UNION ALL
 SELECT 4, 'four', NULL, int4range(0,100), NULL;
 CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
--- restoring stats on index
-SELECT * FROM pg_catalog.pg_restore_relation_stats(
-    'relation', 'stats_import.is_odd'::regclass,
-    'version', '180000'::integer,
-    'relpages', '11'::integer,
-    'reltuples', '10000'::real,
-    'relallvisible', '0'::integer
-);
- pg_restore_relation_stats 
----------------------------
- t
-(1 row)
-
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 CREATE TABLE stats_import.test_clone ( LIKE stats_import.test )
@@ -1176,7 +1186,18 @@ WHERE s.starelid = 'stats_import.is_odd'::regclass;
 ---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
 (0 rows)
 
--- ok
+-- attribute stats exist before a clear, but not after
+SELECT COUNT(*)
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+ count 
+-------
+     1
+(1 row)
+
 SELECT pg_catalog.pg_clear_attribute_stats(
     relation => 'stats_import.test'::regclass,
     attname => 'arange'::name,
@@ -1186,154 +1207,17 @@ SELECT pg_catalog.pg_clear_attribute_stats(
  
 (1 row)
 
---
--- Negative tests
---
---- error: relation is wrong type
-SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::oid,
-        'relpages', 17::integer,
-        'reltuples', 400.0::real,
-        'relallvisible', 4::integer);
-WARNING:  argument "relation" has type "oid", expected type "regclass"
-ERROR:  "relation" cannot be NULL
---- error: relation not found
-SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::regclass,
-        'relpages', 17::integer,
-        'reltuples', 400.0::real,
-        'relallvisible', 4::integer);
-ERROR:  could not open relation with OID 0
--- warn and error: unrecognized argument name
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        'relpages', '17'::integer,
-        'reltuples', 400::real,
-        'nope', 4::integer);
-WARNING:  unrecognized argument name: "nope"
-ERROR:  could not open relation with OID 0
--- error: argument name is NULL
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        NULL, '17'::integer,
-        'reltuples', 400::real,
-        'relallvisible', 4::integer);
-ERROR:  name at variadic position 5 is NULL
--- error: argument name is an integer
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        17, '17'::integer,
-        'reltuples', 400::real,
-        'relallvisible', 4::integer);
-ERROR:  name at variadic position 5 has type "integer", expected type "text"
--- error: odd number of variadic arguments cannot be pairs
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        'relpages', '17'::integer,
-        'reltuples', 400::real,
-        'relallvisible');
-ERROR:  variadic arguments must be name/value pairs
-HINT:  Provide an even number of variadic arguments that can be divided into pairs.
--- error: object doesn't exist
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        'relpages', '17'::integer,
-        'reltuples', 400::real,
-        'relallvisible', 4::integer);
-ERROR:  could not open relation with OID 0
--- error: object does not exist
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', '0'::oid::regclass,
-    'attname', 'id'::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-ERROR:  could not open relation with OID 0
--- error: relation null
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', NULL::oid,
-    'attname', 'id'::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-ERROR:  "relation" cannot be NULL
--- error: missing attname
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', NULL::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-ERROR:  must specify either attname or attnum
--- error: both attname and attnum
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
-    'attnum', 1::smallint,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-ERROR:  cannot specify both attname and attnum
--- error: attname doesn't exist
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'nope'::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-ERROR:  column "nope" of relation "test" does not exist
--- error: attribute is system column
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'xmin'::name,
-    'inherited', false::boolean,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-ERROR:  cannot modify statistics on system column "xmin"
--- error: inherited null
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
-    'inherited', NULL::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-ERROR:  "inherited" cannot be NULL
--- error: relation not found
-SELECT pg_catalog.pg_clear_relation_stats(
-    relation => 'stats_import.nope'::regclass);
-ERROR:  relation "stats_import.nope" does not exist
-LINE 2:     relation => 'stats_import.nope'::regclass);
-                        ^
--- error: attribute is system column
-SELECT pg_catalog.pg_clear_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'ctid'::name,
-    inherited => false::boolean);
-ERROR:  cannot clear statistics on system column "ctid"
--- error: attname doesn't exist
-SELECT pg_catalog.pg_clear_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'nope'::name,
-    inherited => false::boolean);
-ERROR:  column "nope" of relation "test" does not exist
+SELECT COUNT(*)
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+ count 
+-------
+     0
+(1 row)
+
 DROP SCHEMA stats_import CASCADE;
 NOTICE:  drop cascades to 6 other objects
 DETAIL:  drop cascades to type stats_import.complex_type
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index 8c183bceb8a..69e6cc960de 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -17,13 +17,53 @@ CREATE TABLE stats_import.test(
 
 CREATE INDEX test_i ON stats_import.test(id);
 
+--
+-- relstats tests
+--
+
+--- error: relation is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 0::oid,
+        'relpages', 17::integer,
+        'reltuples', 400.0::real,
+        'relallvisible', 4::integer);
+
+-- error: relation not found
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 0::oid::regclass,
+        'relpages', 17::integer,
+        'reltuples', 400.0::real,
+        'relallvisible', 4::integer);
+
+-- error: odd number of variadic arguments cannot be pairs
+SELECT pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'version', 150000::integer,
+        'relpages', '17'::integer,
+        'reltuples', 400::real,
+        'relallvisible');
+
+-- error: argument name is NULL
+SELECT pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'version', 150000::integer,
+        NULL, '17'::integer,
+        'relallvisible', 14::integer);
+
+-- error: argument name is not a text type
+SELECT pg_restore_relation_stats(
+        'relation', '0'::oid::regclass,
+        'version', 150000::integer,
+        17, '17'::integer,
+        'relallvisible', 44::integer);
+
 -- starting stats
 SELECT relpages, reltuples, relallvisible
 FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
+WHERE oid = 'stats_import.test_i'::regclass;
 
-BEGIN;
 -- regular indexes have special case locking rules
+BEGIN;
 SELECT
     pg_catalog.pg_restore_relation_stats(
         'relation', 'stats_import.test_i'::regclass,
@@ -39,20 +79,6 @@ WHERE relation = 'stats_import.test_i'::regclass AND
 
 COMMIT;
 
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.test_i'::regclass,
-        'relpages', 19::integer );
-
--- clear
-SELECT
-    pg_catalog.pg_clear_relation_stats(
-        'stats_import.test'::regclass);
-
-SELECT relpages, reltuples, relallvisible
-FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
-
 --  relpages may be -1 for partitioned tables
 CREATE TABLE stats_import.part_parent ( i integer ) PARTITION BY RANGE(i);
 CREATE TABLE stats_import.part_child_1
@@ -68,18 +94,6 @@ SELECT relpages
 FROM pg_class
 WHERE oid = 'stats_import.part_parent'::regclass;
 
--- although partitioned tables have no storage, setting relpages to a
--- positive value is still allowed
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent_i'::regclass,
-        'relpages', 2::integer);
-
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent'::regclass,
-        'relpages', 2::integer);
-
 --
 -- Partitioned indexes aren't analyzed but it is possible to set
 -- stats. The locking rules are different from normal indexes due to
@@ -103,22 +117,15 @@ WHERE relation = 'stats_import.part_parent_i'::regclass AND
 
 COMMIT;
 
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent_i'::regclass,
-        'relpages', 2::integer);
+SELECT relpages
+FROM pg_class
+WHERE oid = 'stats_import.part_parent_i'::regclass;
 
--- nothing stops us from setting it to -1
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent'::regclass,
-        'relpages', -1::integer);
-
--- ok: set all stats
+-- ok: set all stats, no bounds checking
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
         'version', 150000::integer,
-        'relpages', '17'::integer,
+        'relpages', '-17'::integer,
         'reltuples', 400::real,
         'relallvisible', 4::integer);
 
@@ -126,7 +133,7 @@ SELECT relpages, reltuples, relallvisible
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
--- ok: just relpages
+-- ok: set just relpages, rest stay same
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
         'version', 150000::integer,
@@ -136,7 +143,7 @@ SELECT relpages, reltuples, relallvisible
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
--- ok: just reltuples
+-- ok: set just reltuples, rest stay same
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
         'version', 150000::integer,
@@ -146,7 +153,7 @@ SELECT relpages, reltuples, relallvisible
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
--- ok: just relallvisible
+-- ok: set just relallvisible, rest stay same
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
         'version', 150000::integer,
@@ -156,7 +163,7 @@ SELECT relpages, reltuples, relallvisible
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
--- warn: bad relpages type
+-- warn: bad relpages type, rest updated
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
         'version', 150000::integer,
@@ -168,17 +175,118 @@ SELECT relpages, reltuples, relallvisible
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
+-- unrecognized argument name, rest ok
+SELECT pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'version', 150000::integer,
+        'relpages', '171'::integer,
+        'nope', 10::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+-- ok: clear stats
+SELECT pg_catalog.pg_clear_relation_stats(
+    relation => 'stats_import.test'::regclass);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
 -- invalid relkinds for statistics
 CREATE SEQUENCE stats_import.testseq;
-CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
-SELECT
-    pg_catalog.pg_clear_relation_stats(
+
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.testseq'::regclass);
+
+SELECT pg_catalog.pg_clear_relation_stats(
         'stats_import.testseq'::regclass);
-SELECT
-    pg_catalog.pg_clear_relation_stats(
+
+CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
+
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.testview'::regclass);
+
+SELECT pg_catalog.pg_clear_relation_stats(
         'stats_import.testview'::regclass);
 
--- ok: no stakinds
+--
+-- attribute stats
+--
+
+-- error: object does not exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', '0'::oid::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real);
+
+-- error: relation null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', NULL::oid::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real);
+
+-- error: NULL attname
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', NULL::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real);
+
+-- error: attname doesn't exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'nope'::name,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: both attname and attnum
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'attnum', 1::smallint,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real);
+
+-- error: neither attname nor attnum
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'inherited', false::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real);
+
+-- error: attribute is system column
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'xmin'::name,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: inherited null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', NULL::boolean,
+    'version', 150000::integer,
+    'null_frac', 0.1::real);
+
+-- error: attribute is system column
+SELECT pg_catalog.pg_clear_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'ctid'::name,
+    inherited => false::boolean);
+
+-- ok: just the fixed values, no stakinds
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
@@ -195,15 +303,17 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- ok: restore by attnum
+--
+-- ok: restore by attnum, we normally reserve this for
+-- indexes, but there is no reason it shouldn't work
+-- for any stat-having relation.
+--
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attnum', 1::smallint,
     'inherited', false::boolean,
     'version', 150000::integer,
-    'null_frac', 0.4::real,
-    'avg_width', 5::integer,
-    'n_distinct', 0.6::real);
+    'null_frac', 0.4::real);
 
 SELECT *
 FROM pg_stats
@@ -212,14 +322,13 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: unrecognized argument name
+-- warn: unrecognized argument name, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
     'null_frac', 0.2::real,
-    'avg_width', NULL::integer,
     'nope', 0.5::real);
 
 SELECT *
@@ -229,15 +338,13 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: mcv / mcf null mismatch part 1
+-- warn: mcv / mcf null mismatch part 1, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
-    'null_frac', 0.6::real,
-    'avg_width', 7::integer,
-    'n_distinct', -0.7::real,
+    'null_frac', 0.21::real,
     'most_common_freqs', '{0.1,0.2,0.3}'::real[]
     );
 
@@ -248,15 +355,13 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: mcv / mcf null mismatch part 2
+-- warn: mcv / mcf null mismatch part 2, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
-    'null_frac', 0.7::real,
-    'avg_width', 8::integer,
-    'n_distinct', -0.8::real,
+    'null_frac', 0.21::real,
     'most_common_vals', '{1,2,3}'::text
     );
 
@@ -267,15 +372,13 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: mcv / mcf type mismatch
+-- warn: mcf type mismatch, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
-    'null_frac', 0.8::real,
-    'avg_width', 9::integer,
-    'n_distinct', -0.9::real,
+    'null_frac', 0.22::real,
     'most_common_vals', '{2,1,3}'::text,
     'most_common_freqs', '{0.2,0.1}'::double precision[]
     );
@@ -287,15 +390,13 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: mcv cast failure
+-- warn: mcv cast failure, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
-    'null_frac', 0.9::real,
-    'avg_width', 10::integer,
-    'n_distinct', -0.4::real,
+    'null_frac', 0.23::real,
     'most_common_vals', '{2,four,3}'::text,
     'most_common_freqs', '{0.3,0.25,0.05}'::real[]
     );
@@ -313,9 +414,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'attname', 'id'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 1::integer,
-    'n_distinct', -0.1::real,
     'most_common_vals', '{2,1,3}'::text,
     'most_common_freqs', '{0.3,0.25,0.05}'::real[]
     );
@@ -327,15 +425,13 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: NULL in histogram array
+-- warn: NULL in histogram array, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
-    'null_frac', 0.2::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.2::real,
+    'null_frac', 0.24::real,
     'histogram_bounds', '{1,NULL,3,4}'::text
     );
 
@@ -352,10 +448,8 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'attname', 'id'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
-    'null_frac', 0.3::real,
-    'avg_width', 3::integer,
-    'n_distinct', -0.3::real,
-    'histogram_bounds', '{1,2,3,4}'::text );
+    'histogram_bounds', '{1,2,3,4}'::text
+    );
 
 SELECT *
 FROM pg_stats
@@ -364,16 +458,14 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: elem_count_histogram null element
+-- warn: elem_count_histogram null element, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
-    'null_frac', 0.4::real,
-    'avg_width', 5::integer,
-    'n_distinct', -0.4::real,
-    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    'null_frac', 0.25::real,
+    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1}'::real[]
     );
 
 SELECT *
@@ -389,9 +481,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'attname', 'tags'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
-    'null_frac', 0.5::real,
-    'avg_width', 6::integer,
-    'n_distinct', -0.55::real,
+    'null_frac', 0.26::real,
     'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
     );
 
@@ -402,15 +492,13 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'tags';
 
--- range stats on a scalar type
+-- warn: range stats on a scalar type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
-    'null_frac', 0.6::real,
-    'avg_width', 7::integer,
-    'n_distinct', -0.15::real,
+    'null_frac', 0.27::real,
     'range_empty_frac', 0.5::real,
     'range_length_histogram', '{399,499,Infinity}'::text
     );
@@ -422,15 +510,13 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: range_empty_frac range_length_hist null mismatch
+-- warn: range_empty_frac range_length_hist null mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
-    'null_frac', 0.7::real,
-    'avg_width', 8::integer,
-    'n_distinct', -0.25::real,
+    'null_frac', 0.28::real,
     'range_length_histogram', '{399,499,Infinity}'::text
     );
 
@@ -441,15 +527,13 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'arange';
 
--- warn: range_empty_frac range_length_hist null mismatch part 2
+-- warn: range_empty_frac range_length_hist null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
-    'null_frac', 0.8::real,
-    'avg_width', 9::integer,
-    'n_distinct', -0.35::real,
+    'null_frac', 0.29::real,
     'range_empty_frac', 0.5::real
     );
 
@@ -466,9 +550,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'attname', 'arange'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
-    'null_frac', 0.9::real,
-    'avg_width', 1::integer,
-    'n_distinct', -0.19::real,
     'range_empty_frac', 0.5::real,
     'range_length_histogram', '{399,499,Infinity}'::text
     );
@@ -480,15 +561,13 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'arange';
 
--- warn: range bounds histogram on scalar
+-- warn: range bounds histogram on scalar, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.29::real,
+    'null_frac', 0.31::real,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
     );
 
@@ -505,9 +584,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'attname', 'arange'::name,
     'inherited', false::boolean,
     'version', 150000::integer,
-    'null_frac', 0.2::real,
-    'avg_width', 3::integer,
-    'n_distinct', -0.39::real,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
     );
 
@@ -518,23 +594,14 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'arange';
 
--- warn: cannot set most_common_elems for range type
+-- warn: cannot set most_common_elems for range type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
-    'most_common_vals', '{"[2,3)","[1,2)","[3,4)"}'::text,
-    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
-    'histogram_bounds', '{"[1,2)","[2,3)","[3,4)","[4,5)"}'::text,
-    'correlation', 1.1::real,
+    'null_frac', 0.32::real,
     'most_common_elems', '{3,1}'::text,
-    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
-    'range_empty_frac', -0.5::real,
-    'range_length_histogram', '{399,499,Infinity}'::text,
-    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
     );
 
 SELECT *
@@ -544,14 +611,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'arange';
 
--- warn: scalars can't have mcelem
+-- warn: scalars can't have mcelem, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
+    'null_frac', 0.33::real,
     'most_common_elems', '{1,3}'::text,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
     );
@@ -563,14 +628,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: mcelem / mcelem mismatch
+-- warn: mcelem / mcelem mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
+    'null_frac', 0.34::real,
     'most_common_elems', '{one,two}'::text
     );
 
@@ -581,25 +644,27 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'tags';
 
--- warn: mcelem / mcelem null mismatch part 2
+-- warn: mcelem / mcelem null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
+    'null_frac', 0.35::real,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
     );
 
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
 -- ok: mcelem
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
     'most_common_elems', '{one,three}'::text,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
     );
@@ -611,15 +676,13 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'tags';
 
--- warn: scalars can't have elem_count_histogram
+-- warn: scalars can't have elem_count_histogram, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
-    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    'null_frac', 0.36::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1}'::real[]
     );
 
 SELECT *
@@ -629,32 +692,6 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: too many stat kinds
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
-    'most_common_vals', '{"[2,3)","[1,3)","[3,9)"}'::text,
-    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
-    'histogram_bounds', '{"[1,2)","[2,3)","[3,4)","[4,)"}'::text,
-    'correlation', 1.1::real,
-    'most_common_elems', '{3,1}'::text,
-    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
-    'range_empty_frac', -0.5::real,
-    'range_length_histogram', '{399,499,Infinity}'::text,
-    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text);
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'arange';
-
 --
 -- Test the ability to exactly copy data from one table to an identical table,
 -- correctly reconstructing the stakind order as well as the staopN and
@@ -674,15 +711,6 @@ SELECT 4, 'four', NULL, int4range(0,100), NULL;
 
 CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
 
--- restoring stats on index
-SELECT * FROM pg_catalog.pg_restore_relation_stats(
-    'relation', 'stats_import.is_odd'::regclass,
-    'version', '180000'::integer,
-    'relpages', '11'::integer,
-    'reltuples', '10000'::real,
-    'relallvisible', '0'::integer
-);
-
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 
@@ -835,154 +863,24 @@ FROM pg_statistic s
 JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
 WHERE s.starelid = 'stats_import.is_odd'::regclass;
 
--- ok
+-- attribute stats exist before a clear, but not after
+SELECT COUNT(*)
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
 SELECT pg_catalog.pg_clear_attribute_stats(
     relation => 'stats_import.test'::regclass,
     attname => 'arange'::name,
     inherited => false::boolean);
 
---
--- Negative tests
---
-
---- error: relation is wrong type
-SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::oid,
-        'relpages', 17::integer,
-        'reltuples', 400.0::real,
-        'relallvisible', 4::integer);
-
---- error: relation not found
-SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::regclass,
-        'relpages', 17::integer,
-        'reltuples', 400.0::real,
-        'relallvisible', 4::integer);
-
--- warn and error: unrecognized argument name
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        'relpages', '17'::integer,
-        'reltuples', 400::real,
-        'nope', 4::integer);
-
--- error: argument name is NULL
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        NULL, '17'::integer,
-        'reltuples', 400::real,
-        'relallvisible', 4::integer);
-
--- error: argument name is an integer
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        17, '17'::integer,
-        'reltuples', 400::real,
-        'relallvisible', 4::integer);
-
--- error: odd number of variadic arguments cannot be pairs
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        'relpages', '17'::integer,
-        'reltuples', 400::real,
-        'relallvisible');
-
--- error: object doesn't exist
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        'relpages', '17'::integer,
-        'reltuples', 400::real,
-        'relallvisible', 4::integer);
-
--- error: object does not exist
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', '0'::oid::regclass,
-    'attname', 'id'::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-
--- error: relation null
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', NULL::oid,
-    'attname', 'id'::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-
--- error: missing attname
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', NULL::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-
--- error: both attname and attnum
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
-    'attnum', 1::smallint,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-
--- error: attname doesn't exist
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'nope'::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-
--- error: attribute is system column
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'xmin'::name,
-    'inherited', false::boolean,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-
--- error: inherited null
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
-    'inherited', NULL::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-
--- error: relation not found
-SELECT pg_catalog.pg_clear_relation_stats(
-    relation => 'stats_import.nope'::regclass);
-
--- error: attribute is system column
-SELECT pg_catalog.pg_clear_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'ctid'::name,
-    inherited => false::boolean);
-
--- error: attname doesn't exist
-SELECT pg_catalog.pg_clear_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'nope'::name,
-    inherited => false::boolean);
+SELECT COUNT(*)
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
 
 DROP SCHEMA stats_import CASCADE;

base-commit: 62ec3e1f6786181431210643a2d427b9a98b8af8
-- 
2.48.1

#395Melanie Plageman
melanieplageman@gmail.com
In reply to: Corey Huinker (#394)
Re: Statistics Import and Export

On Wed, Feb 26, 2025 at 9:19 PM Corey Huinker <corey.huinker@gmail.com> wrote:

+1, let's shorten those queries. The coast is probably pretty
clear now if you want to go do that.

On it.

So, I started reviewing this and my original thought about shortening
the queries testing pg_restore_relation_stats() wasn't included in
your patch.

For example:

+--- error: relation is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 0::oid,
+        'relpages', 17::integer,
+        'reltuples', 400.0::real,
+        'relallvisible', 4::integer);

Why do you need to specify all the stats (relpages, reltuples, etc)?
To exercise this you could just do:
select pg_catalog.pg_restore_relation_stats('relation', 0::oid);

Since I haven't been following along with this feature development, I
don't think I can get comfortable enough with all of the changes in
this test diff to commit them. I can't really say if this is the set
of tests that is representative and sufficient for this feature.

If you agree with me that the failure tests could be shorter, I'm
happy to commit that, but I don't really feel comfortable assessing
what the right set of full tests is.

- Melanie

#396Jeff Davis
pgsql@j-davis.com
In reply to: Ashutosh Bapat (#363)
1 attachment(s)
Re: Statistics Import and Export

On Tue, 2025-02-25 at 11:11 +0530, Ashutosh Bapat wrote:

So the dumped statistics are not restored exactly. The reason for
this
is the table statistics is dumped before dumping ALTER TABLE ... ADD
CONSTRAINT command which changes the statistics. I think all the
pg_restore_relation_stats() calls should be dumped after all the
schema and data modifications have been done. OR what's the point in
dumping statistics only to get rewritten even before restore
finishes.

In your example, it's not so bad because the stats are actually better:
the index is built after the data is present, and therefore relpages
and reltuples are correct.

The problem is more clear if you use --no-data. If you load data,
ANALYZE, pg_dump --no-data, then reload the sql file, then the stats
are lost.

That workflow is very close to what pg_upgrade does. We solved the
problem for pg_upgrade in commit 71b66171d0 by simply not updating the
statistics when building an index and IsBinaryUpgrade.

To solve the issue with dump --no-data, I propose that we change the
test in 71b66171d0 to only update the stats if the physical relpages is
non-zero.

Patch attached:

* If the dump is --no-data, or during pg_upgrade, the table will be
empty, so the physical relpages will be zero and the restored stats
won't be overwritten.

* If (like in your example) the dump includes data, the new stats are
based on real data, so they are better anyway. This is sort of like the
case where autoanalyze kicks in.

* If the dump is --statistics-only, then there won't be any indexes
created in the SQL file, so when you restore the stats, they will
remain until you do something else to change them.

* If your example really is a problem, you'd need to dump first with -
-no-statistics, and then with --statistics-only, and restore the two
SQL files in order.

Alternatively, we could put stats into SECTION_POST_DATA, which was
already discussed[*], and we decided against it (though there was not a
clear consensus).

Regards,
Jeff Davis

*:
/messages/by-id/1798867.1712376328@sss.pgh.pa.us

Attachments:

v1-0001-Do-not-update-stats-on-empty-table-when-building-.patchtext/x-patch; charset=UTF-8; name=v1-0001-Do-not-update-stats-on-empty-table-when-building-.patchDownload
From 86c03cb525b49d24019c5c0ea8ec36bb82b3c58a Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Thu, 27 Feb 2025 17:06:00 -0800
Subject: [PATCH v1] Do not update stats on empty table when building index.

We previously fixed this for binary upgrade in 71b66171d0, but a
similar problem exists when using pg_dump --no-data without pg_upgrade
involved. Fix both problems by not updating the stats when the table
has no pages.

Reported-by: Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>
Discussion: https://postgr.es/m/CAExHW5vf9D+8-a5_BEX3y=2y_xY9hiCxV1=C+FnxDvfprWvkng@mail.gmail.com
---
 src/backend/catalog/index.c                | 17 +++++++++++------
 src/test/regress/expected/stats_import.out | 22 +++++++++++++++++++++-
 src/test/regress/sql/stats_import.sql      | 12 ++++++++++++
 3 files changed, 44 insertions(+), 7 deletions(-)

diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index f37b990c81d..1a3fdeab350 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2833,11 +2833,7 @@ index_update_stats(Relation rel,
 	if (reltuples == 0 && rel->rd_rel->reltuples < 0)
 		reltuples = -1;
 
-	/*
-	 * Don't update statistics during binary upgrade, because the indexes are
-	 * created before the data is moved into place.
-	 */
-	update_stats = reltuples >= 0 && !IsBinaryUpgrade;
+	update_stats = reltuples >= 0;
 
 	/*
 	 * Finish I/O and visibility map buffer locks before
@@ -2850,7 +2846,16 @@ index_update_stats(Relation rel,
 	{
 		relpages = RelationGetNumberOfBlocks(rel);
 
-		if (rel->rd_rel->relkind != RELKIND_INDEX)
+		/*
+		 * Don't update statistics when the relation is completely empty. This
+		 * is important during binary upgrade, because at the time the schema
+		 * is loaded, the data has not yet been moved into place. It's also
+		 * useful when restoring a dump containing only schema and statistics.
+		 */
+		if (relpages == 0)
+			update_stats = false;
+
+		if (update_stats && rel->rd_rel->relkind != RELKIND_INDEX)
 			visibilitymap_count(rel, &relallvisible, NULL);
 	}
 
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index 1f150f7b08d..4c81fb60c91 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -12,14 +12,34 @@ CREATE TABLE stats_import.test(
     arange int4range,
     tags text[]
 ) WITH (autovacuum_enabled = false);
+SELECT
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'relpages', 18::integer,
+	'reltuples', 21::real,
+	'relallvisible', 24::integer);
+ pg_restore_relation_stats 
+---------------------------
+ t
+(1 row)
+
+-- creating an index on an empty table shouldn't overwrite stats
 CREATE INDEX test_i ON stats_import.test(id);
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       18 |        21 |            24
+(1 row)
+
 -- starting stats
 SELECT relpages, reltuples, relallvisible
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
  relpages | reltuples | relallvisible 
 ----------+-----------+---------------
-        0 |        -1 |             0
+       18 |        21 |            24
 (1 row)
 
 BEGIN;
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index 8c183bceb8a..c8abb715130 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -15,8 +15,20 @@ CREATE TABLE stats_import.test(
     tags text[]
 ) WITH (autovacuum_enabled = false);
 
+SELECT
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'relpages', 18::integer,
+	'reltuples', 21::real,
+	'relallvisible', 24::integer);
+
+-- creating an index on an empty table shouldn't overwrite stats
 CREATE INDEX test_i ON stats_import.test(id);
 
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
 -- starting stats
 SELECT relpages, reltuples, relallvisible
 FROM pg_class
-- 
2.34.1

#397Corey Huinker
corey.huinker@gmail.com
In reply to: Melanie Plageman (#395)
Re: Statistics Import and Export
+--- error: relation is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 0::oid,
+        'relpages', 17::integer,
+        'reltuples', 400.0::real,
+        'relallvisible', 4::integer);

Why do you need to specify all the stats (relpages, reltuples, etc)?
To exercise this you could just do:
select pg_catalog.pg_restore_relation_stats('relation', 0::oid);

In the above case, it's historical inertia in that the pg_set_* call
required all those parameters, as well as a fear that the code - now or in
the future - might evaluate "can anything actually change from this call"
and short circuit out before actually trying to make sense of the reg_class
oid. But we can assuage that fear with just one of the three stat
parameters, and I'll adjust accordingly.

Since I haven't been following along with this feature development, I
don't think I can get comfortable enough with all of the changes in
this test diff to commit them. I can't really say if this is the set
of tests that is representative and sufficient for this feature.

That's fine, I hadn't anticipated that you'd review this patch, let alone
commit it.

If you agree with me that the failure tests could be shorter, I'm
happy to commit that, but I don't really feel comfortable assessing
what the right set of full tests is.

The set of tests is as short as I feel comfortable with. I'll give the
parameter lists one more pass and repost.

#398Greg Sabino Mullane
htamfids@gmail.com
In reply to: Jeff Davis (#396)
Re: Statistics Import and Export

I know I'm coming late to this, but I would like us to rethink having
statistics dumped by default. I was caught by this today, as I was doing
two dumps in a row, but the output changed between runs solely because the
stats got updated. It got me thinking about all the use cases of pg_dump
I've seen over the years. I think this has the potential to cause a lot of
problems for things like automated scripts. It certainly violates the
principle of least astonishment to have dumps change when no user
interaction has happened.

Alternatively, we could put stats into SECTION_POST_DATA,

No, that would make the above-mentioned problem much worse.

Cheers,
Greg

--
Crunchy Data - https://www.crunchydata.com
Enterprise Postgres Software Products & Tech Support

#399Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#397)
1 attachment(s)
Re: Statistics Import and Export

On Thu, Feb 27, 2025 at 10:01 PM Corey Huinker <corey.huinker@gmail.com>
wrote:

+--- error: relation is wrong type

+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 0::oid,
+        'relpages', 17::integer,
+        'reltuples', 400.0::real,
+        'relallvisible', 4::integer);

Why do you need to specify all the stats (relpages, reltuples, etc)?
To exercise this you could just do:
select pg_catalog.pg_restore_relation_stats('relation', 0::oid);

In the above case, it's historical inertia in that the pg_set_* call
required all those parameters, as well as a fear that the code - now or in
the future - might evaluate "can anything actually change from this call"
and short circuit out before actually trying to make sense of the reg_class
oid. But we can assuage that fear with just one of the three stat
parameters, and I'll adjust accordingly.

* reduced relstats parameters specified to the minimum needed to verify the
error and avoid a theoretical future logic short-circuit described above.
* version parameter usage reduced to absolute minimum - verifying that it
is accepted and ignored, though Melanie's patch may introduce a need to
bring it back in a place or two.

84 lines deleted. Not great, not terrible.

I suppose if we really trusted the TAP test databases to have "one of
everything" in terms of tables with all the datatypes, and sufficient rows
to generate interesting stats, plus some indexes of each, then we could get
rid of those two, but I feel very strongly that it would be a minor savings
at a major cost to clarity.

Attachments:

v2-0001-Organize-and-deduplicate-statistics-import-tests.patchtext/x-patch; charset=US-ASCII; name=v2-0001-Organize-and-deduplicate-statistics-import-tests.patchDownload
From d0dfca62eb8ce27fb4bfce77bd2ee835738899a8 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 26 Feb 2025 21:02:44 -0500
Subject: [PATCH v2] Organize and deduplicate statistics import tests.

Many changes, refactorings, and rebasings have taken their toll on the
statistics import tests. Now that things appear more stable and the
pg_set_* functions are gone in favor of using pg_restore_* in all cases,
it's safe to remove duplicates, combine tests where possible, and make
the test descriptions a bit more descriptive and uniform.

Additionally, parameters that were not strictly needed to demonstrate
the purpose(s) of a test were removed to reduce clutter.
---
 src/test/regress/expected/stats_import.out | 680 ++++++++-------------
 src/test/regress/sql/stats_import.sql      | 554 +++++++----------
 2 files changed, 466 insertions(+), 768 deletions(-)

diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index 1f150f7b08d..7bd7bfb3e7b 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -13,19 +13,48 @@ CREATE TABLE stats_import.test(
     tags text[]
 ) WITH (autovacuum_enabled = false);
 CREATE INDEX test_i ON stats_import.test(id);
+--
+-- relstats tests
+--
+--- error: relation is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 0::oid,
+        'relpages', 17::integer);
+WARNING:  argument "relation" has type "oid", expected type "regclass"
+ERROR:  "relation" cannot be NULL
+-- error: relation not found
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 0::oid::regclass,
+        'relpages', 17::integer);
+ERROR:  could not open relation with OID 0
+-- error: odd number of variadic arguments cannot be pairs
+SELECT pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'relallvisible');
+ERROR:  variadic arguments must be name/value pairs
+HINT:  Provide an even number of variadic arguments that can be divided into pairs.
+-- error: argument name is NULL
+SELECT pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        NULL, '17'::integer);
+ERROR:  name at variadic position 3 is NULL
+-- error: argument name is not a text type
+SELECT pg_restore_relation_stats(
+        'relation', '0'::oid::regclass,
+        17, '17'::integer);
+ERROR:  name at variadic position 3 has type "integer", expected type "text"
 -- starting stats
 SELECT relpages, reltuples, relallvisible
 FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
+WHERE oid = 'stats_import.test_i'::regclass;
  relpages | reltuples | relallvisible 
 ----------+-----------+---------------
-        0 |        -1 |             0
+        1 |         0 |             0
 (1 row)
 
-BEGIN;
 -- regular indexes have special case locking rules
-SELECT
-    pg_catalog.pg_restore_relation_stats(
+BEGIN;
+SELECT pg_catalog.pg_restore_relation_stats(
         'relation', 'stats_import.test_i'::regclass,
         'relpages', 18::integer);
  pg_restore_relation_stats 
@@ -50,32 +79,6 @@ WHERE relation = 'stats_import.test_i'::regclass AND
 (1 row)
 
 COMMIT;
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.test_i'::regclass,
-        'relpages', 19::integer );
- pg_restore_relation_stats 
----------------------------
- t
-(1 row)
-
--- clear
-SELECT
-    pg_catalog.pg_clear_relation_stats(
-        'stats_import.test'::regclass);
- pg_clear_relation_stats 
--------------------------
- 
-(1 row)
-
-SELECT relpages, reltuples, relallvisible
-FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
- relpages | reltuples | relallvisible 
-----------+-----------+---------------
-        0 |        -1 |             0
-(1 row)
-
 --  relpages may be -1 for partitioned tables
 CREATE TABLE stats_import.part_parent ( i integer ) PARTITION BY RANGE(i);
 CREATE TABLE stats_import.part_child_1
@@ -92,26 +95,6 @@ WHERE oid = 'stats_import.part_parent'::regclass;
        -1
 (1 row)
 
--- although partitioned tables have no storage, setting relpages to a
--- positive value is still allowed
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent_i'::regclass,
-        'relpages', 2::integer);
- pg_restore_relation_stats 
----------------------------
- t
-(1 row)
-
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent'::regclass,
-        'relpages', 2::integer);
- pg_restore_relation_stats 
----------------------------
- t
-(1 row)
-
 --
 -- Partitioned indexes aren't analyzed but it is possible to set
 -- stats. The locking rules are different from normal indexes due to
@@ -119,8 +102,7 @@ SELECT
 -- partitioned index are locked in ShareUpdateExclusive mode.
 --
 BEGIN;
-SELECT
-    pg_catalog.pg_restore_relation_stats(
+SELECT pg_catalog.pg_restore_relation_stats(
         'relation', 'stats_import.part_parent_i'::regclass,
         'relpages', 2::integer);
  pg_restore_relation_stats 
@@ -145,30 +127,19 @@ WHERE relation = 'stats_import.part_parent_i'::regclass AND
 (1 row)
 
 COMMIT;
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent_i'::regclass,
-        'relpages', 2::integer);
- pg_restore_relation_stats 
----------------------------
- t
+SELECT relpages
+FROM pg_class
+WHERE oid = 'stats_import.part_parent_i'::regclass;
+ relpages 
+----------
+        2
 (1 row)
 
--- nothing stops us from setting it to -1
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent'::regclass,
-        'relpages', -1::integer);
- pg_restore_relation_stats 
----------------------------
- t
-(1 row)
-
--- ok: set all stats
+-- ok: set all relstats, with version, no bounds checking
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
         'version', 150000::integer,
-        'relpages', '17'::integer,
+        'relpages', '-17'::integer,
         'reltuples', 400::real,
         'relallvisible', 4::integer);
  pg_restore_relation_stats 
@@ -181,13 +152,12 @@ FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
  relpages | reltuples | relallvisible 
 ----------+-----------+---------------
-       17 |       400 |             4
+      -17 |       400 |             4
 (1 row)
 
--- ok: just relpages
+-- ok: set just relpages, rest stay same
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
-        'version', 150000::integer,
         'relpages', '16'::integer);
  pg_restore_relation_stats 
 ---------------------------
@@ -202,10 +172,9 @@ WHERE oid = 'stats_import.test'::regclass;
        16 |       400 |             4
 (1 row)
 
--- ok: just reltuples
+-- ok: set just reltuples, rest stay same
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
-        'version', 150000::integer,
         'reltuples', '500'::real);
  pg_restore_relation_stats 
 ---------------------------
@@ -220,10 +189,9 @@ WHERE oid = 'stats_import.test'::regclass;
        16 |       500 |             4
 (1 row)
 
--- ok: just relallvisible
+-- ok: set just relallvisible, rest stay same
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
-        'version', 150000::integer,
         'relallvisible', 5::integer);
  pg_restore_relation_stats 
 ---------------------------
@@ -238,10 +206,9 @@ WHERE oid = 'stats_import.test'::regclass;
        16 |       500 |             5
 (1 row)
 
--- warn: bad relpages type
+-- warn: bad relpages type, rest updated
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
-        'version', 150000::integer,
         'relpages', 'nope'::text,
         'reltuples', 400.0::real,
         'relallvisible', 4::integer);
@@ -259,20 +226,128 @@ WHERE oid = 'stats_import.test'::regclass;
        16 |       400 |             4
 (1 row)
 
+-- unrecognized argument name, rest ok
+SELECT pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'relpages', '171'::integer,
+        'nope', 10::integer);
+WARNING:  unrecognized argument name: "nope"
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+      171 |       400 |             4
+(1 row)
+
+-- ok: clear stats
+SELECT pg_catalog.pg_clear_relation_stats(
+    relation => 'stats_import.test'::regclass);
+ pg_clear_relation_stats 
+-------------------------
+ 
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+        0 |        -1 |             0
+(1 row)
+
 -- invalid relkinds for statistics
 CREATE SEQUENCE stats_import.testseq;
-CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
-SELECT
-    pg_catalog.pg_clear_relation_stats(
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.testseq'::regclass);
+ERROR:  cannot modify statistics for relation "testseq"
+DETAIL:  This operation is not supported for sequences.
+SELECT pg_catalog.pg_clear_relation_stats(
         'stats_import.testseq'::regclass);
 ERROR:  cannot modify statistics for relation "testseq"
 DETAIL:  This operation is not supported for sequences.
-SELECT
-    pg_catalog.pg_clear_relation_stats(
+CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.testview'::regclass);
+ERROR:  cannot modify statistics for relation "testview"
+DETAIL:  This operation is not supported for views.
+SELECT pg_catalog.pg_clear_relation_stats(
         'stats_import.testview'::regclass);
 ERROR:  cannot modify statistics for relation "testview"
 DETAIL:  This operation is not supported for views.
--- ok: no stakinds
+--
+-- attribute stats
+--
+-- error: object does not exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', '0'::oid::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+ERROR:  could not open relation with OID 0
+-- error: relation null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', NULL::oid::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+ERROR:  "relation" cannot be NULL
+-- error: NULL attname
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', NULL::name,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+ERROR:  must specify either attname or attnum
+-- error: attname doesn't exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'nope'::name,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ERROR:  column "nope" of relation "test" does not exist
+-- error: both attname and attnum
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'attnum', 1::smallint,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+ERROR:  cannot specify both attname and attnum
+-- error: neither attname nor attnum
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+ERROR:  must specify either attname or attnum
+-- error: attribute is system column
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'xmin'::name,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+ERROR:  cannot modify statistics on system column "xmin"
+-- error: inherited null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', NULL::boolean,
+    'null_frac', 0.1::real);
+ERROR:  "inherited" cannot be NULL
+-- error: attribute is system column
+SELECT pg_catalog.pg_clear_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'ctid'::name,
+    inherited => false::boolean);
+ERROR:  cannot clear statistics on system column "ctid"
+-- ok: just the fixed values, with version, no stakinds
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
@@ -297,15 +372,16 @@ AND attname = 'id';
  stats_import | test      | id      | f         |       0.2 |         5 |        0.6 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- ok: restore by attnum
+--
+-- ok: restore by attnum, we normally reserve this for
+-- indexes, but there is no reason it shouldn't work
+-- for any stat-having relation.
+--
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attnum', 1::smallint,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.4::real,
-    'avg_width', 5::integer,
-    'n_distinct', 0.6::real);
+    'null_frac', 0.4::real);
  pg_restore_attribute_stats 
 ----------------------------
  t
@@ -322,14 +398,12 @@ AND attname = 'id';
  stats_import | test      | id      | f         |       0.4 |         5 |        0.6 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: unrecognized argument name
+-- warn: unrecognized argument name, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
     'null_frac', 0.2::real,
-    'avg_width', NULL::integer,
     'nope', 0.5::real);
 WARNING:  unrecognized argument name: "nope"
  pg_restore_attribute_stats 
@@ -348,15 +422,12 @@ AND attname = 'id';
  stats_import | test      | id      | f         |       0.2 |         5 |        0.6 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: mcv / mcf null mismatch part 1
+-- warn: mcv / mcf null mismatch part 1, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.6::real,
-    'avg_width', 7::integer,
-    'n_distinct', -0.7::real,
+    'null_frac', 0.21::real,
     'most_common_freqs', '{0.1,0.2,0.3}'::real[]
     );
 WARNING:  "most_common_vals" must be specified when "most_common_freqs" is specified
@@ -373,18 +444,15 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.6 |         7 |       -0.7 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.21 |         5 |        0.6 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: mcv / mcf null mismatch part 2
+-- warn: mcv / mcf null mismatch part 2, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.7::real,
-    'avg_width', 8::integer,
-    'n_distinct', -0.8::real,
+    'null_frac', 0.21::real,
     'most_common_vals', '{1,2,3}'::text
     );
 WARNING:  "most_common_freqs" must be specified when "most_common_vals" is specified
@@ -401,18 +469,15 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.7 |         8 |       -0.8 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.21 |         5 |        0.6 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: mcv / mcf type mismatch
+-- warn: mcf type mismatch, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.8::real,
-    'avg_width', 9::integer,
-    'n_distinct', -0.9::real,
+    'null_frac', 0.22::real,
     'most_common_vals', '{2,1,3}'::text,
     'most_common_freqs', '{0.2,0.1}'::double precision[]
     );
@@ -431,18 +496,15 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.8 |         9 |       -0.9 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.22 |         5 |        0.6 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: mcv cast failure
+-- warn: mcv cast failure, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.9::real,
-    'avg_width', 10::integer,
-    'n_distinct', -0.4::real,
+    'null_frac', 0.23::real,
     'most_common_vals', '{2,four,3}'::text,
     'most_common_freqs', '{0.3,0.25,0.05}'::real[]
     );
@@ -460,7 +522,7 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.9 |        10 |       -0.4 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.23 |         5 |        0.6 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
 -- ok: mcv+mcf
@@ -468,10 +530,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 1::integer,
-    'n_distinct', -0.1::real,
     'most_common_vals', '{2,1,3}'::text,
     'most_common_freqs', '{0.3,0.25,0.05}'::real[]
     );
@@ -488,18 +546,15 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.1 |         1 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.23 |         5 |        0.6 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: NULL in histogram array
+-- warn: NULL in histogram array, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.2::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.2::real,
+    'null_frac', 0.24::real,
     'histogram_bounds', '{1,NULL,3,4}'::text
     );
 WARNING:  "histogram_bounds" array cannot contain NULL values
@@ -516,7 +571,7 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.2 |         2 |       -0.2 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.24 |         5 |        0.6 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
 -- ok: histogram_bounds
@@ -524,11 +579,8 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.3::real,
-    'avg_width', 3::integer,
-    'n_distinct', -0.3::real,
-    'histogram_bounds', '{1,2,3,4}'::text );
+    'histogram_bounds', '{1,2,3,4}'::text
+    );
  pg_restore_attribute_stats 
 ----------------------------
  t
@@ -542,19 +594,16 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.3 |         3 |       -0.3 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.24 |         5 |        0.6 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: elem_count_histogram null element
+-- warn: elem_count_histogram null element, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.4::real,
-    'avg_width', 5::integer,
-    'n_distinct', -0.4::real,
-    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    'null_frac', 0.25::real,
+    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1}'::real[]
     );
 WARNING:  "elem_count_histogram" array cannot contain NULL values
  pg_restore_attribute_stats 
@@ -570,7 +619,7 @@ AND inherited = false
 AND attname = 'tags';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | tags    | f         |       0.4 |         5 |       -0.4 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | tags    | f         |      0.25 |         0 |          0 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
 -- ok: elem_count_histogram
@@ -578,10 +627,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.5::real,
-    'avg_width', 6::integer,
-    'n_distinct', -0.55::real,
+    'null_frac', 0.26::real,
     'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
     );
  pg_restore_attribute_stats 
@@ -597,18 +643,15 @@ AND inherited = false
 AND attname = 'tags';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
- stats_import | test      | tags    | f         |       0.5 |         6 |      -0.55 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+ stats_import | test      | tags    | f         |      0.26 |         0 |          0 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
 (1 row)
 
--- range stats on a scalar type
+-- warn: range stats on a scalar type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.6::real,
-    'avg_width', 7::integer,
-    'n_distinct', -0.15::real,
+    'null_frac', 0.27::real,
     'range_empty_frac', 0.5::real,
     'range_length_histogram', '{399,499,Infinity}'::text
     );
@@ -627,18 +670,15 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.6 |         7 |      -0.15 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.27 |         5 |        0.6 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: range_empty_frac range_length_hist null mismatch
+-- warn: range_empty_frac range_length_hist null mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.7::real,
-    'avg_width', 8::integer,
-    'n_distinct', -0.25::real,
+    'null_frac', 0.28::real,
     'range_length_histogram', '{399,499,Infinity}'::text
     );
 WARNING:  "range_empty_frac" must be specified when "range_length_histogram" is specified
@@ -655,18 +695,15 @@ AND inherited = false
 AND attname = 'arange';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | arange  | f         |       0.7 |         8 |      -0.25 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | arange  | f         |      0.28 |         0 |          0 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: range_empty_frac range_length_hist null mismatch part 2
+-- warn: range_empty_frac range_length_hist null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.8::real,
-    'avg_width', 9::integer,
-    'n_distinct', -0.35::real,
+    'null_frac', 0.29::real,
     'range_empty_frac', 0.5::real
     );
 WARNING:  "range_length_histogram" must be specified when "range_empty_frac" is specified
@@ -683,7 +720,7 @@ AND inherited = false
 AND attname = 'arange';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | arange  | f         |       0.8 |         9 |      -0.35 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | arange  | f         |      0.29 |         0 |          0 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
 -- ok: range_empty_frac + range_length_hist
@@ -691,10 +728,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.9::real,
-    'avg_width', 1::integer,
-    'n_distinct', -0.19::real,
     'range_empty_frac', 0.5::real,
     'range_length_histogram', '{399,499,Infinity}'::text
     );
@@ -711,18 +744,15 @@ AND inherited = false
 AND attname = 'arange';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | arange  | f         |       0.9 |         1 |      -0.19 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
+ stats_import | test      | arange  | f         |      0.29 |         0 |          0 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
 (1 row)
 
--- warn: range bounds histogram on scalar
+-- warn: range bounds histogram on scalar, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.29::real,
+    'null_frac', 0.31::real,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
     );
 WARNING:  attribute "id" is not a range type
@@ -740,7 +770,7 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.1 |         2 |      -0.29 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.31 |         5 |        0.6 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
 (1 row)
 
 -- ok: range_bounds_histogram
@@ -748,10 +778,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.2::real,
-    'avg_width', 3::integer,
-    'n_distinct', -0.39::real,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
     );
  pg_restore_attribute_stats 
@@ -767,26 +793,17 @@ AND inherited = false
 AND attname = 'arange';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
- stats_import | test      | arange  | f         |       0.2 |         3 |      -0.39 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+ stats_import | test      | arange  | f         |      0.29 |         0 |          0 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
 (1 row)
 
--- warn: cannot set most_common_elems for range type
+-- warn: cannot set most_common_elems for range type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
-    'most_common_vals', '{"[2,3)","[1,2)","[3,4)"}'::text,
-    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
-    'histogram_bounds', '{"[1,2)","[2,3)","[3,4)","[4,5)"}'::text,
-    'correlation', 1.1::real,
+    'null_frac', 0.32::real,
     'most_common_elems', '{3,1}'::text,
-    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
-    'range_empty_frac', -0.5::real,
-    'range_length_histogram', '{399,499,Infinity}'::text,
-    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
     );
 WARNING:  unable to determine element type of attribute "arange"
 DETAIL:  Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.
@@ -801,19 +818,17 @@ WHERE schemaname = 'stats_import'
 AND tablename = 'test'
 AND inherited = false
 AND attname = 'arange';
-  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct |     most_common_vals      | most_common_freqs |         histogram_bounds          | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
---------------+-----------+---------+-----------+-----------+-----------+------------+---------------------------+-------------------+-----------------------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
- stats_import | test      | arange  | f         |       0.5 |         2 |       -0.1 | {"[2,3)","[1,2)","[3,4)"} | {0.3,0.25,0.05}   | {"[1,2)","[2,3)","[3,4)","[4,5)"} |         1.1 |                   |                        |                      | {399,499,Infinity}     |             -0.5 | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
+ stats_import | test      | arange  | f         |      0.32 |         0 |          0 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
 (1 row)
 
--- warn: scalars can't have mcelem
+-- warn: scalars can't have mcelem, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
+    'null_frac', 0.33::real,
     'most_common_elems', '{1,3}'::text,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
     );
@@ -832,17 +847,15 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.33 |         5 |        0.6 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: mcelem / mcelem mismatch
+-- warn: mcelem / mcelem mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
+    'null_frac', 0.34::real,
     'most_common_elems', '{one,two}'::text
     );
 WARNING:  "most_common_elem_freqs" must be specified when "most_common_elems" is specified
@@ -859,17 +872,15 @@ AND inherited = false
 AND attname = 'tags';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
- stats_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+ stats_import | test      | tags    | f         |      0.34 |         0 |          0 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
 (1 row)
 
--- warn: mcelem / mcelem null mismatch part 2
+-- warn: mcelem / mcelem null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
+    'null_frac', 0.35::real,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
     );
 WARNING:  "most_common_elems" must be specified when "most_common_elem_freqs" is specified
@@ -878,14 +889,22 @@ WARNING:  "most_common_elems" must be specified when "most_common_elem_freqs" is
  f
 (1 row)
 
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
+ stats_import | test      | tags    | f         |      0.35 |         0 |          0 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+(1 row)
+
 -- ok: mcelem
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
     'most_common_elems', '{one,three}'::text,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
     );
@@ -902,18 +921,16 @@ AND inherited = false
 AND attname = 'tags';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
- stats_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             | {one,three}       | {0.3,0.2,0.2,0.3,0}    | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+ stats_import | test      | tags    | f         |      0.35 |         0 |          0 |                  |                   |                  |             | {one,three}       | {0.3,0.2,0.2,0.3,0}    | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
 (1 row)
 
--- warn: scalars can't have elem_count_histogram
+-- warn: scalars can't have elem_count_histogram, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
-    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    'null_frac', 0.36::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1}'::real[]
     );
 WARNING:  unable to determine element type of attribute "id"
 DETAIL:  Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.
@@ -930,43 +947,7 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
-(1 row)
-
--- warn: too many stat kinds
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
-    'most_common_vals', '{"[2,3)","[1,3)","[3,9)"}'::text,
-    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
-    'histogram_bounds', '{"[1,2)","[2,3)","[3,4)","[4,)"}'::text,
-    'correlation', 1.1::real,
-    'most_common_elems', '{3,1}'::text,
-    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
-    'range_empty_frac', -0.5::real,
-    'range_length_histogram', '{399,499,Infinity}'::text,
-    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text);
-WARNING:  unable to determine element type of attribute "arange"
-DETAIL:  Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.
- pg_restore_attribute_stats 
-----------------------------
- f
-(1 row)
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'arange';
-  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct |     most_common_vals      | most_common_freqs |         histogram_bounds         | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
---------------+-----------+---------+-----------+-----------+-----------+------------+---------------------------+-------------------+----------------------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
- stats_import | test      | arange  | f         |       0.5 |         2 |       -0.1 | {"[2,3)","[1,3)","[3,9)"} | {0.3,0.25,0.05}   | {"[1,2)","[2,3)","[3,4)","[4,)"} |         1.1 |                   |                        |                      | {399,499,Infinity}     |             -0.5 | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+ stats_import | test      | id      | f         |      0.36 |         5 |        0.6 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
 (1 row)
 
 --
@@ -986,19 +967,6 @@ SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import.complex_type,
 UNION ALL
 SELECT 4, 'four', NULL, int4range(0,100), NULL;
 CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
--- restoring stats on index
-SELECT * FROM pg_catalog.pg_restore_relation_stats(
-    'relation', 'stats_import.is_odd'::regclass,
-    'version', '180000'::integer,
-    'relpages', '11'::integer,
-    'reltuples', '10000'::real,
-    'relallvisible', '0'::integer
-);
- pg_restore_relation_stats 
----------------------------
- t
-(1 row)
-
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 CREATE TABLE stats_import.test_clone ( LIKE stats_import.test )
@@ -1176,7 +1144,18 @@ WHERE s.starelid = 'stats_import.is_odd'::regclass;
 ---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
 (0 rows)
 
--- ok
+-- attribute stats exist before a clear, but not after
+SELECT COUNT(*)
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+ count 
+-------
+     1
+(1 row)
+
 SELECT pg_catalog.pg_clear_attribute_stats(
     relation => 'stats_import.test'::regclass,
     attname => 'arange'::name,
@@ -1186,154 +1165,17 @@ SELECT pg_catalog.pg_clear_attribute_stats(
  
 (1 row)
 
---
--- Negative tests
---
---- error: relation is wrong type
-SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::oid,
-        'relpages', 17::integer,
-        'reltuples', 400.0::real,
-        'relallvisible', 4::integer);
-WARNING:  argument "relation" has type "oid", expected type "regclass"
-ERROR:  "relation" cannot be NULL
---- error: relation not found
-SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::regclass,
-        'relpages', 17::integer,
-        'reltuples', 400.0::real,
-        'relallvisible', 4::integer);
-ERROR:  could not open relation with OID 0
--- warn and error: unrecognized argument name
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        'relpages', '17'::integer,
-        'reltuples', 400::real,
-        'nope', 4::integer);
-WARNING:  unrecognized argument name: "nope"
-ERROR:  could not open relation with OID 0
--- error: argument name is NULL
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        NULL, '17'::integer,
-        'reltuples', 400::real,
-        'relallvisible', 4::integer);
-ERROR:  name at variadic position 5 is NULL
--- error: argument name is an integer
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        17, '17'::integer,
-        'reltuples', 400::real,
-        'relallvisible', 4::integer);
-ERROR:  name at variadic position 5 has type "integer", expected type "text"
--- error: odd number of variadic arguments cannot be pairs
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        'relpages', '17'::integer,
-        'reltuples', 400::real,
-        'relallvisible');
-ERROR:  variadic arguments must be name/value pairs
-HINT:  Provide an even number of variadic arguments that can be divided into pairs.
--- error: object doesn't exist
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        'relpages', '17'::integer,
-        'reltuples', 400::real,
-        'relallvisible', 4::integer);
-ERROR:  could not open relation with OID 0
--- error: object does not exist
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', '0'::oid::regclass,
-    'attname', 'id'::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-ERROR:  could not open relation with OID 0
--- error: relation null
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', NULL::oid,
-    'attname', 'id'::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-ERROR:  "relation" cannot be NULL
--- error: missing attname
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', NULL::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-ERROR:  must specify either attname or attnum
--- error: both attname and attnum
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
-    'attnum', 1::smallint,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-ERROR:  cannot specify both attname and attnum
--- error: attname doesn't exist
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'nope'::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-ERROR:  column "nope" of relation "test" does not exist
--- error: attribute is system column
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'xmin'::name,
-    'inherited', false::boolean,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-ERROR:  cannot modify statistics on system column "xmin"
--- error: inherited null
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
-    'inherited', NULL::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-ERROR:  "inherited" cannot be NULL
--- error: relation not found
-SELECT pg_catalog.pg_clear_relation_stats(
-    relation => 'stats_import.nope'::regclass);
-ERROR:  relation "stats_import.nope" does not exist
-LINE 2:     relation => 'stats_import.nope'::regclass);
-                        ^
--- error: attribute is system column
-SELECT pg_catalog.pg_clear_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'ctid'::name,
-    inherited => false::boolean);
-ERROR:  cannot clear statistics on system column "ctid"
--- error: attname doesn't exist
-SELECT pg_catalog.pg_clear_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'nope'::name,
-    inherited => false::boolean);
-ERROR:  column "nope" of relation "test" does not exist
+SELECT COUNT(*)
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+ count 
+-------
+     0
+(1 row)
+
 DROP SCHEMA stats_import CASCADE;
 NOTICE:  drop cascades to 6 other objects
 DETAIL:  drop cascades to type stats_import.complex_type
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index 8c183bceb8a..7b2c7d6617f 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -17,15 +17,43 @@ CREATE TABLE stats_import.test(
 
 CREATE INDEX test_i ON stats_import.test(id);
 
+--
+-- relstats tests
+--
+
+--- error: relation is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 0::oid,
+        'relpages', 17::integer);
+
+-- error: relation not found
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 0::oid::regclass,
+        'relpages', 17::integer);
+
+-- error: odd number of variadic arguments cannot be pairs
+SELECT pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'relallvisible');
+
+-- error: argument name is NULL
+SELECT pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        NULL, '17'::integer);
+
+-- error: argument name is not a text type
+SELECT pg_restore_relation_stats(
+        'relation', '0'::oid::regclass,
+        17, '17'::integer);
+
 -- starting stats
 SELECT relpages, reltuples, relallvisible
 FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
+WHERE oid = 'stats_import.test_i'::regclass;
 
-BEGIN;
 -- regular indexes have special case locking rules
-SELECT
-    pg_catalog.pg_restore_relation_stats(
+BEGIN;
+SELECT pg_catalog.pg_restore_relation_stats(
         'relation', 'stats_import.test_i'::regclass,
         'relpages', 18::integer);
 
@@ -39,20 +67,6 @@ WHERE relation = 'stats_import.test_i'::regclass AND
 
 COMMIT;
 
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.test_i'::regclass,
-        'relpages', 19::integer );
-
--- clear
-SELECT
-    pg_catalog.pg_clear_relation_stats(
-        'stats_import.test'::regclass);
-
-SELECT relpages, reltuples, relallvisible
-FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
-
 --  relpages may be -1 for partitioned tables
 CREATE TABLE stats_import.part_parent ( i integer ) PARTITION BY RANGE(i);
 CREATE TABLE stats_import.part_child_1
@@ -68,18 +82,6 @@ SELECT relpages
 FROM pg_class
 WHERE oid = 'stats_import.part_parent'::regclass;
 
--- although partitioned tables have no storage, setting relpages to a
--- positive value is still allowed
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent_i'::regclass,
-        'relpages', 2::integer);
-
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent'::regclass,
-        'relpages', 2::integer);
-
 --
 -- Partitioned indexes aren't analyzed but it is possible to set
 -- stats. The locking rules are different from normal indexes due to
@@ -88,8 +90,7 @@ SELECT
 --
 BEGIN;
 
-SELECT
-    pg_catalog.pg_restore_relation_stats(
+SELECT pg_catalog.pg_restore_relation_stats(
         'relation', 'stats_import.part_parent_i'::regclass,
         'relpages', 2::integer);
 
@@ -103,22 +104,15 @@ WHERE relation = 'stats_import.part_parent_i'::regclass AND
 
 COMMIT;
 
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent_i'::regclass,
-        'relpages', 2::integer);
+SELECT relpages
+FROM pg_class
+WHERE oid = 'stats_import.part_parent_i'::regclass;
 
--- nothing stops us from setting it to -1
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent'::regclass,
-        'relpages', -1::integer);
-
--- ok: set all stats
+-- ok: set all relstats, with version, no bounds checking
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
         'version', 150000::integer,
-        'relpages', '17'::integer,
+        'relpages', '-17'::integer,
         'reltuples', 400::real,
         'relallvisible', 4::integer);
 
@@ -126,40 +120,36 @@ SELECT relpages, reltuples, relallvisible
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
--- ok: just relpages
+-- ok: set just relpages, rest stay same
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
-        'version', 150000::integer,
         'relpages', '16'::integer);
 
 SELECT relpages, reltuples, relallvisible
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
--- ok: just reltuples
+-- ok: set just reltuples, rest stay same
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
-        'version', 150000::integer,
         'reltuples', '500'::real);
 
 SELECT relpages, reltuples, relallvisible
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
--- ok: just relallvisible
+-- ok: set just relallvisible, rest stay same
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
-        'version', 150000::integer,
         'relallvisible', 5::integer);
 
 SELECT relpages, reltuples, relallvisible
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
--- warn: bad relpages type
+-- warn: bad relpages type, rest updated
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
-        'version', 150000::integer,
         'relpages', 'nope'::text,
         'reltuples', 400.0::real,
         'relallvisible', 4::integer);
@@ -168,17 +158,110 @@ SELECT relpages, reltuples, relallvisible
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
+-- unrecognized argument name, rest ok
+SELECT pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'relpages', '171'::integer,
+        'nope', 10::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+-- ok: clear stats
+SELECT pg_catalog.pg_clear_relation_stats(
+    relation => 'stats_import.test'::regclass);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
 -- invalid relkinds for statistics
 CREATE SEQUENCE stats_import.testseq;
-CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
-SELECT
-    pg_catalog.pg_clear_relation_stats(
+
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.testseq'::regclass);
+
+SELECT pg_catalog.pg_clear_relation_stats(
         'stats_import.testseq'::regclass);
-SELECT
-    pg_catalog.pg_clear_relation_stats(
+
+CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
+
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.testview'::regclass);
+
+SELECT pg_catalog.pg_clear_relation_stats(
         'stats_import.testview'::regclass);
 
--- ok: no stakinds
+--
+-- attribute stats
+--
+
+-- error: object does not exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', '0'::oid::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: relation null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', NULL::oid::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: NULL attname
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', NULL::name,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: attname doesn't exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'nope'::name,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: both attname and attnum
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'attnum', 1::smallint,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: neither attname nor attnum
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: attribute is system column
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'xmin'::name,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: inherited null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', NULL::boolean,
+    'null_frac', 0.1::real);
+
+-- error: attribute is system column
+SELECT pg_catalog.pg_clear_attribute_stats(
+    relation => 'stats_import.test'::regclass,
+    attname => 'ctid'::name,
+    inherited => false::boolean);
+
+-- ok: just the fixed values, with version, no stakinds
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
@@ -195,15 +278,16 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- ok: restore by attnum
+--
+-- ok: restore by attnum, we normally reserve this for
+-- indexes, but there is no reason it shouldn't work
+-- for any stat-having relation.
+--
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attnum', 1::smallint,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.4::real,
-    'avg_width', 5::integer,
-    'n_distinct', 0.6::real);
+    'null_frac', 0.4::real);
 
 SELECT *
 FROM pg_stats
@@ -212,14 +296,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: unrecognized argument name
+-- warn: unrecognized argument name, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
     'null_frac', 0.2::real,
-    'avg_width', NULL::integer,
     'nope', 0.5::real);
 
 SELECT *
@@ -229,15 +311,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: mcv / mcf null mismatch part 1
+-- warn: mcv / mcf null mismatch part 1, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.6::real,
-    'avg_width', 7::integer,
-    'n_distinct', -0.7::real,
+    'null_frac', 0.21::real,
     'most_common_freqs', '{0.1,0.2,0.3}'::real[]
     );
 
@@ -248,15 +327,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: mcv / mcf null mismatch part 2
+-- warn: mcv / mcf null mismatch part 2, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.7::real,
-    'avg_width', 8::integer,
-    'n_distinct', -0.8::real,
+    'null_frac', 0.21::real,
     'most_common_vals', '{1,2,3}'::text
     );
 
@@ -267,15 +343,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: mcv / mcf type mismatch
+-- warn: mcf type mismatch, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.8::real,
-    'avg_width', 9::integer,
-    'n_distinct', -0.9::real,
+    'null_frac', 0.22::real,
     'most_common_vals', '{2,1,3}'::text,
     'most_common_freqs', '{0.2,0.1}'::double precision[]
     );
@@ -287,15 +360,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: mcv cast failure
+-- warn: mcv cast failure, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.9::real,
-    'avg_width', 10::integer,
-    'n_distinct', -0.4::real,
+    'null_frac', 0.23::real,
     'most_common_vals', '{2,four,3}'::text,
     'most_common_freqs', '{0.3,0.25,0.05}'::real[]
     );
@@ -312,10 +382,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 1::integer,
-    'n_distinct', -0.1::real,
     'most_common_vals', '{2,1,3}'::text,
     'most_common_freqs', '{0.3,0.25,0.05}'::real[]
     );
@@ -327,15 +393,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: NULL in histogram array
+-- warn: NULL in histogram array, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.2::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.2::real,
+    'null_frac', 0.24::real,
     'histogram_bounds', '{1,NULL,3,4}'::text
     );
 
@@ -351,11 +414,8 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.3::real,
-    'avg_width', 3::integer,
-    'n_distinct', -0.3::real,
-    'histogram_bounds', '{1,2,3,4}'::text );
+    'histogram_bounds', '{1,2,3,4}'::text
+    );
 
 SELECT *
 FROM pg_stats
@@ -364,16 +424,13 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: elem_count_histogram null element
+-- warn: elem_count_histogram null element, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.4::real,
-    'avg_width', 5::integer,
-    'n_distinct', -0.4::real,
-    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    'null_frac', 0.25::real,
+    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1}'::real[]
     );
 
 SELECT *
@@ -388,10 +445,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.5::real,
-    'avg_width', 6::integer,
-    'n_distinct', -0.55::real,
+    'null_frac', 0.26::real,
     'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
     );
 
@@ -402,15 +456,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'tags';
 
--- range stats on a scalar type
+-- warn: range stats on a scalar type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.6::real,
-    'avg_width', 7::integer,
-    'n_distinct', -0.15::real,
+    'null_frac', 0.27::real,
     'range_empty_frac', 0.5::real,
     'range_length_histogram', '{399,499,Infinity}'::text
     );
@@ -422,15 +473,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: range_empty_frac range_length_hist null mismatch
+-- warn: range_empty_frac range_length_hist null mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.7::real,
-    'avg_width', 8::integer,
-    'n_distinct', -0.25::real,
+    'null_frac', 0.28::real,
     'range_length_histogram', '{399,499,Infinity}'::text
     );
 
@@ -441,15 +489,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'arange';
 
--- warn: range_empty_frac range_length_hist null mismatch part 2
+-- warn: range_empty_frac range_length_hist null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.8::real,
-    'avg_width', 9::integer,
-    'n_distinct', -0.35::real,
+    'null_frac', 0.29::real,
     'range_empty_frac', 0.5::real
     );
 
@@ -465,10 +510,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.9::real,
-    'avg_width', 1::integer,
-    'n_distinct', -0.19::real,
     'range_empty_frac', 0.5::real,
     'range_length_histogram', '{399,499,Infinity}'::text
     );
@@ -480,15 +521,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'arange';
 
--- warn: range bounds histogram on scalar
+-- warn: range bounds histogram on scalar, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.29::real,
+    'null_frac', 0.31::real,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
     );
 
@@ -504,10 +542,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.2::real,
-    'avg_width', 3::integer,
-    'n_distinct', -0.39::real,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
     );
 
@@ -518,23 +552,14 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'arange';
 
--- warn: cannot set most_common_elems for range type
+-- warn: cannot set most_common_elems for range type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
-    'most_common_vals', '{"[2,3)","[1,2)","[3,4)"}'::text,
-    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
-    'histogram_bounds', '{"[1,2)","[2,3)","[3,4)","[4,5)"}'::text,
-    'correlation', 1.1::real,
+    'null_frac', 0.32::real,
     'most_common_elems', '{3,1}'::text,
-    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
-    'range_empty_frac', -0.5::real,
-    'range_length_histogram', '{399,499,Infinity}'::text,
-    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
     );
 
 SELECT *
@@ -544,14 +569,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'arange';
 
--- warn: scalars can't have mcelem
+-- warn: scalars can't have mcelem, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
+    'null_frac', 0.33::real,
     'most_common_elems', '{1,3}'::text,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
     );
@@ -563,14 +586,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: mcelem / mcelem mismatch
+-- warn: mcelem / mcelem mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
+    'null_frac', 0.34::real,
     'most_common_elems', '{one,two}'::text
     );
 
@@ -581,25 +602,27 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'tags';
 
--- warn: mcelem / mcelem null mismatch part 2
+-- warn: mcelem / mcelem null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
+    'null_frac', 0.35::real,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
     );
 
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
 -- ok: mcelem
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
     'most_common_elems', '{one,three}'::text,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
     );
@@ -611,15 +634,13 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'tags';
 
--- warn: scalars can't have elem_count_histogram
+-- warn: scalars can't have elem_count_histogram, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
-    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    'null_frac', 0.36::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1}'::real[]
     );
 
 SELECT *
@@ -629,32 +650,6 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: too many stat kinds
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
-    'most_common_vals', '{"[2,3)","[1,3)","[3,9)"}'::text,
-    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
-    'histogram_bounds', '{"[1,2)","[2,3)","[3,4)","[4,)"}'::text,
-    'correlation', 1.1::real,
-    'most_common_elems', '{3,1}'::text,
-    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
-    'range_empty_frac', -0.5::real,
-    'range_length_histogram', '{399,499,Infinity}'::text,
-    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text);
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'arange';
-
 --
 -- Test the ability to exactly copy data from one table to an identical table,
 -- correctly reconstructing the stakind order as well as the staopN and
@@ -674,15 +669,6 @@ SELECT 4, 'four', NULL, int4range(0,100), NULL;
 
 CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
 
--- restoring stats on index
-SELECT * FROM pg_catalog.pg_restore_relation_stats(
-    'relation', 'stats_import.is_odd'::regclass,
-    'version', '180000'::integer,
-    'relpages', '11'::integer,
-    'reltuples', '10000'::real,
-    'relallvisible', '0'::integer
-);
-
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 
@@ -835,154 +821,24 @@ FROM pg_statistic s
 JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
 WHERE s.starelid = 'stats_import.is_odd'::regclass;
 
--- ok
+-- attribute stats exist before a clear, but not after
+SELECT COUNT(*)
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
 SELECT pg_catalog.pg_clear_attribute_stats(
     relation => 'stats_import.test'::regclass,
     attname => 'arange'::name,
     inherited => false::boolean);
 
---
--- Negative tests
---
-
---- error: relation is wrong type
-SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::oid,
-        'relpages', 17::integer,
-        'reltuples', 400.0::real,
-        'relallvisible', 4::integer);
-
---- error: relation not found
-SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::regclass,
-        'relpages', 17::integer,
-        'reltuples', 400.0::real,
-        'relallvisible', 4::integer);
-
--- warn and error: unrecognized argument name
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        'relpages', '17'::integer,
-        'reltuples', 400::real,
-        'nope', 4::integer);
-
--- error: argument name is NULL
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        NULL, '17'::integer,
-        'reltuples', 400::real,
-        'relallvisible', 4::integer);
-
--- error: argument name is an integer
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        17, '17'::integer,
-        'reltuples', 400::real,
-        'relallvisible', 4::integer);
-
--- error: odd number of variadic arguments cannot be pairs
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        'relpages', '17'::integer,
-        'reltuples', 400::real,
-        'relallvisible');
-
--- error: object doesn't exist
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        'relpages', '17'::integer,
-        'reltuples', 400::real,
-        'relallvisible', 4::integer);
-
--- error: object does not exist
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', '0'::oid::regclass,
-    'attname', 'id'::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-
--- error: relation null
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', NULL::oid,
-    'attname', 'id'::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-
--- error: missing attname
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', NULL::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-
--- error: both attname and attnum
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
-    'attnum', 1::smallint,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-
--- error: attname doesn't exist
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'nope'::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-
--- error: attribute is system column
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'xmin'::name,
-    'inherited', false::boolean,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-
--- error: inherited null
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
-    'inherited', NULL::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-
--- error: relation not found
-SELECT pg_catalog.pg_clear_relation_stats(
-    relation => 'stats_import.nope'::regclass);
-
--- error: attribute is system column
-SELECT pg_catalog.pg_clear_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'ctid'::name,
-    inherited => false::boolean);
-
--- error: attname doesn't exist
-SELECT pg_catalog.pg_clear_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'nope'::name,
-    inherited => false::boolean);
+SELECT COUNT(*)
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
 
 DROP SCHEMA stats_import CASCADE;

base-commit: c2a50ac678eb5ccee271aef3e7ed146ac395a32b
-- 
2.48.1

#400Ashutosh Bapat
ashutosh.bapat.oss@gmail.com
In reply to: Jeff Davis (#396)
Re: Statistics Import and Export: difference in statistics dumped

Hi Jeff,
I am changing the subject on this email and thus creating a new thread
to discuss this issue.

On Fri, Feb 28, 2025 at 8:02 AM Jeff Davis <pgsql@j-davis.com> wrote:

On Tue, 2025-02-25 at 11:11 +0530, Ashutosh Bapat wrote:

So the dumped statistics are not restored exactly. The reason for
this
is the table statistics is dumped before dumping ALTER TABLE ... ADD
CONSTRAINT command which changes the statistics. I think all the
pg_restore_relation_stats() calls should be dumped after all the
schema and data modifications have been done. OR what's the point in
dumping statistics only to get rewritten even before restore
finishes.

In your example, it's not so bad because the stats are actually better:
the index is built after the data is present, and therefore relpages
and reltuples are correct.

The problem is more clear if you use --no-data. If you load data,
ANALYZE, pg_dump --no-data, then reload the sql file, then the stats
are lost.

That workflow is very close to what pg_upgrade does. We solved the
problem for pg_upgrade in commit 71b66171d0 by simply not updating the
statistics when building an index and IsBinaryUpgrade.

To solve the issue with dump --no-data, I propose that we change the
test in 71b66171d0 to only update the stats if the physical relpages is
non-zero.

I don't think I understand the patch well, but here's one question: If
a table is truncated and index is rebuilt would the code in patch stop
it from updating the stats? If yes, that looks problematic.

Patch attached:

* If the dump is --no-data, or during pg_upgrade, the table will be
empty, so the physical relpages will be zero and the restored stats
won't be overwritten.

* If (like in your example) the dump includes data, the new stats are
based on real data, so they are better anyway. This is sort of like the
case where autoanalyze kicks in.

* If the dump is --statistics-only, then there won't be any indexes
created in the SQL file, so when you restore the stats, they will
remain until you do something else to change them.

* If your example really is a problem, you'd need to dump first with -
-no-statistics, and then with --statistics-only, and restore the two
SQL files in order.

There are few problems

1. If there are thousands of tables with primary key constraints, we
have twice the number of calls to pg_restore_relation_stats() of which
only half will be useful. The stats written by the first set of calls
will be overwritten by the second set of calls. The time spent in
executing the first set of calls can be saved completely and to some
extent time dumping the calls as well. It will be some measurable
improvement I think.

2. We aren't restoring the statistics faithfully - as mentioned in
Greg's reply. If users dump and restore with autovacuum turned off,
they will be surprised to see the statistics to be different on the
original and restored database - which may have other effects like
change in plans.

3. The test I am building over at [1]/messages/by-id/CAExHW5sBbMki6Xs4XxFQQF3C4Wx3wxkLAcySrtuW3vrnOxXDNQ@mail.gmail.com is aimed at testing whether the
objects dumped get restored faithfully by comparing dumps from the
original and restored database. That's a bit crude method but is being
used by some of our tests. I think it will be good to test statistics
as well in that test. But if it's not going to be same on the original
and the restored database we can not test it. For now, I have used
--no-statistics.

Alternatively, we could put stats into SECTION_POST_DATA, which was
already discussed[*], and we decided against it (though there was not a
clear consensus).

I haven't looked at the code which dumps the statistics, but it does
seem simple dump the statistics after the constraint creation command
for the tables with primary key constraint. That will dump
not-up-to-date statistics and might overwrite the statistics

[1]: /messages/by-id/CAExHW5sBbMki6Xs4XxFQQF3C4Wx3wxkLAcySrtuW3vrnOxXDNQ@mail.gmail.com

--
Best Wishes,
Ashutosh Bapat

#401Jeff Davis
pgsql@j-davis.com
In reply to: Ashutosh Bapat (#400)
1 attachment(s)
Re: Statistics Import and Export: difference in statistics dumped

On Fri, 2025-02-28 at 14:51 +0530, Ashutosh Bapat wrote:

2. We aren't restoring the statistics faithfully - as mentioned in
Greg's reply. If users dump and restore with autovacuum turned off,
they will be surprised to see the statistics to be different on the
original and restored database - which may have other effects like
change in plans.

Then let's just address that concern directly: disable updating stats
implicitly if autovacuum is off. If autovacuum is on, the user
shouldn't have an expectation of stable stats anyway. Patch attached.

Regards,
Jeff Davis

Attachments:

v2-0001-During-CREATE-INDEX-don-t-update-stats-if-autovac.patchtext/x-patch; charset=UTF-8; name=v2-0001-During-CREATE-INDEX-don-t-update-stats-if-autovac.patchDownload
From a8945b9ce4e358f4a79d3065c07f3b42a94fd387 Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Thu, 27 Feb 2025 17:06:00 -0800
Subject: [PATCH v2] During CREATE INDEX, don't update stats if autovacuum is
 off.

We previously fixed this for binary upgrade in 71b66171d0, but a
similar problem existed when using pg_dump --no-data without
pg_upgrade involved.

Fix by not implicitly updating stats during create index when
autovacuum is disabled.

Reported-by: Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>
Discussion: https://postgr.es/m/CAExHW5vf9D+8-a5_BEX3y=2y_xY9hiCxV1=C+FnxDvfprWvkng@mail.gmail.com
---
 src/backend/catalog/index.c                | 31 +++++++++++++++--
 src/test/regress/expected/stats_import.out | 39 ++++++++++++++++++----
 src/test/regress/sql/stats_import.sql      | 22 ++++++++++--
 3 files changed, 82 insertions(+), 10 deletions(-)

diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index f37b990c81d..318a44e1e1d 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -63,6 +63,7 @@
 #include "optimizer/optimizer.h"
 #include "parser/parser.h"
 #include "pgstat.h"
+#include "postmaster/autovacuum.h"
 #include "rewrite/rewriteManip.h"
 #include "storage/bufmgr.h"
 #include "storage/lmgr.h"
@@ -121,6 +122,7 @@ static void UpdateIndexRelation(Oid indexoid, Oid heapoid,
 								bool isready);
 static void index_update_stats(Relation rel,
 							   bool hasindex,
+							   bool update_stats,
 							   double reltuples);
 static void IndexCheckExclusion(Relation heapRelation,
 								Relation indexRelation,
@@ -1261,6 +1263,17 @@ index_create(Relation heapRelation,
 	}
 	else if ((flags & INDEX_CREATE_SKIP_BUILD) != 0)
 	{
+		bool update_stats = true;
+
+		/*
+		 * If autovacuum is disabled, don't implicitly update stats as a part
+		 * of index creation.
+		 */
+		if (!AutoVacuumingActive() ||
+			(heapRelation->rd_options != NULL &&
+			 !((StdRdOptions *) heapRelation->rd_options)->autovacuum.enabled))
+			update_stats = false;
+
 		/*
 		 * Caller is responsible for filling the index later on.  However,
 		 * we'd better make sure that the heap relation is correctly marked as
@@ -1268,6 +1281,7 @@ index_create(Relation heapRelation,
 		 */
 		index_update_stats(heapRelation,
 						   true,
+						   update_stats,
 						   -1.0);
 		/* Make the above update visible */
 		CommandCounterIncrement();
@@ -2807,9 +2821,9 @@ FormIndexDatum(IndexInfo *indexInfo,
 static void
 index_update_stats(Relation rel,
 				   bool hasindex,
+				   bool update_stats,
 				   double reltuples)
 {
-	bool		update_stats;
 	BlockNumber relpages = 0;	/* keep compiler quiet */
 	BlockNumber relallvisible = 0;
 	Oid			relid = RelationGetRelid(rel);
@@ -2837,7 +2851,8 @@ index_update_stats(Relation rel,
 	 * Don't update statistics during binary upgrade, because the indexes are
 	 * created before the data is moved into place.
 	 */
-	update_stats = reltuples >= 0 && !IsBinaryUpgrade;
+	if (reltuples < 0 || IsBinaryUpgrade)
+		update_stats = false;
 
 	/*
 	 * Finish I/O and visibility map buffer locks before
@@ -2981,6 +2996,7 @@ index_build(Relation heapRelation,
 	Oid			save_userid;
 	int			save_sec_context;
 	int			save_nestlevel;
+	bool		update_stats = true;
 
 	/*
 	 * sanity checks
@@ -3121,15 +3137,26 @@ index_build(Relation heapRelation,
 		table_close(pg_index, RowExclusiveLock);
 	}
 
+	/*
+	 * If autovacuum is disabled, don't implicitly update stats as a part
+	 * of index creation.
+	 */
+	if (!AutoVacuumingActive() ||
+		(heapRelation->rd_options != NULL &&
+		 !((StdRdOptions *) heapRelation->rd_options)->autovacuum.enabled))
+		update_stats = false;
+
 	/*
 	 * Update heap and index pg_class rows
 	 */
 	index_update_stats(heapRelation,
 					   true,
+					   update_stats,
 					   stats->heap_tuples);
 
 	index_update_stats(indexRelation,
 					   false,
+					   update_stats,
 					   stats->index_tuples);
 
 	/* Make the updated catalog row versions visible */
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index 1f150f7b08d..abd391181e4 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -12,15 +12,42 @@ CREATE TABLE stats_import.test(
     arange int4range,
     tags text[]
 ) WITH (autovacuum_enabled = false);
+SELECT
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'relpages', 18::integer,
+	'reltuples', 21::real,
+	'relallvisible', 24::integer);
+ pg_restore_relation_stats 
+---------------------------
+ t
+(1 row)
+
+-- CREATE INDEX on a table with autovac disabled should not overwrite
+-- stats
 CREATE INDEX test_i ON stats_import.test(id);
+SELECT
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.test_i'::regclass,
+        'relpages', 28::integer,
+	'reltuples', 35::real,
+	'relallvisible', 42::integer);
+ pg_restore_relation_stats 
+---------------------------
+ t
+(1 row)
+
 -- starting stats
-SELECT relpages, reltuples, relallvisible
+SELECT relname, relpages, reltuples, relallvisible
 FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
- relpages | reltuples | relallvisible 
-----------+-----------+---------------
-        0 |        -1 |             0
-(1 row)
+WHERE oid = 'stats_import.test'::regclass
+   OR oid = 'stats_import.test_i'::regclass
+ORDER BY relname;
+ relname | relpages | reltuples | relallvisible 
+---------+----------+-----------+---------------
+ test    |       18 |        21 |            24
+ test_i  |       28 |        35 |            42
+(2 rows)
 
 BEGIN;
 -- regular indexes have special case locking rules
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index 8c183bceb8a..f8907504de1 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -15,12 +15,30 @@ CREATE TABLE stats_import.test(
     tags text[]
 ) WITH (autovacuum_enabled = false);
 
+SELECT
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'relpages', 18::integer,
+	'reltuples', 21::real,
+	'relallvisible', 24::integer);
+
+-- CREATE INDEX on a table with autovac disabled should not overwrite
+-- stats
 CREATE INDEX test_i ON stats_import.test(id);
 
+SELECT
+    pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.test_i'::regclass,
+        'relpages', 28::integer,
+	'reltuples', 35::real,
+	'relallvisible', 42::integer);
+
 -- starting stats
-SELECT relpages, reltuples, relallvisible
+SELECT relname, relpages, reltuples, relallvisible
 FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
+WHERE oid = 'stats_import.test'::regclass
+   OR oid = 'stats_import.test_i'::regclass
+ORDER BY relname;
 
 BEGIN;
 -- regular indexes have special case locking rules
-- 
2.34.1

#402Jeff Davis
pgsql@j-davis.com
In reply to: Greg Sabino Mullane (#398)
Re: Statistics Import and Export

On Thu, 2025-02-27 at 22:42 -0500, Greg Sabino Mullane wrote:

I know I'm coming late to this, but I would like us to rethink having
statistics dumped by default. I was caught by this today, as I was
doing two dumps in a row, but the output changed between runs solely
because the stats got updated. It got me thinking about all the use
cases of pg_dump I've seen over the years. I think this has the
potential to cause a lot of problems for things like automated
scripts.

Can you expand on some of those cases?

There are some good reasons to make dumping stats the default:

* The argument here[1]/messages/by-id/3228677.1713844341@sss.pgh.pa.us seemed compelling: pg_dump has always dumped
everything by default, so not doing so for stats could be surprising.

* When dumping into the custom format, we'd almost certainly want to
include the stats so you can decide later whether to restore them or
not.

* For most of the cases I'm aware of, if you encounter a diff related
to stats, it would be obvious what the problem is and the fix would be
easy. I can imagine cases where it might not be easy, but I can't
recall any, so if you can then it would be helpful to list them.

so we will need to weigh the costs and benefits.

Unless there's a consensus to change it, I'm inclined to keep it the
default at least into beta, so that we can get feedback from users and
make a more informed decision.

(Aside: I assume everyone here agrees that pg_upgrade should transfer
the stats by default.)

Regards,
Jeff Davis

[1]: /messages/by-id/3228677.1713844341@sss.pgh.pa.us
/messages/by-id/3228677.1713844341@sss.pgh.pa.us

#403Nathan Bossart
nathandbossart@gmail.com
In reply to: Jeff Davis (#402)
Re: Statistics Import and Export

On Fri, Feb 28, 2025 at 12:54:03PM -0800, Jeff Davis wrote:

(Aside: I assume everyone here agrees that pg_upgrade should transfer
the stats by default.)

That feels like a safe assumption to me... I'm curious what the use-case
is for pg_upgrade's --no-statistics option.

--
nathan

#404Jeff Davis
pgsql@j-davis.com
In reply to: Nathan Bossart (#403)
Re: Statistics Import and Export

On Fri, 2025-02-28 at 14:56 -0600, Nathan Bossart wrote:

On Fri, Feb 28, 2025 at 12:54:03PM -0800, Jeff Davis wrote:

(Aside: I assume everyone here agrees that pg_upgrade should
transfer
the stats by default.)

That feels like a safe assumption to me...  I'm curious what the use-
case
is for pg_upgrade's --no-statistics option.

Mostly completeness and paranoia. I don't see a real use case. If we
decide we don't need it, that's fine with me.

Regards,
Jeff Davis

#405Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#404)
Re: Statistics Import and Export

On Fri, Feb 28, 2025 at 4:25 PM Jeff Davis <pgsql@j-davis.com> wrote:

On Fri, 2025-02-28 at 14:56 -0600, Nathan Bossart wrote:

On Fri, Feb 28, 2025 at 12:54:03PM -0800, Jeff Davis wrote:

(Aside: I assume everyone here agrees that pg_upgrade should
transfer
the stats by default.)

That feels like a safe assumption to me... I'm curious what the use-
case
is for pg_upgrade's --no-statistics option.

Mostly completeness and paranoia. I don't see a real use case. If we
decide we don't need it, that's fine with me.

Completeness/symmetry and paranoia was how I viewed it. I suppose it might
be useful in a failsafe if a pg_upgrade failed and you needed a way to
retry.

#406Nathan Bossart
nathandbossart@gmail.com
In reply to: Corey Huinker (#405)
Re: Statistics Import and Export

On Fri, Feb 28, 2025 at 04:52:02PM -0500, Corey Huinker wrote:

On Fri, Feb 28, 2025 at 4:25 PM Jeff Davis <pgsql@j-davis.com> wrote:

On Fri, 2025-02-28 at 14:56 -0600, Nathan Bossart wrote:

I'm curious what the use-case is for pg_upgrade's --no-statistics
option.

Mostly completeness and paranoia. I don't see a real use case. If we
decide we don't need it, that's fine with me.

Completeness/symmetry and paranoia was how I viewed it. I suppose it might
be useful in a failsafe if a pg_upgrade failed and you needed a way to
retry.

Got it. I have no strong opinion on the matter.

--
nathan

#407Alexander Lakhin
exclusion@gmail.com
In reply to: Jeff Davis (#369)
Re: Statistics Import and Export

Hello Jeff,

26.02.2025 04:00, Jeff Davis wrote:

I plan to commit the patches soon.

It looks like 8f427187d broke pg_dump on Cygwin:
https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=fairywren&amp;dt=2025-02-26%2010%3A03%3A07

As far as I can see, it exits prematurely here:
               float           reltuples = strtof(PQgetvalue(res, i, i_reltuples), NULL);

because of:
/*
 * Cygwin has a strtof() which is literally just (float)strtod(), which means
 * we get misrounding _and_ silent over/underflow. Using our wrapper doesn't
 * fix the misrounding but does fix the error checks, which cuts down on the
 * number of test variant files needed.
 */
#define HAVE_BUGGY_STRTOF 1
...
#ifdef HAVE_BUGGY_STRTOF
extern float pg_strtof(const char *nptr, char **endptr);
#define strtof(a,b) (pg_strtof((a),(b)))
#endif

and:
float
pg_strtof(const char *nptr, char **endptr)
{
...
    if (errno)
    {
        /* On error, just return the error to the caller. */
        return fresult;
    }
    else if ((*endptr == nptr) || isnan(fresult) ||
...

Best regards,
Alexander Lakhin
Neon (https://neon.tech)

#408Tom Lane
tgl@sss.pgh.pa.us
In reply to: Alexander Lakhin (#407)
Re: Statistics Import and Export

Alexander Lakhin <exclusion@gmail.com> writes:

It looks like 8f427187d broke pg_dump on Cygwin:
https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=fairywren&amp;dt=2025-02-26%2010%3A03%3A07

Yeah, Andrew and I have been puzzling over that off-list. pg_dump
is definitely exiting unceremoniously.

As far as I can see, it exits prematurely here:
               float           reltuples = strtof(PQgetvalue(res, i, i_reltuples), NULL);

I was suspecting those float conversions as a likely cause, but
what do you think is wrong exactly? I see nothing obviously
buggy in pg_strtof().

But I'm not sure it's worth running to ground. I don't love any of
the portability-related hacks that 8f427187d made: the setlocale()
call looks like something with an undesirably large blast radius,
and pg_dump has never made use of strtof or f2s.c before. Sure,
those *ought* to work, but they evidently don't work everywhere,
and I don't especially want to expend more brain cells figuring out
what's wrong here. I think we ought to cut our losses and store
reltuples in string form, as Corey wanted to do originally.

regards, tom lane

#409Alexander Lakhin
exclusion@gmail.com
In reply to: Tom Lane (#408)
Re: Statistics Import and Export

Hello Tom,

01.03.2025 20:04, Tom Lane wrote:

Alexander Lakhin <exclusion@gmail.com> writes:

It looks like 8f427187d broke pg_dump on Cygwin:
https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=fairywren&amp;dt=2025-02-26%2010%3A03%3A07

Yeah, Andrew and I have been puzzling over that off-list. pg_dump
is definitely exiting unceremoniously.

As far as I can see, it exits prematurely here:
               float           reltuples = strtof(PQgetvalue(res, i, i_reltuples), NULL);

I was suspecting those float conversions as a likely cause, but
what do you think is wrong exactly? I see nothing obviously
buggy in pg_strtof().

From my understanding, pg_strtof () can't stand against endptr == NULL.
I have changed that line to:
        char *tptr;
        float        reltuples = strtof(PQgetvalue(res, i, i_reltuples), &tptr);

and 002_compare_backups passed for me.

Best regards,
Alexander Lakhin
Neon (https://neon.tech)

#410Greg Sabino Mullane
htamfids@gmail.com
In reply to: Jeff Davis (#402)
Re: Statistics Import and Export

Can you expand on some of those cases?

Certainly. I think one of the problems is that because this patch is
solving a pg_upgrade issue, the focus is on the "dump and restore"
scenarios. But pg_dump is used for much more than that, especially "dump
and examine".

Although pg_dump is meant to be a canonical, logical representation of your
schema and data, the stats add a non-determinant element to that.
Statistical sampling is random, so pg_dump output changes with each run.
(yes, COPY can also change, but much less so, as I argue later).

One use case is a program that is simply using pg_dump to verify that
nothing has modified your table data (I'll use a single table for these
examples, but obviously this applies to a whole database as well). So let's
say we create a table and populate it at time X, then check back at a later
time to verify things are still exactly as we left them.

dropdb gregtest
createdb gregtest
pgbench gregtest -i 2> /dev/null
pg_dump gregtest -t pgbench_accounts > a1
sleep 10
pg_dump gregtest -t pgbench_accounts > a2
diff a1 a2 | cut -c1-50

100078c100078
< 'histogram_bounds', '{2,964,1921,2917,3892,4935
---

'histogram_bounds', '{7,989,1990,2969,3973,4977

While COPY is not going to promise a particular output order, the order
should not change except for manual things: insert, update, delete,
truncate, vacuum full, cluster (off the top of my head). What should not
change the output is a background process gathering some metadata. Or
someone running a database-wide ANALYZE.

Another use case is someone rolling out their schema to a QA box. All the
table definitions and data are checked into a git repository, with a
checksum. They want to roll it out, and then verify that everything is
exactly as they expect it to be. Or the program is part of a test suite
that does a sanity check that the database is in an exact known state
before starting.

(Our system catalogs are very difficult when reverse engineering objects.
Thus, many programs rely on pg_dump to do the heavy lifting for them.
Parsing the text file generated by pg_dump is much easier than trying to
manipulate the system catalogs.)

So let's say the process is to create a new database, load things into it,
and then checksum the result. We can simulate that with pg_bench:

dropdb qa1; dropdb qa2
createdb qa1; createdb qa2
pgbench qa1 -i 2>/dev/null
pgbench qa2 -i 2>/dev/null
pg_dump qa1 > dump1; pg_dump qa2 > dump2

$ md5sum dump1
39a2da5e51e8541e9a2c025c918bf463 dump1

This md5sum does not match our repo! It doesn't even match the other one:

$ md5sum dump2
4a977657dfdf910cb66c875d29cfebf2 dump2

It's the stats, or course, which has added a dose of randomness that was
not there before, and makes our checksums useless:

$ diff dump1 dump2 | cut -c1-50
100172c100172
< 'histogram_bounds', '{1,979,1974,2952,3973,4900
---

'histogram_bounds', '{8,1017,2054,3034,4045,513

With --no-statistics, the diff shows no difference, and the md5sum is
always the same.

Just to be clear, I love this patch, and I love the fact that one of our
major upgrade warts is finally getting fixed. I've tried fixing it myself a
few times over the last decade or so, but lacked the skills to do so. :) So
I am thrilled to have this finally done. I just don't think it should be
enabled by default for everything using pg_dump. For the record, I would
not strongly object to having stats on by default for binary dumps,
although I would prefer them off.

So why not just expect people to modify their programs to use
--no-statistics for cases like this? That's certainly an option, but it's
going to break a lot of existing things, and create branching code:

old code:
pg_dump mydb -f pg.dump

new code:
if pg_dump.version >= 18
pg_dump --no-statistics mydb -f pg.dump
else
pg_dump mydb -f pg.dump

Also, anything trained to parse pg_dump output will have to learn about the
new SELECT pg_restore_ calls with their multi-line formats (not 100% sure
we don't have that anywhere, as things like "SELECT setval" and "SELECT
set_config" are single line, but there may be existing things)

Cheers,
Greg

--
Crunchy Data - https://www.crunchydata.com
Enterprise Postgres Software Products & Tech Support

#411Tom Lane
tgl@sss.pgh.pa.us
In reply to: Alexander Lakhin (#409)
Re: Statistics Import and Export

Alexander Lakhin <exclusion@gmail.com> writes:

01.03.2025 20:04, Tom Lane wrote:

I was suspecting those float conversions as a likely cause, but
what do you think is wrong exactly? I see nothing obviously
buggy in pg_strtof().

From my understanding, pg_strtof () can't stand against endptr == NULL.

D'oh! I'm blind as a bat today.

I have changed that line to:
        char *tptr;
        float        reltuples = strtof(PQgetvalue(res, i, i_reltuples), &tptr);
and 002_compare_backups passed for me.

Cool, but surely the right fix is to make pg_strtof() adhere to
the POSIX specification, so we don't have to learn this lesson
again elsewhere. I'll go make it so.

Independently of that, do we want to switch over to storing
reltuples as a string instead of converting it? I still feel
uncomfortable about the amount of baggage we added to pg_dump
to avoid that.

regards, tom lane

#412Jeff Davis
pgsql@j-davis.com
In reply to: Greg Sabino Mullane (#410)
Re: Statistics Import and Export

On Sat, 2025-03-01 at 13:52 -0500, Greg Sabino Mullane wrote:

Can you expand on some of those cases?

Certainly. I think one of the problems is that because this patch is
solving a pg_upgrade issue, the focus is on the "dump and restore"
scenarios. But pg_dump is used for much more than that, especially
"dump and examine".

Thank you for going through these examples.

I just don't think it should be enabled by default for everything
using pg_dump. For the record, I would not strongly object to having
stats on by default for binary dumps, although I would prefer them
off.

I am open to that idea, I just want to get it right, because probably
whatever the default is in 18 will stay that way.

Also, we will need to think through the set of pg_dump options again. A
lot of our tools seem to assume that "if it's the default, we don't
need a way to ask for it explicitly", which makes it a lot harder to
ever change the default and keep a coherent set of options.

So why not just expect people to modify their programs to use --no-
statistics for cases like this? That's certainly an option, but it's
going to break a lot of existing things, and create branching code:

I suggest that we wait a bit to see what additional feedback we get
early in beta.

Also, anything trained to parse pg_dump output will have to learn
about the new SELECT pg_restore_ calls with their multi-line formats
(not 100% sure we don't have that anywhere, as things like "SELECT
setval" and "SELECT set_config" are single line, but there may be
existing things)

That's an interesting point. What tools are currrently trying to parse
pg_dump output?

Regards,
Jeff Davis

#413Tom Lane
tgl@sss.pgh.pa.us
In reply to: Jeff Davis (#412)
Re: Statistics Import and Export

Jeff Davis <pgsql@j-davis.com> writes:

On Sat, 2025-03-01 at 13:52 -0500, Greg Sabino Mullane wrote:

Also, anything trained to parse pg_dump output will have to learn
about the new SELECT pg_restore_ calls with their multi-line formats
(not 100% sure we don't have that anywhere, as things like "SELECT
setval" and "SELECT set_config" are single line, but there may be
existing things)

That's an interesting point. What tools are currrently trying to parse
pg_dump output?

That particular argument needs to be rejected vociferously. Otherwise
we could never make any change at all in what pg_dump emits. I think
the standard has to be "if you parse pg_dump output, it's on you to
cope with any legal SQL".

I do grasp Greg's larger point that this is a big change in pg_dump's
behavior and will certainly break some expectations. I kind of lean
to the position that we'll be sad in the long run if we don't change
the default, though. What other part of pg_dump's output is not
produced by default?

regards, tom lane

#414Corey Huinker
corey.huinker@gmail.com
In reply to: Tom Lane (#411)
Re: Statistics Import and Export

Independently of that, do we want to switch over to storing
reltuples as a string instead of converting it? I still feel
uncomfortable about the amount of baggage we added to pg_dump
to avoid that.

I'm obviously a 'yes' vote for string, either fixed width buffer or
pg_strdup'd, for the reduced complexity. I'm not dismissing concerns about
memory usage, and we could free the RelStatsInfo structure after use, but
we're already not freeing the parent structures tbinfo or indxinfo,
probably because they're needed right up til the end of the program, and
there's no subsequent consumer for the memory that we'd be freeing up.

#415Magnus Hagander
magnus@hagander.net
In reply to: Jeff Davis (#412)
Re: Statistics Import and Export

On Sat, Mar 1, 2025 at 9:48 PM Jeff Davis <pgsql@j-davis.com> wrote:

On Sat, 2025-03-01 at 13:52 -0500, Greg Sabino Mullane wrote:

Can you expand on some of those cases?

Certainly. I think one of the problems is that because this patch is
solving a pg_upgrade issue, the focus is on the "dump and restore"
scenarios. But pg_dump is used for much more than that, especially
"dump and examine".

Thank you for going through these examples.

I just don't think it should be enabled by default for everything
using pg_dump. For the record, I would not strongly object to having
stats on by default for binary dumps, although I would prefer them
off.

I am open to that idea, I just want to get it right, because probably
whatever the default is in 18 will stay that way.

Also, we will need to think through the set of pg_dump options again. A
lot of our tools seem to assume that "if it's the default, we don't
need a way to ask for it explicitly", which makes it a lot harder to
ever change the default and keep a coherent set of options.

That's a good point in general, and definitely something we should think
through, independently of his patch.

So why not just expect people to modify their programs to use --no-
statistics for cases like this? That's certainly an option, but it's
going to break a lot of existing things, and create branching code:

I suggest that we wait a bit to see what additional feedback we get
early in beta.

I definitely thing it should be on by default.

FWIW, I've seen many cases of people using automated tools to verify the
*schema* between two databases. I'd say that's quite common. But they use
pg_dump -s, which I believe is not affected by this one.

I don't think I've ever come across an automated tool to verify the
contents of an entire database this way. That doesn't mean it's not out
there of course, just that it's not so common. The cases I've seen pg_dump
used to verify the contents that's always been in combination with a myriad
of other switches such as include/exclude of specific tables etc, and
adding just one more switch to those seems like a small price to pay for
having the default behaviour be a big improvement for the majority of
usecases.

Also, anything trained to parse pg_dump output will have to learn

about the new SELECT pg_restore_ calls with their multi-line formats
(not 100% sure we don't have that anywhere, as things like "SELECT
setval" and "SELECT set_config" are single line, but there may be
existing things)

That's going to be true every time we add something to pg_dump. And for
that matter, anything new to *postgresql*, since surely we'd want pg_dump
to dump objects by default. Any tool that parses the pg_dump output
directly will always have to carefully analyze each new version. And
probably shouldn't be using the plaintext format in the first place - and
if using pg_restore it comes out as it's own type of object, making it easy
to exclude at that level.

--
Magnus Hagander
Me: https://www.hagander.net/ <http://www.hagander.net/&gt;
Work: https://www.redpill-linpro.com/ <http://www.redpill-linpro.com/&gt;

#416Corey Huinker
corey.huinker@gmail.com
In reply to: Magnus Hagander (#415)
Re: Statistics Import and Export

Also, we will need to think through the set of pg_dump options again. A

lot of our tools seem to assume that "if it's the default, we don't
need a way to ask for it explicitly", which makes it a lot harder to
ever change the default and keep a coherent set of options.

That's a good point in general, and definitely something we should think
through, independently of his patch.

I agree. There was a --with-statistics option in earlier patchsets, which
was effectively a no-op because statistics are the default, and it was
removed when its existence was questioned. I mention this only to say that
consensus for those options will have to be built.

FWIW, I've seen many cases of people using automated tools to verify the
*schema* between two databases. I'd say that's quite common. But they use
pg_dump -s, which I believe is not affected by this one.

Correct, -s behaves as before, as does --data-only. Schema, data, and
statistics are independent, each has their own -only flag, each each has
their own --no- flag.

If you were using --no-schema to mean data-only, or --no-data to mean
schema-only, then you'll have to add --no-statistics to that call, but I'd
argue that they already had a better option of getting what they wanted.

If you thought you saw major changes in the patchsets around those flags,
you weren't imagining it. There was a lot of internal logic that worked on
the assumptions like "If schema_only is false then we must want data" but
that's no longer strictly true, so we resolved all the user flags to
dumpSchema/dumpData/dumpStatistics at the very start, and now the internal
logic work is based on those affirmative flags rather than the bankshot
absence-of-the-opposite logic that was there before.

Show quoted text
#417Ashutosh Bapat
ashutosh.bapat.oss@gmail.com
In reply to: Jeff Davis (#401)
Re: Statistics Import and Export: difference in statistics dumped

On Sat, Mar 1, 2025 at 1:40 AM Jeff Davis <pgsql@j-davis.com> wrote:

On Fri, 2025-02-28 at 14:51 +0530, Ashutosh Bapat wrote:

2. We aren't restoring the statistics faithfully - as mentioned in
Greg's reply. If users dump and restore with autovacuum turned off,
they will be surprised to see the statistics to be different on the
original and restored database - which may have other effects like
change in plans.

Then let's just address that concern directly: disable updating stats
implicitly if autovacuum is off. If autovacuum is on, the user
shouldn't have an expectation of stable stats anyway. Patch attached.

The fact that statistics gets updated is not documented at least under
CREATE INDEX page. So at least users should not rely on that
behaviour. But while we have hold of reltuples wasting a chance to
update it in pg_class does not look right to me. Changing regular
behaviour for the sake of pg_dump/pg_restore doesn't seem right to me.
I think the solution should be on the pg_dump/restore side and not on
the server side.

--
Best Wishes,
Ashutosh Bapat

#418Greg Sabino Mullane
htamfids@gmail.com
In reply to: Tom Lane (#413)
Re: Statistics Import and Export

On Sat, Mar 1, 2025 at 4:23 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:

That particular argument needs to be rejected vociferously.

Okay, I will concede that part of my argument. And for the record, I've
written pg_dump output parsing programs many times over the years, and seen
others in the wild. It's not uncommon as some in this thread think.

What other part of pg_dump's output is not produced by default?

None, but that's kind of the point - this is a very special class of data
(so much so, that we've been arguing about where it fits in our usual
pre/data/post paradigm). So I don't think it's unreasonable that something
this unique (and non-deterministic) gets excluded by default. It's still
only a flag away if people require it.

Cheers,
Greg

--
Crunchy Data - https://www.crunchydata.com
Enterprise Postgres Software Products & Tech Support

#419Jeff Davis
pgsql@j-davis.com
In reply to: Ashutosh Bapat (#417)
Re: Statistics Import and Export: difference in statistics dumped

On Mon, 2025-03-03 at 22:04 +0530, Ashutosh Bapat wrote:

But while we have hold of reltuples wasting a chance to
update it in pg_class does not look right to me.

To me, autovacuum=off is a pretty clear signal that the user doesn't
want this kind of side-effect to happen. Am I missing something?

I think the solution should be on the pg_dump/restore side and not on
the server side.

What solution are you suggesting? The only one that comes to mind is
moving everything to SECTION_POST_DATA, which is possible, but it seems
like a big design change to satisfy a small detail.

Regards,
Jeff Davis

#420Ashutosh Bapat
ashutosh.bapat.oss@gmail.com
In reply to: Jeff Davis (#419)
Re: Statistics Import and Export: difference in statistics dumped

On Tue, Mar 4, 2025 at 6:25 AM Jeff Davis <pgsql@j-davis.com> wrote:

On Mon, 2025-03-03 at 22:04 +0530, Ashutosh Bapat wrote:

But while we have hold of reltuples wasting a chance to
update it in pg_class does not look right to me.

To me, autovacuum=off is a pretty clear signal that the user doesn't
want this kind of side-effect to happen. Am I missing something?

Documentation of autovacuum says "Controls whether the server should
run the autovacuum launcher daemon." It doesn't talk about updates
happening as a side-effect. With autovacuum there is an extra scan and
resources are consumed but with index creation all that cost is
already paid. I wouldn't compare those two.

The case with IsBinaryUpdate is straight, statistics is not updated
only when run in binary upgrade mode. If we could devise a way to not
update statistics only when the index is created as part of restoring
a dump, that will be easily acceptable. But I don't know

I think the solution should be on the pg_dump/restore side and not on
the server side.

What solution are you suggesting? The only one that comes to mind is
moving everything to SECTION_POST_DATA, which is possible, but it seems
like a big design change to satisfy a small detail.

We don't have to do that. We can manage it by making statistics of
index dependent upon the indexes on the table. As far as dump is
concerned, they are dependent since index creation rewrites the
statistics so we would like to add statistics after index creation.
For that we will need to track the statistics dumpable object in the
TableInfo. When adding indexes to TableInfo in getIndexes, we add
dependency between the index and the table statistics. The dependency
based sorting will automatically take care ordering statistics objects
after all the index objects and thus print it after all CREATE INDEX
commands. I have not tried to code this. Do you see any problems with
that?

--
Best Wishes,
Ashutosh Bapat

#421Jeff Davis
pgsql@j-davis.com
In reply to: Ashutosh Bapat (#420)
Re: Statistics Import and Export: difference in statistics dumped

On Tue, 2025-03-04 at 10:28 +0530, Ashutosh Bapat wrote:

What solution are you suggesting? The only one that comes to mind
is
moving everything to SECTION_POST_DATA, which is possible, but it
seems
like a big design change to satisfy a small detail.

We don't have to do that. We can manage it by making statistics of
index dependent upon the indexes on the table.

The index relstats are already dependent on the index definition. If
you have a simple database like:

CREATE TABLE t(i INT);
INSERT INTO t SELECT generate_series(1,10);
CREATE INDEX t_idx ON t (i);
ANALYZE;

and then you dump it, you get:

------- SECTION_PRE_DATA -------

CREATE TABLE public.t ...

------- SECTION_DATA -----------

COPY public.t (i) FROM stdin;
...
SELECT * FROM pg_catalog.pg_restore_relation_stats(
'version', '180000'::integer,
'relation', 'public.t'::regclass,
'relpages', '1'::integer,
'reltuples', '10'::real,
'relallvisible', '0'::integer
);
...

------- SECTION_POST_DATA ------

CREATE INDEX t_idx ON public.t USING btree (i);
SELECT * FROM pg_catalog.pg_restore_relation_stats(
'version', '180000'::integer,
'relation', 'public.t_idx'::regclass,
'relpages', '2'::integer,
'reltuples', '10'::real,
'relallvisible', '0'::integer
);

(section annotations added for clarity)

There is no problem with the index relstats, because they are already
dependent on the index definition, and will be restored after the
CREATE INDEX.

The issue is when the table's restored relstats are different from what
CREATE INDEX calculates, and then the CREATE INDEX overwrites the
table's just-restored relation stats. The easiest way to see this is
when restoring with --no-data, because CREATE INDEX will see an empty
table and overwrite the table's restored relstats with zeros.

If we view this issue as a dependency problem, then we'd have to make
the *table relstats* depend on the *index definition*. If a table has
any indexes, the relstats would need to go after the last index
definition, effectively moving most relstats to SECTION_POST_DATA. The
table's attribute stats would not be dependent on the index definition
(because CREATE INDEX doesn't touch those), so they could stay in
SECTION_DATA. And if the table doesn't have any indexes, then its
relstats could also stay in SECTION_DATA. But then we have a mess, so
we might as well just put all stats in SECTION_POST_DATA.

But I don't see it as a dependency problem. When I look at the above
SQL, it reads nicely to me and there's no obvious problem with it.

If we want stats to be stable, we need some kind of mode to tell the
server not to apply these kind of helpful optimizations, otherwise the
issue will resurface in some form no matter what we do with pg_dump. We
could invent a new mode, but autovacuum=off seems close enough to me.

Regards,
Jeff Davis

#422Corey Huinker
corey.huinker@gmail.com
In reply to: Greg Sabino Mullane (#418)
3 attachment(s)
Re: Statistics Import and Export

Attached are a couple updates that fell by the wayside and I'd like to
bring focus back to them, plus one potential change, and a recap of where
things stand.

0001 is a patch from Jian He [1]/messages/by-id/CACJufxFVq=tq9u1zrHWYSbMi1T07gS9Ff0LJScMco4HZmtZ1xw@mail.gmail.com which removes a logic deduplication and I
believe should be committed.
0002 is some attempt to cull the regression tests, eliminating extraneous
parameters where possible, and reorganizing the tests

0003 is a bit more experimental.

In the interest of reducing potential ERRORs raised by
pg_restore_relation_stats and pg_restore_attribute_stats within a restore
or upgrade, the possibility that we attempt to restore stats to a relation
that does not yet exist means that the 'foo.bar'::regclass call will fail
with an ERROR. If, however, we replace the relation regclass parameter with
text parameters schemaname and relname, we have the flexibility to catch
this particular scenario and turn it into a WARNING instead. Likewise, if
we change the attname parameter to text, we avoid those casts as well.

I'm seeking feedback on whether people think this is a positive change.

TODO

1. If the schemaname/relname change is amenable, there are some other
conditions where we currently raise errors but could instead issue a
WARNING instead and return false. We should settle on our preference
soon-ish.

2. Commit 99f8f3fbbc8f74 introduced relallfrozen to pg_class, and pg_dump
does not presently dump relallfrozen stats. I can implement this pending
the feedback on schemaname/relname vs relation regclass.

3. We still have a gap in functionality in that we do not currently dump
and restore extended stats. That patchset was recently updated and is
covered in thread [3]/messages/by-id/CADkLM=dnFKZMAo7MwqD2X6JjiiLCoVXHmszqtZp8sycYmoCcMQ@mail.gmail.com. I know time is getting short, but having this at the
same time would
reduce the number of customers who need to use the new vacuumdb option
under development in [4]/messages/by-id/CANWCAZZf_jNn2i+6-JfQt_j909DBk-U6Dg0M7iArZPLgdXCAmw@mail.gmail.com, and reduce customer confusion concerning whether
they are in need of post-upgrade analyzing of anything.

[1]: /messages/by-id/CACJufxFVq=tq9u1zrHWYSbMi1T07gS9Ff0LJScMco4HZmtZ1xw@mail.gmail.com
/messages/by-id/CACJufxFVq=tq9u1zrHWYSbMi1T07gS9Ff0LJScMco4HZmtZ1xw@mail.gmail.com
[2]: /messages/by-id/CAExHW5sNgxkqkyscm9KRrcwvi+_Hg=PRe_u+xZYJzX+w4XAMjQ@mail.gmail.com
/messages/by-id/CAExHW5sNgxkqkyscm9KRrcwvi+_Hg=PRe_u+xZYJzX+w4XAMjQ@mail.gmail.com
[3]: /messages/by-id/CADkLM=dnFKZMAo7MwqD2X6JjiiLCoVXHmszqtZp8sycYmoCcMQ@mail.gmail.com
/messages/by-id/CADkLM=dnFKZMAo7MwqD2X6JjiiLCoVXHmszqtZp8sycYmoCcMQ@mail.gmail.com
[4]: /messages/by-id/CANWCAZZf_jNn2i+6-JfQt_j909DBk-U6Dg0M7iArZPLgdXCAmw@mail.gmail.com
/messages/by-id/CANWCAZZf_jNn2i+6-JfQt_j909DBk-U6Dg0M7iArZPLgdXCAmw@mail.gmail.com

Attachments:

v5-0001-refactor-_tocEntryRequired-for-STATISTICS-DATA.patchtext/x-patch; charset=US-ASCII; name=v5-0001-refactor-_tocEntryRequired-for-STATISTICS-DATA.patchDownload
From 0ed0773d920f96e2f1d2e054a81f364d45d4826d Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Tue, 25 Feb 2025 14:13:24 +0800
Subject: [PATCH v5 1/3] refactor _tocEntryRequired for STATISTICS DATA.

we don't dump STATISTICS DATA in --section=pre-data.
so in _tocEntryRequired, we evaulate "(strcmp(te->desc, "STATISTICS DATA") == 0)"
after ``switch (curSection)``.
---
 src/bin/pg_dump/pg_backup_archiver.c | 21 +++++++++------------
 1 file changed, 9 insertions(+), 12 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 632077113a4..0fc4ba04ba4 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2932,14 +2932,6 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 		strcmp(te->desc, "SEARCHPATH") == 0)
 		return REQ_SPECIAL;
 
-	if (strcmp(te->desc, "STATISTICS DATA") == 0)
-	{
-		if (!ropt->dumpStatistics)
-			return 0;
-		else
-			res = REQ_STATS;
-	}
-
 	/*
 	 * DATABASE and DATABASE PROPERTIES also have a special rule: they are
 	 * restored in createDB mode, and not restored otherwise, independently of
@@ -2984,10 +2976,6 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
-	/* If it's statistics and we don't want statistics, maybe ignore it */
-	if (!ropt->dumpStatistics && strcmp(te->desc, "STATISTICS DATA") == 0)
-		return 0;
-
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -3008,6 +2996,15 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 			return 0;
 	}
 
+	/* If it's statistics and we don't want statistics, ignore it */
+	if (strcmp(te->desc, "STATISTICS DATA") == 0)
+	{
+		if (!ropt->dumpStatistics)
+			return 0;
+		else
+			return REQ_STATS;
+	}
+
 	/* Ignore it if rejected by idWanted[] (cf. SortTocFromFile) */
 	if (ropt->idWanted && !ropt->idWanted[te->dumpId - 1])
 		return 0;

base-commit: f4694e0f35b218238cbc87bcf8f8f5c6639bb1d4
-- 
2.48.1

v5-0002-Organize-and-deduplicate-statistics-import-tests.patchtext/x-patch; charset=US-ASCII; name=v5-0002-Organize-and-deduplicate-statistics-import-tests.patchDownload
From 4fb95c2dc726d666fe32a59595d5dc2478b80465 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 26 Feb 2025 21:02:44 -0500
Subject: [PATCH v5 2/3] Organize and deduplicate statistics import tests.

Many changes, refactorings, and rebasings have taken their toll on the
statistics import tests. Now that things appear more stable and the
pg_set_* functions are gone in favor of using pg_restore_* in all cases,
it's safe to remove duplicates, combine tests where possible, and make
the test descriptions a bit more descriptive and uniform.

Additionally, parameters that were not strictly needed to demonstrate
the purpose(s) of a test were removed to reduce clutter.
---
 src/test/regress/expected/stats_import.out | 681 ++++++++-------------
 src/test/regress/sql/stats_import.sql      | 555 ++++++-----------
 2 files changed, 454 insertions(+), 782 deletions(-)

diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index 4df287e547f..ebba14c6a1d 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -13,19 +13,48 @@ CREATE TABLE stats_import.test(
     tags text[]
 ) WITH (autovacuum_enabled = false);
 CREATE INDEX test_i ON stats_import.test(id);
+--
+-- relstats tests
+--
+--- error: relation is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 0::oid,
+        'relpages', 17::integer);
+WARNING:  argument "relation" has type "oid", expected type "regclass"
+ERROR:  "relation" cannot be NULL
+-- error: relation not found
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 0::oid::regclass,
+        'relpages', 17::integer);
+ERROR:  could not open relation with OID 0
+-- error: odd number of variadic arguments cannot be pairs
+SELECT pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'relallvisible');
+ERROR:  variadic arguments must be name/value pairs
+HINT:  Provide an even number of variadic arguments that can be divided into pairs.
+-- error: argument name is NULL
+SELECT pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        NULL, '17'::integer);
+ERROR:  name at variadic position 3 is NULL
+-- error: argument name is not a text type
+SELECT pg_restore_relation_stats(
+        'relation', '0'::oid::regclass,
+        17, '17'::integer);
+ERROR:  name at variadic position 3 has type "integer", expected type "text"
 -- starting stats
 SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
+WHERE oid = 'stats_import.test_i'::regclass;
  relpages | reltuples | relallvisible | relallfrozen 
 ----------+-----------+---------------+--------------
-        0 |        -1 |             0 |            0
+        1 |         0 |             0 |            0
 (1 row)
 
-BEGIN;
 -- regular indexes have special case locking rules
-SELECT
-    pg_catalog.pg_restore_relation_stats(
+BEGIN;
+SELECT pg_catalog.pg_restore_relation_stats(
         'relation', 'stats_import.test_i'::regclass,
         'relpages', 18::integer);
  pg_restore_relation_stats 
@@ -50,32 +79,6 @@ WHERE relation = 'stats_import.test_i'::regclass AND
 (1 row)
 
 COMMIT;
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.test_i'::regclass,
-        'relpages', 19::integer );
- pg_restore_relation_stats 
----------------------------
- t
-(1 row)
-
--- clear
-SELECT
-    pg_catalog.pg_clear_relation_stats(
-        'stats_import.test'::regclass);
- pg_clear_relation_stats 
--------------------------
- 
-(1 row)
-
-SELECT relpages, reltuples, relallvisible, relallfrozen
-FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
- relpages | reltuples | relallvisible | relallfrozen 
-----------+-----------+---------------+--------------
-        0 |        -1 |             0 |            0
-(1 row)
-
 --  relpages may be -1 for partitioned tables
 CREATE TABLE stats_import.part_parent ( i integer ) PARTITION BY RANGE(i);
 CREATE TABLE stats_import.part_child_1
@@ -92,26 +95,6 @@ WHERE oid = 'stats_import.part_parent'::regclass;
        -1
 (1 row)
 
--- although partitioned tables have no storage, setting relpages to a
--- positive value is still allowed
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent_i'::regclass,
-        'relpages', 2::integer);
- pg_restore_relation_stats 
----------------------------
- t
-(1 row)
-
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent'::regclass,
-        'relpages', 2::integer);
- pg_restore_relation_stats 
----------------------------
- t
-(1 row)
-
 --
 -- Partitioned indexes aren't analyzed but it is possible to set
 -- stats. The locking rules are different from normal indexes due to
@@ -119,8 +102,7 @@ SELECT
 -- partitioned index are locked in ShareUpdateExclusive mode.
 --
 BEGIN;
-SELECT
-    pg_catalog.pg_restore_relation_stats(
+SELECT pg_catalog.pg_restore_relation_stats(
         'relation', 'stats_import.part_parent_i'::regclass,
         'relpages', 2::integer);
  pg_restore_relation_stats 
@@ -145,30 +127,19 @@ WHERE relation = 'stats_import.part_parent_i'::regclass AND
 (1 row)
 
 COMMIT;
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent_i'::regclass,
-        'relpages', 2::integer);
- pg_restore_relation_stats 
----------------------------
- t
+SELECT relpages
+FROM pg_class
+WHERE oid = 'stats_import.part_parent_i'::regclass;
+ relpages 
+----------
+        2
 (1 row)
 
--- nothing stops us from setting it to -1
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent'::regclass,
-        'relpages', -1::integer);
- pg_restore_relation_stats 
----------------------------
- t
-(1 row)
-
--- ok: set all stats
+-- ok: set all relstats, with version, no bounds checking
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
         'version', 150000::integer,
-        'relpages', '17'::integer,
+        'relpages', '-17'::integer,
         'reltuples', 400::real,
         'relallvisible', 4::integer,
         'relallfrozen', 2::integer);
@@ -182,13 +153,12 @@ FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
  relpages | reltuples | relallvisible | relallfrozen 
 ----------+-----------+---------------+--------------
-       17 |       400 |             4 |            2
+      -17 |       400 |             4 |            2
 (1 row)
 
--- ok: just relpages
+-- ok: set just relpages, rest stay same
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
-        'version', 150000::integer,
         'relpages', '16'::integer);
  pg_restore_relation_stats 
 ---------------------------
@@ -203,10 +173,9 @@ WHERE oid = 'stats_import.test'::regclass;
        16 |       400 |             4 |            2
 (1 row)
 
--- ok: just reltuples
+-- ok: set just reltuples, rest stay same
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
-        'version', 150000::integer,
         'reltuples', '500'::real);
  pg_restore_relation_stats 
 ---------------------------
@@ -221,10 +190,9 @@ WHERE oid = 'stats_import.test'::regclass;
        16 |       500 |             4 |            2
 (1 row)
 
--- ok: just relallvisible
+-- ok: set just relallvisible, rest stay same
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
-        'version', 150000::integer,
         'relallvisible', 5::integer);
  pg_restore_relation_stats 
 ---------------------------
@@ -257,10 +225,9 @@ WHERE oid = 'stats_import.test'::regclass;
        16 |       500 |             5 |            3
 (1 row)
 
--- warn: bad relpages type
+-- warn: bad relpages type, rest updated
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
-        'version', 150000::integer,
         'relpages', 'nope'::text,
         'reltuples', 400.0::real,
         'relallvisible', 4::integer,
@@ -279,20 +246,122 @@ WHERE oid = 'stats_import.test'::regclass;
        16 |       400 |             4 |            3
 (1 row)
 
+-- unrecognized argument name, rest ok
+SELECT pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'relpages', '171'::integer,
+        'nope', 10::integer);
+WARNING:  unrecognized argument name: "nope"
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+      171 |       400 |             4
+(1 row)
+
+-- ok: clear stats
+SELECT pg_catalog.pg_clear_relation_stats(
+    relation => 'stats_import.test'::regclass);
+ pg_clear_relation_stats 
+-------------------------
+ 
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+        0 |        -1 |             0
+(1 row)
+
 -- invalid relkinds for statistics
 CREATE SEQUENCE stats_import.testseq;
-CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
-SELECT
-    pg_catalog.pg_clear_relation_stats(
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.testseq'::regclass);
+ERROR:  cannot modify statistics for relation "testseq"
+DETAIL:  This operation is not supported for sequences.
+SELECT pg_catalog.pg_clear_relation_stats(
         'stats_import.testseq'::regclass);
 ERROR:  cannot modify statistics for relation "testseq"
 DETAIL:  This operation is not supported for sequences.
-SELECT
-    pg_catalog.pg_clear_relation_stats(
+CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.testview'::regclass);
+ERROR:  cannot modify statistics for relation "testview"
+DETAIL:  This operation is not supported for views.
+SELECT pg_catalog.pg_clear_relation_stats(
         'stats_import.testview'::regclass);
 ERROR:  cannot modify statistics for relation "testview"
 DETAIL:  This operation is not supported for views.
--- ok: no stakinds
+--
+-- attribute stats
+--
+-- error: object does not exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', '0'::oid::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+ERROR:  could not open relation with OID 0
+-- error: relation null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', NULL::oid::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+ERROR:  "relation" cannot be NULL
+-- error: NULL attname
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', NULL::name,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+ERROR:  must specify either attname or attnum
+-- error: attname doesn't exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'nope'::name,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ERROR:  column "nope" of relation "test" does not exist
+-- error: both attname and attnum
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'attnum', 1::smallint,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+ERROR:  cannot specify both attname and attnum
+-- error: neither attname nor attnum
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+ERROR:  must specify either attname or attnum
+-- error: attribute is system column
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'xmin'::name,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+ERROR:  cannot modify statistics on system column "xmin"
+-- error: inherited null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', NULL::boolean,
+    'null_frac', 0.1::real);
+ERROR:  "inherited" cannot be NULL
+-- ok: just the fixed values, with version, no stakinds
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
@@ -317,15 +386,16 @@ AND attname = 'id';
  stats_import | test      | id      | f         |       0.2 |         5 |        0.6 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- ok: restore by attnum
+--
+-- ok: restore by attnum, we normally reserve this for
+-- indexes, but there is no reason it shouldn't work
+-- for any stat-having relation.
+--
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attnum', 1::smallint,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.4::real,
-    'avg_width', 5::integer,
-    'n_distinct', 0.6::real);
+    'null_frac', 0.4::real);
  pg_restore_attribute_stats 
 ----------------------------
  t
@@ -342,14 +412,12 @@ AND attname = 'id';
  stats_import | test      | id      | f         |       0.4 |         5 |        0.6 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: unrecognized argument name
+-- warn: unrecognized argument name, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
     'null_frac', 0.2::real,
-    'avg_width', NULL::integer,
     'nope', 0.5::real);
 WARNING:  unrecognized argument name: "nope"
  pg_restore_attribute_stats 
@@ -368,15 +436,12 @@ AND attname = 'id';
  stats_import | test      | id      | f         |       0.2 |         5 |        0.6 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: mcv / mcf null mismatch part 1
+-- warn: mcv / mcf null mismatch part 1, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.6::real,
-    'avg_width', 7::integer,
-    'n_distinct', -0.7::real,
+    'null_frac', 0.21::real,
     'most_common_freqs', '{0.1,0.2,0.3}'::real[]
     );
 WARNING:  "most_common_vals" must be specified when "most_common_freqs" is specified
@@ -393,18 +458,15 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.6 |         7 |       -0.7 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.21 |         5 |        0.6 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: mcv / mcf null mismatch part 2
+-- warn: mcv / mcf null mismatch part 2, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.7::real,
-    'avg_width', 8::integer,
-    'n_distinct', -0.8::real,
+    'null_frac', 0.21::real,
     'most_common_vals', '{1,2,3}'::text
     );
 WARNING:  "most_common_freqs" must be specified when "most_common_vals" is specified
@@ -421,18 +483,15 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.7 |         8 |       -0.8 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.21 |         5 |        0.6 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: mcv / mcf type mismatch
+-- warn: mcf type mismatch, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.8::real,
-    'avg_width', 9::integer,
-    'n_distinct', -0.9::real,
+    'null_frac', 0.22::real,
     'most_common_vals', '{2,1,3}'::text,
     'most_common_freqs', '{0.2,0.1}'::double precision[]
     );
@@ -451,18 +510,15 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.8 |         9 |       -0.9 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.22 |         5 |        0.6 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: mcv cast failure
+-- warn: mcv cast failure, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.9::real,
-    'avg_width', 10::integer,
-    'n_distinct', -0.4::real,
+    'null_frac', 0.23::real,
     'most_common_vals', '{2,four,3}'::text,
     'most_common_freqs', '{0.3,0.25,0.05}'::real[]
     );
@@ -480,7 +536,7 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.9 |        10 |       -0.4 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.23 |         5 |        0.6 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
 -- ok: mcv+mcf
@@ -488,10 +544,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 1::integer,
-    'n_distinct', -0.1::real,
     'most_common_vals', '{2,1,3}'::text,
     'most_common_freqs', '{0.3,0.25,0.05}'::real[]
     );
@@ -508,18 +560,15 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.1 |         1 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.23 |         5 |        0.6 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: NULL in histogram array
+-- warn: NULL in histogram array, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.2::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.2::real,
+    'null_frac', 0.24::real,
     'histogram_bounds', '{1,NULL,3,4}'::text
     );
 WARNING:  "histogram_bounds" array cannot contain NULL values
@@ -536,7 +585,7 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.2 |         2 |       -0.2 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.24 |         5 |        0.6 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
 -- ok: histogram_bounds
@@ -544,11 +593,8 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.3::real,
-    'avg_width', 3::integer,
-    'n_distinct', -0.3::real,
-    'histogram_bounds', '{1,2,3,4}'::text );
+    'histogram_bounds', '{1,2,3,4}'::text
+    );
  pg_restore_attribute_stats 
 ----------------------------
  t
@@ -562,19 +608,16 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.3 |         3 |       -0.3 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.24 |         5 |        0.6 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: elem_count_histogram null element
+-- warn: elem_count_histogram null element, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.4::real,
-    'avg_width', 5::integer,
-    'n_distinct', -0.4::real,
-    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    'null_frac', 0.25::real,
+    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1}'::real[]
     );
 WARNING:  "elem_count_histogram" array cannot contain NULL values
  pg_restore_attribute_stats 
@@ -590,7 +633,7 @@ AND inherited = false
 AND attname = 'tags';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | tags    | f         |       0.4 |         5 |       -0.4 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | tags    | f         |      0.25 |         0 |          0 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
 -- ok: elem_count_histogram
@@ -598,10 +641,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.5::real,
-    'avg_width', 6::integer,
-    'n_distinct', -0.55::real,
+    'null_frac', 0.26::real,
     'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
     );
  pg_restore_attribute_stats 
@@ -617,18 +657,15 @@ AND inherited = false
 AND attname = 'tags';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
- stats_import | test      | tags    | f         |       0.5 |         6 |      -0.55 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+ stats_import | test      | tags    | f         |      0.26 |         0 |          0 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
 (1 row)
 
--- range stats on a scalar type
+-- warn: range stats on a scalar type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.6::real,
-    'avg_width', 7::integer,
-    'n_distinct', -0.15::real,
+    'null_frac', 0.27::real,
     'range_empty_frac', 0.5::real,
     'range_length_histogram', '{399,499,Infinity}'::text
     );
@@ -647,18 +684,15 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.6 |         7 |      -0.15 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.27 |         5 |        0.6 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: range_empty_frac range_length_hist null mismatch
+-- warn: range_empty_frac range_length_hist null mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.7::real,
-    'avg_width', 8::integer,
-    'n_distinct', -0.25::real,
+    'null_frac', 0.28::real,
     'range_length_histogram', '{399,499,Infinity}'::text
     );
 WARNING:  "range_empty_frac" must be specified when "range_length_histogram" is specified
@@ -675,18 +709,15 @@ AND inherited = false
 AND attname = 'arange';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | arange  | f         |       0.7 |         8 |      -0.25 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | arange  | f         |      0.28 |         0 |          0 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: range_empty_frac range_length_hist null mismatch part 2
+-- warn: range_empty_frac range_length_hist null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.8::real,
-    'avg_width', 9::integer,
-    'n_distinct', -0.35::real,
+    'null_frac', 0.29::real,
     'range_empty_frac', 0.5::real
     );
 WARNING:  "range_length_histogram" must be specified when "range_empty_frac" is specified
@@ -703,7 +734,7 @@ AND inherited = false
 AND attname = 'arange';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | arange  | f         |       0.8 |         9 |      -0.35 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | arange  | f         |      0.29 |         0 |          0 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
 -- ok: range_empty_frac + range_length_hist
@@ -711,10 +742,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.9::real,
-    'avg_width', 1::integer,
-    'n_distinct', -0.19::real,
     'range_empty_frac', 0.5::real,
     'range_length_histogram', '{399,499,Infinity}'::text
     );
@@ -731,18 +758,15 @@ AND inherited = false
 AND attname = 'arange';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | arange  | f         |       0.9 |         1 |      -0.19 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
+ stats_import | test      | arange  | f         |      0.29 |         0 |          0 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
 (1 row)
 
--- warn: range bounds histogram on scalar
+-- warn: range bounds histogram on scalar, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.29::real,
+    'null_frac', 0.31::real,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
     );
 WARNING:  attribute "id" is not a range type
@@ -760,7 +784,7 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.1 |         2 |      -0.29 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.31 |         5 |        0.6 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
 (1 row)
 
 -- ok: range_bounds_histogram
@@ -768,10 +792,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.2::real,
-    'avg_width', 3::integer,
-    'n_distinct', -0.39::real,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
     );
  pg_restore_attribute_stats 
@@ -787,26 +807,17 @@ AND inherited = false
 AND attname = 'arange';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
- stats_import | test      | arange  | f         |       0.2 |         3 |      -0.39 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+ stats_import | test      | arange  | f         |      0.29 |         0 |          0 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
 (1 row)
 
--- warn: cannot set most_common_elems for range type
+-- warn: cannot set most_common_elems for range type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
-    'most_common_vals', '{"[2,3)","[1,2)","[3,4)"}'::text,
-    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
-    'histogram_bounds', '{"[1,2)","[2,3)","[3,4)","[4,5)"}'::text,
-    'correlation', 1.1::real,
+    'null_frac', 0.32::real,
     'most_common_elems', '{3,1}'::text,
-    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
-    'range_empty_frac', -0.5::real,
-    'range_length_histogram', '{399,499,Infinity}'::text,
-    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
     );
 WARNING:  unable to determine element type of attribute "arange"
 DETAIL:  Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.
@@ -821,19 +832,17 @@ WHERE schemaname = 'stats_import'
 AND tablename = 'test'
 AND inherited = false
 AND attname = 'arange';
-  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct |     most_common_vals      | most_common_freqs |         histogram_bounds          | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
---------------+-----------+---------+-----------+-----------+-----------+------------+---------------------------+-------------------+-----------------------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
- stats_import | test      | arange  | f         |       0.5 |         2 |       -0.1 | {"[2,3)","[1,2)","[3,4)"} | {0.3,0.25,0.05}   | {"[1,2)","[2,3)","[3,4)","[4,5)"} |         1.1 |                   |                        |                      | {399,499,Infinity}     |             -0.5 | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
+ stats_import | test      | arange  | f         |      0.32 |         0 |          0 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
 (1 row)
 
--- warn: scalars can't have mcelem
+-- warn: scalars can't have mcelem, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
+    'null_frac', 0.33::real,
     'most_common_elems', '{1,3}'::text,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
     );
@@ -852,17 +861,15 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.33 |         5 |        0.6 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: mcelem / mcelem mismatch
+-- warn: mcelem / mcelem mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
+    'null_frac', 0.34::real,
     'most_common_elems', '{one,two}'::text
     );
 WARNING:  "most_common_elem_freqs" must be specified when "most_common_elems" is specified
@@ -879,17 +886,15 @@ AND inherited = false
 AND attname = 'tags';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
- stats_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+ stats_import | test      | tags    | f         |      0.34 |         0 |          0 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
 (1 row)
 
--- warn: mcelem / mcelem null mismatch part 2
+-- warn: mcelem / mcelem null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
+    'null_frac', 0.35::real,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
     );
 WARNING:  "most_common_elems" must be specified when "most_common_elem_freqs" is specified
@@ -898,14 +903,22 @@ WARNING:  "most_common_elems" must be specified when "most_common_elem_freqs" is
  f
 (1 row)
 
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
+ stats_import | test      | tags    | f         |      0.35 |         0 |          0 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+(1 row)
+
 -- ok: mcelem
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
     'most_common_elems', '{one,three}'::text,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
     );
@@ -922,18 +935,16 @@ AND inherited = false
 AND attname = 'tags';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
- stats_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             | {one,three}       | {0.3,0.2,0.2,0.3,0}    | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+ stats_import | test      | tags    | f         |      0.35 |         0 |          0 |                  |                   |                  |             | {one,three}       | {0.3,0.2,0.2,0.3,0}    | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
 (1 row)
 
--- warn: scalars can't have elem_count_histogram
+-- warn: scalars can't have elem_count_histogram, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
-    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    'null_frac', 0.36::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1}'::real[]
     );
 WARNING:  unable to determine element type of attribute "id"
 DETAIL:  Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.
@@ -950,43 +961,7 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
-(1 row)
-
--- warn: too many stat kinds
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
-    'most_common_vals', '{"[2,3)","[1,3)","[3,9)"}'::text,
-    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
-    'histogram_bounds', '{"[1,2)","[2,3)","[3,4)","[4,)"}'::text,
-    'correlation', 1.1::real,
-    'most_common_elems', '{3,1}'::text,
-    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
-    'range_empty_frac', -0.5::real,
-    'range_length_histogram', '{399,499,Infinity}'::text,
-    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text);
-WARNING:  unable to determine element type of attribute "arange"
-DETAIL:  Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.
- pg_restore_attribute_stats 
-----------------------------
- f
-(1 row)
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'arange';
-  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct |     most_common_vals      | most_common_freqs |         histogram_bounds         | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
---------------+-----------+---------+-----------+-----------+-----------+------------+---------------------------+-------------------+----------------------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
- stats_import | test      | arange  | f         |       0.5 |         2 |       -0.1 | {"[2,3)","[1,3)","[3,9)"} | {0.3,0.25,0.05}   | {"[1,2)","[2,3)","[3,4)","[4,)"} |         1.1 |                   |                        |                      | {399,499,Infinity}     |             -0.5 | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+ stats_import | test      | id      | f         |      0.36 |         5 |        0.6 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
 (1 row)
 
 --
@@ -1006,20 +981,6 @@ SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import.complex_type,
 UNION ALL
 SELECT 4, 'four', NULL, int4range(0,100), NULL;
 CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
--- restoring stats on index
-SELECT * FROM pg_catalog.pg_restore_relation_stats(
-    'relation', 'stats_import.is_odd'::regclass,
-    'version', '180000'::integer,
-    'relpages', '11'::integer,
-    'reltuples', '10000'::real,
-    'relallvisible', '0'::integer,
-    'relallfrozen', '0'::integer
-);
- pg_restore_relation_stats 
----------------------------
- t
-(1 row)
-
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 CREATE TABLE stats_import.test_clone ( LIKE stats_import.test )
@@ -1197,7 +1158,18 @@ WHERE s.starelid = 'stats_import.is_odd'::regclass;
 ---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
 (0 rows)
 
--- ok
+-- attribute stats exist before a clear, but not after
+SELECT COUNT(*)
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+ count 
+-------
+     1
+(1 row)
+
 SELECT pg_catalog.pg_clear_attribute_stats(
     relation => 'stats_import.test'::regclass,
     attname => 'arange'::name,
@@ -1207,160 +1179,17 @@ SELECT pg_catalog.pg_clear_attribute_stats(
  
 (1 row)
 
---
--- Negative tests
---
---- error: relation is wrong type
-SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::oid,
-        'relpages', 17::integer,
-        'reltuples', 400.0::real,
-        'relallvisible', 4::integer,
-        'relallfrozen', 3::integer);
-WARNING:  argument "relation" has type "oid", expected type "regclass"
-ERROR:  "relation" cannot be NULL
---- error: relation not found
-SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::regclass,
-        'relpages', 17::integer,
-        'reltuples', 400.0::real,
-        'relallvisible', 4::integer,
-        'relallfrozen', 3::integer);
-ERROR:  could not open relation with OID 0
--- warn and error: unrecognized argument name
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        'relpages', '17'::integer,
-        'reltuples', 400::real,
-        'nope', 4::integer);
-WARNING:  unrecognized argument name: "nope"
-ERROR:  could not open relation with OID 0
--- error: argument name is NULL
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        NULL, '17'::integer,
-        'reltuples', 400::real,
-        'relallvisible', 4::integer,
-        'relallfrozen', 3::integer);
-ERROR:  name at variadic position 5 is NULL
--- error: argument name is an integer
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        17, '17'::integer,
-        'reltuples', 400::real,
-        'relallvisible', 4::integer,
-        'relallfrozen', 3::integer);
-ERROR:  name at variadic position 5 has type "integer", expected type "text"
--- error: odd number of variadic arguments cannot be pairs
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        'relpages', '17'::integer,
-        'reltuples', 400::real,
-        'relallfrozen', 3::integer,
-        'relallvisible');
-ERROR:  variadic arguments must be name/value pairs
-HINT:  Provide an even number of variadic arguments that can be divided into pairs.
--- error: object doesn't exist
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        'relpages', '17'::integer,
-        'reltuples', 400::real,
-        'relallvisible', 4::integer,
-        'relallfrozen', 3::integer);
-ERROR:  could not open relation with OID 0
--- error: object does not exist
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', '0'::oid::regclass,
-    'attname', 'id'::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-ERROR:  could not open relation with OID 0
--- error: relation null
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', NULL::oid,
-    'attname', 'id'::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-ERROR:  "relation" cannot be NULL
--- error: missing attname
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', NULL::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-ERROR:  must specify either attname or attnum
--- error: both attname and attnum
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
-    'attnum', 1::smallint,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-ERROR:  cannot specify both attname and attnum
--- error: attname doesn't exist
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'nope'::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-ERROR:  column "nope" of relation "test" does not exist
--- error: attribute is system column
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'xmin'::name,
-    'inherited', false::boolean,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-ERROR:  cannot modify statistics on system column "xmin"
--- error: inherited null
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
-    'inherited', NULL::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-ERROR:  "inherited" cannot be NULL
--- error: relation not found
-SELECT pg_catalog.pg_clear_relation_stats(
-    relation => 'stats_import.nope'::regclass);
-ERROR:  relation "stats_import.nope" does not exist
-LINE 2:     relation => 'stats_import.nope'::regclass);
-                        ^
--- error: attribute is system column
-SELECT pg_catalog.pg_clear_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'ctid'::name,
-    inherited => false::boolean);
-ERROR:  cannot clear statistics on system column "ctid"
--- error: attname doesn't exist
-SELECT pg_catalog.pg_clear_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'nope'::name,
-    inherited => false::boolean);
-ERROR:  column "nope" of relation "test" does not exist
+SELECT COUNT(*)
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+ count 
+-------
+     0
+(1 row)
+
 DROP SCHEMA stats_import CASCADE;
 NOTICE:  drop cascades to 6 other objects
 DETAIL:  drop cascades to type stats_import.complex_type
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index febda3d18d9..8d04ff4f378 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -17,15 +17,43 @@ CREATE TABLE stats_import.test(
 
 CREATE INDEX test_i ON stats_import.test(id);
 
+--
+-- relstats tests
+--
+
+--- error: relation is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 0::oid,
+        'relpages', 17::integer);
+
+-- error: relation not found
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 0::oid::regclass,
+        'relpages', 17::integer);
+
+-- error: odd number of variadic arguments cannot be pairs
+SELECT pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'relallvisible');
+
+-- error: argument name is NULL
+SELECT pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        NULL, '17'::integer);
+
+-- error: argument name is not a text type
+SELECT pg_restore_relation_stats(
+        'relation', '0'::oid::regclass,
+        17, '17'::integer);
+
 -- starting stats
 SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
+WHERE oid = 'stats_import.test_i'::regclass;
 
-BEGIN;
 -- regular indexes have special case locking rules
-SELECT
-    pg_catalog.pg_restore_relation_stats(
+BEGIN;
+SELECT pg_catalog.pg_restore_relation_stats(
         'relation', 'stats_import.test_i'::regclass,
         'relpages', 18::integer);
 
@@ -39,20 +67,6 @@ WHERE relation = 'stats_import.test_i'::regclass AND
 
 COMMIT;
 
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.test_i'::regclass,
-        'relpages', 19::integer );
-
--- clear
-SELECT
-    pg_catalog.pg_clear_relation_stats(
-        'stats_import.test'::regclass);
-
-SELECT relpages, reltuples, relallvisible, relallfrozen
-FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
-
 --  relpages may be -1 for partitioned tables
 CREATE TABLE stats_import.part_parent ( i integer ) PARTITION BY RANGE(i);
 CREATE TABLE stats_import.part_child_1
@@ -68,18 +82,6 @@ SELECT relpages
 FROM pg_class
 WHERE oid = 'stats_import.part_parent'::regclass;
 
--- although partitioned tables have no storage, setting relpages to a
--- positive value is still allowed
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent_i'::regclass,
-        'relpages', 2::integer);
-
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent'::regclass,
-        'relpages', 2::integer);
-
 --
 -- Partitioned indexes aren't analyzed but it is possible to set
 -- stats. The locking rules are different from normal indexes due to
@@ -88,8 +90,7 @@ SELECT
 --
 BEGIN;
 
-SELECT
-    pg_catalog.pg_restore_relation_stats(
+SELECT pg_catalog.pg_restore_relation_stats(
         'relation', 'stats_import.part_parent_i'::regclass,
         'relpages', 2::integer);
 
@@ -103,22 +104,15 @@ WHERE relation = 'stats_import.part_parent_i'::regclass AND
 
 COMMIT;
 
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent_i'::regclass,
-        'relpages', 2::integer);
+SELECT relpages
+FROM pg_class
+WHERE oid = 'stats_import.part_parent_i'::regclass;
 
--- nothing stops us from setting it to -1
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent'::regclass,
-        'relpages', -1::integer);
-
--- ok: set all stats
+-- ok: set all relstats, with version, no bounds checking
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
         'version', 150000::integer,
-        'relpages', '17'::integer,
+        'relpages', '-17'::integer,
         'reltuples', 400::real,
         'relallvisible', 4::integer,
         'relallfrozen', 2::integer);
@@ -127,30 +121,27 @@ SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
--- ok: just relpages
+-- ok: set just relpages, rest stay same
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
-        'version', 150000::integer,
         'relpages', '16'::integer);
 
 SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
--- ok: just reltuples
+-- ok: set just reltuples, rest stay same
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
-        'version', 150000::integer,
         'reltuples', '500'::real);
 
 SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
--- ok: just relallvisible
+-- ok: set just relallvisible, rest stay same
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
-        'version', 150000::integer,
         'relallvisible', 5::integer);
 
 SELECT relpages, reltuples, relallvisible, relallfrozen
@@ -167,10 +158,9 @@ SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
--- warn: bad relpages type
+-- warn: bad relpages type, rest updated
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
-        'version', 150000::integer,
         'relpages', 'nope'::text,
         'reltuples', 400.0::real,
         'relallvisible', 4::integer,
@@ -180,17 +170,104 @@ SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
+-- unrecognized argument name, rest ok
+SELECT pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'relpages', '171'::integer,
+        'nope', 10::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+-- ok: clear stats
+SELECT pg_catalog.pg_clear_relation_stats(
+    relation => 'stats_import.test'::regclass);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
 -- invalid relkinds for statistics
 CREATE SEQUENCE stats_import.testseq;
-CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
-SELECT
-    pg_catalog.pg_clear_relation_stats(
+
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.testseq'::regclass);
+
+SELECT pg_catalog.pg_clear_relation_stats(
         'stats_import.testseq'::regclass);
-SELECT
-    pg_catalog.pg_clear_relation_stats(
+
+CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
+
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.testview'::regclass);
+
+SELECT pg_catalog.pg_clear_relation_stats(
         'stats_import.testview'::regclass);
 
--- ok: no stakinds
+--
+-- attribute stats
+--
+
+-- error: object does not exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', '0'::oid::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: relation null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', NULL::oid::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: NULL attname
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', NULL::name,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: attname doesn't exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'nope'::name,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: both attname and attnum
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'attnum', 1::smallint,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: neither attname nor attnum
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: attribute is system column
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'xmin'::name,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: inherited null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', NULL::boolean,
+    'null_frac', 0.1::real);
+
+-- ok: just the fixed values, with version, no stakinds
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
@@ -207,15 +284,16 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- ok: restore by attnum
+--
+-- ok: restore by attnum, we normally reserve this for
+-- indexes, but there is no reason it shouldn't work
+-- for any stat-having relation.
+--
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attnum', 1::smallint,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.4::real,
-    'avg_width', 5::integer,
-    'n_distinct', 0.6::real);
+    'null_frac', 0.4::real);
 
 SELECT *
 FROM pg_stats
@@ -224,14 +302,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: unrecognized argument name
+-- warn: unrecognized argument name, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
     'null_frac', 0.2::real,
-    'avg_width', NULL::integer,
     'nope', 0.5::real);
 
 SELECT *
@@ -241,15 +317,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: mcv / mcf null mismatch part 1
+-- warn: mcv / mcf null mismatch part 1, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.6::real,
-    'avg_width', 7::integer,
-    'n_distinct', -0.7::real,
+    'null_frac', 0.21::real,
     'most_common_freqs', '{0.1,0.2,0.3}'::real[]
     );
 
@@ -260,15 +333,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: mcv / mcf null mismatch part 2
+-- warn: mcv / mcf null mismatch part 2, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.7::real,
-    'avg_width', 8::integer,
-    'n_distinct', -0.8::real,
+    'null_frac', 0.21::real,
     'most_common_vals', '{1,2,3}'::text
     );
 
@@ -279,15 +349,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: mcv / mcf type mismatch
+-- warn: mcf type mismatch, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.8::real,
-    'avg_width', 9::integer,
-    'n_distinct', -0.9::real,
+    'null_frac', 0.22::real,
     'most_common_vals', '{2,1,3}'::text,
     'most_common_freqs', '{0.2,0.1}'::double precision[]
     );
@@ -299,15 +366,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: mcv cast failure
+-- warn: mcv cast failure, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.9::real,
-    'avg_width', 10::integer,
-    'n_distinct', -0.4::real,
+    'null_frac', 0.23::real,
     'most_common_vals', '{2,four,3}'::text,
     'most_common_freqs', '{0.3,0.25,0.05}'::real[]
     );
@@ -324,10 +388,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 1::integer,
-    'n_distinct', -0.1::real,
     'most_common_vals', '{2,1,3}'::text,
     'most_common_freqs', '{0.3,0.25,0.05}'::real[]
     );
@@ -339,15 +399,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: NULL in histogram array
+-- warn: NULL in histogram array, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.2::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.2::real,
+    'null_frac', 0.24::real,
     'histogram_bounds', '{1,NULL,3,4}'::text
     );
 
@@ -363,11 +420,8 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.3::real,
-    'avg_width', 3::integer,
-    'n_distinct', -0.3::real,
-    'histogram_bounds', '{1,2,3,4}'::text );
+    'histogram_bounds', '{1,2,3,4}'::text
+    );
 
 SELECT *
 FROM pg_stats
@@ -376,16 +430,13 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: elem_count_histogram null element
+-- warn: elem_count_histogram null element, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.4::real,
-    'avg_width', 5::integer,
-    'n_distinct', -0.4::real,
-    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    'null_frac', 0.25::real,
+    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1}'::real[]
     );
 
 SELECT *
@@ -400,10 +451,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.5::real,
-    'avg_width', 6::integer,
-    'n_distinct', -0.55::real,
+    'null_frac', 0.26::real,
     'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
     );
 
@@ -414,15 +462,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'tags';
 
--- range stats on a scalar type
+-- warn: range stats on a scalar type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.6::real,
-    'avg_width', 7::integer,
-    'n_distinct', -0.15::real,
+    'null_frac', 0.27::real,
     'range_empty_frac', 0.5::real,
     'range_length_histogram', '{399,499,Infinity}'::text
     );
@@ -434,15 +479,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: range_empty_frac range_length_hist null mismatch
+-- warn: range_empty_frac range_length_hist null mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.7::real,
-    'avg_width', 8::integer,
-    'n_distinct', -0.25::real,
+    'null_frac', 0.28::real,
     'range_length_histogram', '{399,499,Infinity}'::text
     );
 
@@ -453,15 +495,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'arange';
 
--- warn: range_empty_frac range_length_hist null mismatch part 2
+-- warn: range_empty_frac range_length_hist null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.8::real,
-    'avg_width', 9::integer,
-    'n_distinct', -0.35::real,
+    'null_frac', 0.29::real,
     'range_empty_frac', 0.5::real
     );
 
@@ -477,10 +516,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.9::real,
-    'avg_width', 1::integer,
-    'n_distinct', -0.19::real,
     'range_empty_frac', 0.5::real,
     'range_length_histogram', '{399,499,Infinity}'::text
     );
@@ -492,15 +527,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'arange';
 
--- warn: range bounds histogram on scalar
+-- warn: range bounds histogram on scalar, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.29::real,
+    'null_frac', 0.31::real,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
     );
 
@@ -516,10 +548,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.2::real,
-    'avg_width', 3::integer,
-    'n_distinct', -0.39::real,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
     );
 
@@ -530,23 +558,14 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'arange';
 
--- warn: cannot set most_common_elems for range type
+-- warn: cannot set most_common_elems for range type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
-    'most_common_vals', '{"[2,3)","[1,2)","[3,4)"}'::text,
-    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
-    'histogram_bounds', '{"[1,2)","[2,3)","[3,4)","[4,5)"}'::text,
-    'correlation', 1.1::real,
+    'null_frac', 0.32::real,
     'most_common_elems', '{3,1}'::text,
-    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
-    'range_empty_frac', -0.5::real,
-    'range_length_histogram', '{399,499,Infinity}'::text,
-    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
     );
 
 SELECT *
@@ -556,14 +575,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'arange';
 
--- warn: scalars can't have mcelem
+-- warn: scalars can't have mcelem, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
+    'null_frac', 0.33::real,
     'most_common_elems', '{1,3}'::text,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
     );
@@ -575,14 +592,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: mcelem / mcelem mismatch
+-- warn: mcelem / mcelem mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
+    'null_frac', 0.34::real,
     'most_common_elems', '{one,two}'::text
     );
 
@@ -593,25 +608,27 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'tags';
 
--- warn: mcelem / mcelem null mismatch part 2
+-- warn: mcelem / mcelem null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
+    'null_frac', 0.35::real,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
     );
 
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
 -- ok: mcelem
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
     'most_common_elems', '{one,three}'::text,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
     );
@@ -623,15 +640,13 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'tags';
 
--- warn: scalars can't have elem_count_histogram
+-- warn: scalars can't have elem_count_histogram, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
-    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    'null_frac', 0.36::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1}'::real[]
     );
 
 SELECT *
@@ -641,32 +656,6 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: too many stat kinds
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
-    'most_common_vals', '{"[2,3)","[1,3)","[3,9)"}'::text,
-    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
-    'histogram_bounds', '{"[1,2)","[2,3)","[3,4)","[4,)"}'::text,
-    'correlation', 1.1::real,
-    'most_common_elems', '{3,1}'::text,
-    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
-    'range_empty_frac', -0.5::real,
-    'range_length_histogram', '{399,499,Infinity}'::text,
-    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text);
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'arange';
-
 --
 -- Test the ability to exactly copy data from one table to an identical table,
 -- correctly reconstructing the stakind order as well as the staopN and
@@ -686,16 +675,6 @@ SELECT 4, 'four', NULL, int4range(0,100), NULL;
 
 CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
 
--- restoring stats on index
-SELECT * FROM pg_catalog.pg_restore_relation_stats(
-    'relation', 'stats_import.is_odd'::regclass,
-    'version', '180000'::integer,
-    'relpages', '11'::integer,
-    'reltuples', '10000'::real,
-    'relallvisible', '0'::integer,
-    'relallfrozen', '0'::integer
-);
-
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 
@@ -848,160 +827,24 @@ FROM pg_statistic s
 JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
 WHERE s.starelid = 'stats_import.is_odd'::regclass;
 
--- ok
+-- attribute stats exist before a clear, but not after
+SELECT COUNT(*)
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
 SELECT pg_catalog.pg_clear_attribute_stats(
     relation => 'stats_import.test'::regclass,
     attname => 'arange'::name,
     inherited => false::boolean);
 
---
--- Negative tests
---
-
---- error: relation is wrong type
-SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::oid,
-        'relpages', 17::integer,
-        'reltuples', 400.0::real,
-        'relallvisible', 4::integer,
-        'relallfrozen', 3::integer);
-
---- error: relation not found
-SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::regclass,
-        'relpages', 17::integer,
-        'reltuples', 400.0::real,
-        'relallvisible', 4::integer,
-        'relallfrozen', 3::integer);
-
--- warn and error: unrecognized argument name
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        'relpages', '17'::integer,
-        'reltuples', 400::real,
-        'nope', 4::integer);
-
--- error: argument name is NULL
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        NULL, '17'::integer,
-        'reltuples', 400::real,
-        'relallvisible', 4::integer,
-        'relallfrozen', 3::integer);
-
--- error: argument name is an integer
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        17, '17'::integer,
-        'reltuples', 400::real,
-        'relallvisible', 4::integer,
-        'relallfrozen', 3::integer);
-
--- error: odd number of variadic arguments cannot be pairs
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        'relpages', '17'::integer,
-        'reltuples', 400::real,
-        'relallfrozen', 3::integer,
-        'relallvisible');
-
--- error: object doesn't exist
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        'relpages', '17'::integer,
-        'reltuples', 400::real,
-        'relallvisible', 4::integer,
-        'relallfrozen', 3::integer);
-
--- error: object does not exist
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', '0'::oid::regclass,
-    'attname', 'id'::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-
--- error: relation null
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', NULL::oid,
-    'attname', 'id'::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-
--- error: missing attname
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', NULL::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-
--- error: both attname and attnum
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
-    'attnum', 1::smallint,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-
--- error: attname doesn't exist
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'nope'::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-
--- error: attribute is system column
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'xmin'::name,
-    'inherited', false::boolean,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-
--- error: inherited null
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
-    'inherited', NULL::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-
--- error: relation not found
-SELECT pg_catalog.pg_clear_relation_stats(
-    relation => 'stats_import.nope'::regclass);
-
--- error: attribute is system column
-SELECT pg_catalog.pg_clear_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'ctid'::name,
-    inherited => false::boolean);
-
--- error: attname doesn't exist
-SELECT pg_catalog.pg_clear_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'nope'::name,
-    inherited => false::boolean);
+SELECT COUNT(*)
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
 
 DROP SCHEMA stats_import CASCADE;
-- 
2.48.1

v5-0003-Split-relation-into-schemaname-and-relname.patchtext/x-patch; charset=US-ASCII; name=v5-0003-Split-relation-into-schemaname-and-relname.patchDownload
From 87ab5a7a5061ba1b064bf81f115bb5c4204d0fc6 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Tue, 4 Mar 2025 22:16:52 -0500
Subject: [PATCH v5 3/3] Split relation into schemaname and relname.

In order to further reduce potential error-failures in restores and
upgrades, replace the numerous casts of fully qualified relation names
into their schema+relname text components.

Further remove the ::name casts on attname and change the expected
datatype to text.
---
 src/include/catalog/pg_proc.dat            |   8 +-
 src/backend/statistics/attribute_stats.c   |  98 +++++--
 src/backend/statistics/relation_stats.c    |  70 +++--
 src/bin/pg_dump/pg_dump.c                  |  25 +-
 src/bin/pg_dump/t/002_pg_dump.pl           |   6 +-
 src/test/regress/expected/stats_import.out | 302 +++++++++++++--------
 src/test/regress/sql/stats_import.sql      | 271 +++++++++++-------
 doc/src/sgml/func.sgml                     |  41 +--
 8 files changed, 537 insertions(+), 284 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index cd9422d0bac..25a63f279aa 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12424,8 +12424,8 @@
   descr => 'clear statistics on relation',
   proname => 'pg_clear_relation_stats', provolatile => 'v', proisstrict => 'f',
   proparallel => 'u', prorettype => 'void',
-  proargtypes => 'regclass',
-  proargnames => '{relation}',
+  proargtypes => 'text text',
+  proargnames => '{schemaname,relname}',
   prosrc => 'pg_clear_relation_stats' },
 { oid => '8461',
   descr => 'restore statistics on attribute',
@@ -12440,8 +12440,8 @@
   descr => 'clear statistics on attribute',
   proname => 'pg_clear_attribute_stats', provolatile => 'v', proisstrict => 'f',
   proparallel => 'u', prorettype => 'void',
-  proargtypes => 'regclass name bool',
-  proargnames => '{relation,attname,inherited}',
+  proargtypes => 'text text text bool',
+  proargnames => '{schemaname,relname,attname,inherited}',
   prosrc => 'pg_clear_attribute_stats' },
 
 # GiST stratnum implementations
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index 6bcbee0edba..65456a04ae5 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -19,6 +19,7 @@
 
 #include "access/heapam.h"
 #include "catalog/indexing.h"
+#include "catalog/namespace.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_operator.h"
 #include "nodes/nodeFuncs.h"
@@ -36,7 +37,8 @@
 
 enum attribute_stats_argnum
 {
-	ATTRELATION_ARG = 0,
+	ATTRELSCHEMA_ARG = 0,
+	ATTRELNAME_ARG,
 	ATTNAME_ARG,
 	ATTNUM_ARG,
 	INHERITED_ARG,
@@ -58,8 +60,9 @@ enum attribute_stats_argnum
 
 static struct StatsArgInfo attarginfo[] =
 {
-	[ATTRELATION_ARG] = {"relation", REGCLASSOID},
-	[ATTNAME_ARG] = {"attname", NAMEOID},
+	[ATTRELSCHEMA_ARG] = {"schemaname", TEXTOID},
+	[ATTRELNAME_ARG] = {"relname", TEXTOID},
+	[ATTNAME_ARG] = {"attname", TEXTOID},
 	[ATTNUM_ARG] = {"attnum", INT2OID},
 	[INHERITED_ARG] = {"inherited", BOOLOID},
 	[NULL_FRAC_ARG] = {"null_frac", FLOAT4OID},
@@ -80,7 +83,8 @@ static struct StatsArgInfo attarginfo[] =
 
 enum clear_attribute_stats_argnum
 {
-	C_ATTRELATION_ARG = 0,
+	C_ATTRELSCHEMA_ARG = 0,
+	C_ATTRELNAME_ARG,
 	C_ATTNAME_ARG,
 	C_INHERITED_ARG,
 	C_NUM_ATTRIBUTE_STATS_ARGS
@@ -88,8 +92,9 @@ enum clear_attribute_stats_argnum
 
 static struct StatsArgInfo cleararginfo[] =
 {
-	[C_ATTRELATION_ARG] = {"relation", REGCLASSOID},
-	[C_ATTNAME_ARG] = {"attname", NAMEOID},
+	[C_ATTRELSCHEMA_ARG] = {"relation", TEXTOID},
+	[C_ATTRELNAME_ARG] = {"relation", TEXTOID},
+	[C_ATTNAME_ARG] = {"attname", TEXTOID},
 	[C_INHERITED_ARG] = {"inherited", BOOLOID},
 	[C_NUM_ATTRIBUTE_STATS_ARGS] = {0}
 };
@@ -133,6 +138,9 @@ static void init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
 static bool
 attribute_statistics_update(FunctionCallInfo fcinfo)
 {
+	char	   *nspname;
+	Oid			nspoid;
+	char	   *relname;
 	Oid			reloid;
 	char	   *attname;
 	AttrNumber	attnum;
@@ -170,8 +178,28 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 	bool		result = true;
 
-	stats_check_required_arg(fcinfo, attarginfo, ATTRELATION_ARG);
-	reloid = PG_GETARG_OID(ATTRELATION_ARG);
+	stats_check_required_arg(fcinfo, attarginfo, ATTRELSCHEMA_ARG);
+	stats_check_required_arg(fcinfo, attarginfo, ATTRELNAME_ARG);
+
+	nspname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELSCHEMA_ARG));
+	nspoid = get_namespace_oid(nspname, true);
+	if (nspoid == InvalidOid)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("Namespace \"%s\" not found.", nspname)));
+		return false;
+	}
+
+	relname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELNAME_ARG));
+	reloid = get_relname_relid(relname, nspoid);
+	if (reloid == InvalidOid)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("Relation \"%s\".\"%s\" not found.", nspname, relname)));
+		return false;
+	}
 
 	if (RecoveryInProgress())
 		ereport(ERROR,
@@ -185,21 +213,18 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	/* user can specify either attname or attnum, but not both */
 	if (!PG_ARGISNULL(ATTNAME_ARG))
 	{
-		Name		attnamename;
-
 		if (!PG_ARGISNULL(ATTNUM_ARG))
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("cannot specify both attname and attnum")));
-		attnamename = PG_GETARG_NAME(ATTNAME_ARG);
-		attname = NameStr(*attnamename);
+		attname = TextDatumGetCString(PG_GETARG_DATUM(ATTNAME_ARG));
 		attnum = get_attnum(reloid, attname);
 		/* note that this test covers attisdropped cases too: */
 		if (attnum == InvalidAttrNumber)
 			ereport(ERROR,
 					(errcode(ERRCODE_UNDEFINED_COLUMN),
-					 errmsg("column \"%s\" of relation \"%s\" does not exist",
-							attname, get_rel_name(reloid))));
+					 errmsg("column \"%s\" of relation \"%s\".\"%s\" does not exist",
+							attname, nspname, relname)));
 	}
 	else if (!PG_ARGISNULL(ATTNUM_ARG))
 	{
@@ -210,8 +235,8 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 			!SearchSysCacheExistsAttName(reloid, attname))
 			ereport(ERROR,
 					(errcode(ERRCODE_UNDEFINED_COLUMN),
-					 errmsg("column %d of relation \"%s\" does not exist",
-							attnum, get_rel_name(reloid))));
+					 errmsg("column %d of relation \"%s\".\"%s\" does not exist",
+							attnum, nspname, relname)));
 	}
 	else
 	{
@@ -900,13 +925,38 @@ init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
 Datum
 pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 {
+	char	   *nspname;
+	Oid			nspoid;
+	char	   *relname;
 	Oid			reloid;
-	Name		attname;
+	char	   *attname;
 	AttrNumber	attnum;
 	bool		inherited;
 
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELATION_ARG);
-	reloid = PG_GETARG_OID(C_ATTRELATION_ARG);
+	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELSCHEMA_ARG);
+	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELNAME_ARG);
+	stats_check_required_arg(fcinfo, cleararginfo, C_ATTNAME_ARG);
+	stats_check_required_arg(fcinfo, cleararginfo, C_INHERITED_ARG);
+
+	nspname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELSCHEMA_ARG));
+	nspoid = get_namespace_oid(nspname, true);
+	if (nspoid == InvalidOid)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("Namespace \"%s\" not found.", nspname)));
+		return false;
+	}
+
+	relname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELNAME_ARG));
+	reloid = get_relname_relid(relname, nspoid);
+	if (reloid == InvalidOid)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("Relation \"%s\".\"%s\" not found.", nspname, relname)));
+		return false;
+	}
 
 	if (RecoveryInProgress())
 		ereport(ERROR,
@@ -916,23 +966,21 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 
 	stats_lock_check_privileges(reloid);
 
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTNAME_ARG);
-	attname = PG_GETARG_NAME(C_ATTNAME_ARG);
-	attnum = get_attnum(reloid, NameStr(*attname));
+	attname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTNAME_ARG));
+	attnum = get_attnum(reloid, attname);
 
 	if (attnum < 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot clear statistics on system column \"%s\"",
-						NameStr(*attname))));
+						attname)));
 
 	if (attnum == InvalidAttrNumber)
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 errmsg("column \"%s\" of relation \"%s\" does not exist",
-						NameStr(*attname), get_rel_name(reloid))));
+						attname, get_rel_name(reloid))));
 
-	stats_check_required_arg(fcinfo, cleararginfo, C_INHERITED_ARG);
 	inherited = PG_GETARG_BOOL(C_INHERITED_ARG);
 
 	delete_pg_statistic(reloid, attnum, inherited);
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index 2c1cea3fc80..4c1e75a3c78 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -19,9 +19,12 @@
 
 #include "access/heapam.h"
 #include "catalog/indexing.h"
+#include "catalog/namespace.h"
 #include "statistics/stat_utils.h"
+#include "utils/builtins.h"
 #include "utils/fmgroids.h"
 #include "utils/fmgrprotos.h"
+#include "utils/lsyscache.h"
 #include "utils/syscache.h"
 
 
@@ -32,7 +35,8 @@
 
 enum relation_stats_argnum
 {
-	RELATION_ARG = 0,
+	RELSCHEMA_ARG = 0,
+	RELNAME_ARG,
 	RELPAGES_ARG,
 	RELTUPLES_ARG,
 	RELALLVISIBLE_ARG,
@@ -42,7 +46,8 @@ enum relation_stats_argnum
 
 static struct StatsArgInfo relarginfo[] =
 {
-	[RELATION_ARG] = {"relation", REGCLASSOID},
+	[RELSCHEMA_ARG] = {"schemaname", TEXTOID},
+	[RELNAME_ARG] = {"relname", TEXTOID},
 	[RELPAGES_ARG] = {"relpages", INT4OID},
 	[RELTUPLES_ARG] = {"reltuples", FLOAT4OID},
 	[RELALLVISIBLE_ARG] = {"relallvisible", INT4OID},
@@ -59,6 +64,9 @@ static bool
 relation_statistics_update(FunctionCallInfo fcinfo)
 {
 	bool		result = true;
+	char	   *nspname;
+	Oid			nspoid;
+	char	   *relname;
 	Oid			reloid;
 	Relation	crel;
 	BlockNumber relpages = 0;
@@ -76,6 +84,37 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 	bool		nulls[4] = {0};
 	int			nreplaces = 0;
 
+	stats_check_required_arg(fcinfo, relarginfo, RELSCHEMA_ARG);
+	stats_check_required_arg(fcinfo, relarginfo, RELNAME_ARG);
+
+	nspname = TextDatumGetCString(PG_GETARG_DATUM(RELSCHEMA_ARG));
+	nspoid = get_namespace_oid(nspname, true);
+	if (nspoid == InvalidOid)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("Namespace \"%s\" not found.", nspname)));
+		return false;
+	}
+
+	relname = TextDatumGetCString(PG_GETARG_DATUM(RELNAME_ARG));
+	reloid = get_relname_relid(relname, nspoid);
+	if (reloid == InvalidOid)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("Relation \"%s\".\"%s\" not found.", nspname, relname)));
+		return false;
+	}
+
+	if (RecoveryInProgress())
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("recovery is in progress"),
+				 errhint("Statistics cannot be modified during recovery.")));
+
+	stats_lock_check_privileges(reloid);
+
 	if (!PG_ARGISNULL(RELPAGES_ARG))
 	{
 		relpages = PG_GETARG_UINT32(RELPAGES_ARG);
@@ -108,17 +147,6 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 		update_relallfrozen = true;
 	}
 
-	stats_check_required_arg(fcinfo, relarginfo, RELATION_ARG);
-	reloid = PG_GETARG_OID(RELATION_ARG);
-
-	if (RecoveryInProgress())
-		ereport(ERROR,
-				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
-				 errmsg("recovery is in progress"),
-				 errhint("Statistics cannot be modified during recovery.")));
-
-	stats_lock_check_privileges(reloid);
-
 	/*
 	 * Take RowExclusiveLock on pg_class, consistent with
 	 * vac_update_relstats().
@@ -193,20 +221,22 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 Datum
 pg_clear_relation_stats(PG_FUNCTION_ARGS)
 {
-	LOCAL_FCINFO(newfcinfo, 5);
+	LOCAL_FCINFO(newfcinfo, 6);
 
-	InitFunctionCallInfoData(*newfcinfo, NULL, 5, InvalidOid, NULL, NULL);
+	InitFunctionCallInfoData(*newfcinfo, NULL, 6, InvalidOid, NULL, NULL);
 
-	newfcinfo->args[0].value = PG_GETARG_OID(0);
+	newfcinfo->args[0].value = PG_GETARG_DATUM(0);
 	newfcinfo->args[0].isnull = PG_ARGISNULL(0);
-	newfcinfo->args[1].value = UInt32GetDatum(0);
-	newfcinfo->args[1].isnull = false;
-	newfcinfo->args[2].value = Float4GetDatum(-1.0);
+	newfcinfo->args[1].value = PG_GETARG_DATUM(1);
+	newfcinfo->args[1].isnull = PG_ARGISNULL(1);
+	newfcinfo->args[2].value = UInt32GetDatum(0);
 	newfcinfo->args[2].isnull = false;
-	newfcinfo->args[3].value = UInt32GetDatum(0);
+	newfcinfo->args[3].value = Float4GetDatum(-1.0);
 	newfcinfo->args[3].isnull = false;
 	newfcinfo->args[4].value = UInt32GetDatum(0);
 	newfcinfo->args[4].isnull = false;
+	newfcinfo->args[5].value = UInt32GetDatum(0);
+	newfcinfo->args[5].isnull = false;
 
 	relation_statistics_update(newfcinfo);
 	PG_RETURN_VOID();
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 4f4ad2ee150..6cf2c7d1fe4 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -10492,7 +10492,6 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 	PQExpBuffer out;
 	DumpId	   *deps = NULL;
 	int			ndeps = 0;
-	char	   *qualified_name;
 	char		reltuples_str[FLOAT_SHORTEST_DECIMAL_LEN];
 	int			i_attname;
 	int			i_inherited;
@@ -10558,15 +10557,16 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 
 	out = createPQExpBuffer();
 
-	qualified_name = pg_strdup(fmtQualifiedDumpable(rsinfo));
-
 	/* restore relation stats */
 	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
 	appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
 					  fout->remoteVersion);
-	appendPQExpBufferStr(out, "\t'relation', ");
-	appendStringLiteralAH(out, qualified_name, fout);
-	appendPQExpBufferStr(out, "::regclass,\n");
+	appendPQExpBufferStr(out, "\t'schemaname', ");
+	appendStringLiteralAH(out, rsinfo->dobj.namespace->dobj.name, fout);
+	appendPQExpBufferStr(out, ",\n");
+	appendPQExpBufferStr(out, "\t'relname', ");
+	appendStringLiteralAH(out, rsinfo->dobj.name, fout);
+	appendPQExpBufferStr(out, ",\n");
 	appendPQExpBuffer(out, "\t'relpages', '%d'::integer,\n", rsinfo->relpages);
 	float_to_shortest_decimal_buf(rsinfo->reltuples, reltuples_str);
 	appendPQExpBuffer(out, "\t'reltuples', '%s'::real,\n", reltuples_str);
@@ -10606,9 +10606,10 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
 		appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
 						  fout->remoteVersion);
-		appendPQExpBufferStr(out, "\t'relation', ");
-		appendStringLiteralAH(out, qualified_name, fout);
-		appendPQExpBufferStr(out, "::regclass");
+		appendPQExpBufferStr(out, "\t'schemaname', ");
+		appendStringLiteralAH(out, rsinfo->dobj.namespace->dobj.name, fout);
+		appendPQExpBufferStr(out, ",\n\t'relname', ");
+		appendStringLiteralAH(out, rsinfo->dobj.name, fout);
 
 		if (PQgetisnull(res, rownum, i_attname))
 			pg_fatal("attname cannot be NULL");
@@ -10620,7 +10621,10 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 		 * their attnames are not necessarily stable across dump/reload.
 		 */
 		if (rsinfo->nindAttNames == 0)
-			appendNamedArgument(out, fout, "attname", "name", attname);
+		{
+			appendPQExpBuffer(out, ",\n\t'attname', ");
+			appendStringLiteralAH(out, attname, fout);
+		}
 		else
 		{
 			bool		found = false;
@@ -10700,7 +10704,6 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 							  .deps = deps,
 							  .nDeps = ndeps));
 
-	free(qualified_name);
 	destroyPQExpBuffer(out);
 	destroyPQExpBuffer(query);
 }
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index c7bffc1b045..b037f239136 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -4725,14 +4725,16 @@ my %tests = (
 		regexp => qr/^
 			\QSELECT * FROM pg_catalog.pg_restore_relation_stats(\E\s+
 			'version',\s'\d+'::integer,\s+
-			'relation',\s'dump_test.dup_test_post_data_ix'::regclass,\s+
+			'schemaname',\s'dump_test',\s+
+			'relname',\s'dup_test_post_data_ix',\s+
 			'relpages',\s'\d+'::integer,\s+
 			'reltuples',\s'\d+'::real,\s+
 			'relallvisible',\s'\d+'::integer\s+
 			\);\s+
 			\QSELECT * FROM pg_catalog.pg_restore_attribute_stats(\E\s+
 			'version',\s'\d+'::integer,\s+
-			'relation',\s'dump_test.dup_test_post_data_ix'::regclass,\s+
+			'schemaname',\s'dump_test',\s+
+			'relname',\s'dup_test_post_data_ix',\s+
 			'attnum',\s'2'::smallint,\s+
 			'inherited',\s'f'::boolean,\s+
 			'null_frac',\s'0'::real,\s+
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index ebba14c6a1d..ca7fe39660e 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -16,33 +16,54 @@ CREATE INDEX test_i ON stats_import.test(id);
 --
 -- relstats tests
 --
---- error: relation is wrong type
+-- error: schemaname missing
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::oid,
+        'relname', 'test',
         'relpages', 17::integer);
-WARNING:  argument "relation" has type "oid", expected type "regclass"
-ERROR:  "relation" cannot be NULL
+ERROR:  "schemaname" cannot be NULL
+-- error: relname missing
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relpages', 17::integer);
+ERROR:  "relname" cannot be NULL
+--- error: schemaname is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 3.6::float,
+        'relname', 'test',
+        'relpages', 17::integer);
+WARNING:  argument "schemaname" has type "double precision", expected type "text"
+ERROR:  "schemaname" cannot be NULL
+--- error: relname is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relname', 0::oid,
+        'relpages', 17::integer);
+WARNING:  argument "relname" has type "oid", expected type "text"
+ERROR:  "relname" cannot be NULL
 -- error: relation not found
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::oid::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'nope',
         'relpages', 17::integer);
-ERROR:  could not open relation with OID 0
+WARNING:  Relation "stats_import"."nope" not found.
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
 -- error: odd number of variadic arguments cannot be pairs
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relallvisible');
 ERROR:  variadic arguments must be name/value pairs
 HINT:  Provide an even number of variadic arguments that can be divided into pairs.
 -- error: argument name is NULL
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         NULL, '17'::integer);
-ERROR:  name at variadic position 3 is NULL
--- error: argument name is not a text type
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        17, '17'::integer);
-ERROR:  name at variadic position 3 has type "integer", expected type "text"
+ERROR:  name at variadic position 5 is NULL
 -- starting stats
 SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
@@ -55,7 +76,8 @@ WHERE oid = 'stats_import.test_i'::regclass;
 -- regular indexes have special case locking rules
 BEGIN;
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.test_i'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test_i',
         'relpages', 18::integer);
  pg_restore_relation_stats 
 ---------------------------
@@ -103,7 +125,8 @@ WHERE oid = 'stats_import.part_parent'::regclass;
 --
 BEGIN;
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent_i'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'part_parent_i',
         'relpages', 2::integer);
  pg_restore_relation_stats 
 ---------------------------
@@ -137,7 +160,8 @@ WHERE oid = 'stats_import.part_parent_i'::regclass;
 
 -- ok: set all relstats, with version, no bounds checking
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'version', 150000::integer,
         'relpages', '-17'::integer,
         'reltuples', 400::real,
@@ -158,7 +182,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just relpages, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', '16'::integer);
  pg_restore_relation_stats 
 ---------------------------
@@ -175,7 +200,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just reltuples, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'reltuples', '500'::real);
  pg_restore_relation_stats 
 ---------------------------
@@ -192,7 +218,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just relallvisible, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relallvisible', 5::integer);
  pg_restore_relation_stats 
 ---------------------------
@@ -209,7 +236,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: just relallfrozen
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'version', 150000::integer,
         'relallfrozen', 3::integer);
  pg_restore_relation_stats 
@@ -227,7 +255,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- warn: bad relpages type, rest updated
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', 'nope'::text,
         'reltuples', 400.0::real,
         'relallvisible', 4::integer,
@@ -248,7 +277,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- unrecognized argument name, rest ok
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', '171'::integer,
         'nope', 10::integer);
 WARNING:  unrecognized argument name: "nope"
@@ -266,8 +296,7 @@ WHERE oid = 'stats_import.test'::regclass;
 (1 row)
 
 -- ok: clear stats
-SELECT pg_catalog.pg_clear_relation_stats(
-    relation => 'stats_import.test'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'test');
  pg_clear_relation_stats 
 -------------------------
  
@@ -284,87 +313,123 @@ WHERE oid = 'stats_import.test'::regclass;
 -- invalid relkinds for statistics
 CREATE SEQUENCE stats_import.testseq;
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.testseq'::regclass);
+        'schemaname', 'stats_import',
+        'relname', 'testseq');
 ERROR:  cannot modify statistics for relation "testseq"
 DETAIL:  This operation is not supported for sequences.
-SELECT pg_catalog.pg_clear_relation_stats(
-        'stats_import.testseq'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testseq');
 ERROR:  cannot modify statistics for relation "testseq"
 DETAIL:  This operation is not supported for sequences.
 CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
-SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.testview'::regclass);
-ERROR:  cannot modify statistics for relation "testview"
-DETAIL:  This operation is not supported for views.
-SELECT pg_catalog.pg_clear_relation_stats(
-        'stats_import.testview'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testview');
 ERROR:  cannot modify statistics for relation "testview"
 DETAIL:  This operation is not supported for views.
 --
 -- attribute stats
 --
--- error: object does not exist
+-- error: schemaname missing
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', '0'::oid::regclass,
-    'attname', 'id'::name,
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  could not open relation with OID 0
--- error: relation null
+ERROR:  "schemaname" cannot be NULL
+-- error: schema does not exist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', NULL::oid::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'nope',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  "relation" cannot be NULL
+WARNING:  Namespace "nope" not found.
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- error: relname missing
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+ERROR:  "relname" cannot be NULL
+-- error: relname does not exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'relname', 'nope',
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+WARNING:  Relation "stats_import"."nope" not found.
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- error: relname null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'relname', NULL,
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+ERROR:  "relname" cannot be NULL
 -- error: NULL attname
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', NULL::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', NULL,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 ERROR:  must specify either attname or attnum
 -- error: attname doesn't exist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'nope'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'nope',
     'inherited', false::boolean,
     'null_frac', 0.1::real,
     'avg_width', 2::integer,
     'n_distinct', 0.3::real);
-ERROR:  column "nope" of relation "test" does not exist
+ERROR:  column "nope" of relation "stats_import"."test" does not exist
 -- error: both attname and attnum
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'attnum', 1::smallint,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 ERROR:  cannot specify both attname and attnum
 -- error: neither attname nor attnum
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
+    'schemaname', 'stats_import',
+    'relname', 'test',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 ERROR:  must specify either attname or attnum
 -- error: attribute is system column
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'xmin'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'xmin',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 ERROR:  cannot modify statistics on system column "xmin"
 -- error: inherited null
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', NULL::boolean,
     'null_frac', 0.1::real);
 ERROR:  "inherited" cannot be NULL
 -- ok: just the fixed values, with version, no stakinds
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'version', 150000::integer,
     'null_frac', 0.2::real,
@@ -392,7 +457,8 @@ AND attname = 'id';
 -- for any stat-having relation.
 --
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
+    'schemaname', 'stats_import',
+    'relname', 'test',
     'attnum', 1::smallint,
     'inherited', false::boolean,
     'null_frac', 0.4::real);
@@ -414,8 +480,9 @@ AND attname = 'id';
 
 -- warn: unrecognized argument name, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.2::real,
     'nope', 0.5::real);
@@ -438,8 +505,9 @@ AND attname = 'id';
 
 -- warn: mcv / mcf null mismatch part 1, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.21::real,
     'most_common_freqs', '{0.1,0.2,0.3}'::real[]
@@ -463,8 +531,9 @@ AND attname = 'id';
 
 -- warn: mcv / mcf null mismatch part 2, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.21::real,
     'most_common_vals', '{1,2,3}'::text
@@ -488,8 +557,9 @@ AND attname = 'id';
 
 -- warn: mcf type mismatch, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.22::real,
     'most_common_vals', '{2,1,3}'::text,
@@ -515,8 +585,9 @@ AND attname = 'id';
 
 -- warn: mcv cast failure, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.23::real,
     'most_common_vals', '{2,four,3}'::text,
@@ -541,8 +612,9 @@ AND attname = 'id';
 
 -- ok: mcv+mcf
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'most_common_vals', '{2,1,3}'::text,
     'most_common_freqs', '{0.3,0.25,0.05}'::real[]
@@ -565,8 +637,9 @@ AND attname = 'id';
 
 -- warn: NULL in histogram array, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.24::real,
     'histogram_bounds', '{1,NULL,3,4}'::text
@@ -590,8 +663,9 @@ AND attname = 'id';
 
 -- ok: histogram_bounds
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'histogram_bounds', '{1,2,3,4}'::text
     );
@@ -613,8 +687,9 @@ AND attname = 'id';
 
 -- warn: elem_count_histogram null element, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.25::real,
     'elem_count_histogram', '{1,1,NULL,1,1,1,1,1}'::real[]
@@ -638,8 +713,9 @@ AND attname = 'tags';
 
 -- ok: elem_count_histogram
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.26::real,
     'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
@@ -662,8 +738,9 @@ AND attname = 'tags';
 
 -- warn: range stats on a scalar type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.27::real,
     'range_empty_frac', 0.5::real,
@@ -689,8 +766,9 @@ AND attname = 'id';
 
 -- warn: range_empty_frac range_length_hist null mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.28::real,
     'range_length_histogram', '{399,499,Infinity}'::text
@@ -714,8 +792,9 @@ AND attname = 'arange';
 
 -- warn: range_empty_frac range_length_hist null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.29::real,
     'range_empty_frac', 0.5::real
@@ -739,8 +818,9 @@ AND attname = 'arange';
 
 -- ok: range_empty_frac + range_length_hist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'range_empty_frac', 0.5::real,
     'range_length_histogram', '{399,499,Infinity}'::text
@@ -763,8 +843,9 @@ AND attname = 'arange';
 
 -- warn: range bounds histogram on scalar, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.31::real,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
@@ -789,8 +870,9 @@ AND attname = 'id';
 
 -- ok: range_bounds_histogram
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
     );
@@ -812,8 +894,9 @@ AND attname = 'arange';
 
 -- warn: cannot set most_common_elems for range type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.32::real,
     'most_common_elems', '{3,1}'::text,
@@ -839,8 +922,9 @@ AND attname = 'arange';
 
 -- warn: scalars can't have mcelem, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.33::real,
     'most_common_elems', '{1,3}'::text,
@@ -866,8 +950,9 @@ AND attname = 'id';
 
 -- warn: mcelem / mcelem mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.34::real,
     'most_common_elems', '{one,two}'::text
@@ -891,8 +976,9 @@ AND attname = 'tags';
 
 -- warn: mcelem / mcelem null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.35::real,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
@@ -916,8 +1002,9 @@ AND attname = 'tags';
 
 -- ok: mcelem
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'most_common_elems', '{one,three}'::text,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
@@ -940,8 +1027,9 @@ AND attname = 'tags';
 
 -- warn: scalars can't have elem_count_histogram, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.36::real,
     'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1}'::real[]
@@ -993,8 +1081,9 @@ SELECT s.schemaname, s.tablename, s.attname, s.inherited, r.*
 FROM pg_catalog.pg_stats AS s
 CROSS JOIN LATERAL
     pg_catalog.pg_restore_attribute_stats(
-        'relation', ('stats_import.' || s.tablename || '_clone')::regclass,
-        'attname', s.attname,
+        'schemaname', 'stats_import',
+        'relname', s.tablename::text || '_clone',
+        'attname', s.attname::text,
         'inherited', s.inherited,
         'version', 150000,
         'null_frac', s.null_frac,
@@ -1171,9 +1260,10 @@ AND attname = 'arange';
 (1 row)
 
 SELECT pg_catalog.pg_clear_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'arange'::name,
-    inherited => false::boolean);
+    schemaname => 'stats_import',
+    relname => 'test',
+    attname => 'arange',
+    inherited => false);
  pg_clear_attribute_stats 
 --------------------------
  
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index 8d04ff4f378..3c330387243 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -21,31 +21,46 @@ CREATE INDEX test_i ON stats_import.test(id);
 -- relstats tests
 --
 
---- error: relation is wrong type
+-- error: schemaname missing
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::oid,
+        'relname', 'test',
+        'relpages', 17::integer);
+
+-- error: relname missing
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relpages', 17::integer);
+
+--- error: schemaname is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 3.6::float,
+        'relname', 'test',
+        'relpages', 17::integer);
+
+--- error: relname is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relname', 0::oid,
         'relpages', 17::integer);
 
 -- error: relation not found
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::oid::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'nope',
         'relpages', 17::integer);
 
 -- error: odd number of variadic arguments cannot be pairs
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relallvisible');
 
 -- error: argument name is NULL
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         NULL, '17'::integer);
 
--- error: argument name is not a text type
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        17, '17'::integer);
-
 -- starting stats
 SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
@@ -54,7 +69,8 @@ WHERE oid = 'stats_import.test_i'::regclass;
 -- regular indexes have special case locking rules
 BEGIN;
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.test_i'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test_i',
         'relpages', 18::integer);
 
 SELECT mode FROM pg_locks
@@ -91,7 +107,8 @@ WHERE oid = 'stats_import.part_parent'::regclass;
 BEGIN;
 
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent_i'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'part_parent_i',
         'relpages', 2::integer);
 
 SELECT mode FROM pg_locks
@@ -110,7 +127,8 @@ WHERE oid = 'stats_import.part_parent_i'::regclass;
 
 -- ok: set all relstats, with version, no bounds checking
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'version', 150000::integer,
         'relpages', '-17'::integer,
         'reltuples', 400::real,
@@ -123,7 +141,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just relpages, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', '16'::integer);
 
 SELECT relpages, reltuples, relallvisible, relallfrozen
@@ -132,7 +151,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just reltuples, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'reltuples', '500'::real);
 
 SELECT relpages, reltuples, relallvisible, relallfrozen
@@ -141,7 +161,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just relallvisible, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relallvisible', 5::integer);
 
 SELECT relpages, reltuples, relallvisible, relallfrozen
@@ -150,7 +171,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: just relallfrozen
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'version', 150000::integer,
         'relallfrozen', 3::integer);
 
@@ -160,7 +182,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- warn: bad relpages type, rest updated
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', 'nope'::text,
         'reltuples', 400.0::real,
         'relallvisible', 4::integer,
@@ -172,7 +195,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- unrecognized argument name, rest ok
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', '171'::integer,
         'nope', 10::integer);
 
@@ -181,8 +205,7 @@ FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: clear stats
-SELECT pg_catalog.pg_clear_relation_stats(
-    relation => 'stats_import.test'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'test');
 
 SELECT relpages, reltuples, relallvisible
 FROM pg_class
@@ -192,48 +215,70 @@ WHERE oid = 'stats_import.test'::regclass;
 CREATE SEQUENCE stats_import.testseq;
 
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.testseq'::regclass);
+        'schemaname', 'stats_import',
+        'relname', 'testseq');
 
-SELECT pg_catalog.pg_clear_relation_stats(
-        'stats_import.testseq'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testseq');
 
 CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
 
-SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.testview'::regclass);
-
-SELECT pg_catalog.pg_clear_relation_stats(
-        'stats_import.testview'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testview');
 
 --
 -- attribute stats
 --
 
--- error: object does not exist
+-- error: schemaname missing
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', '0'::oid::regclass,
-    'attname', 'id'::name,
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: relation null
+-- error: schema does not exist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', NULL::oid::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'nope',
+    'relname', 'test',
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: relname missing
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: relname does not exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'relname', 'nope',
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: relname null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'relname', NULL,
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
 -- error: NULL attname
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', NULL::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', NULL,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
 -- error: attname doesn't exist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'nope'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'nope',
     'inherited', false::boolean,
     'null_frac', 0.1::real,
     'avg_width', 2::integer,
@@ -241,36 +286,41 @@ SELECT pg_catalog.pg_restore_attribute_stats(
 
 -- error: both attname and attnum
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'attnum', 1::smallint,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
 -- error: neither attname nor attnum
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
+    'schemaname', 'stats_import',
+    'relname', 'test',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
 -- error: attribute is system column
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'xmin'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'xmin',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
 -- error: inherited null
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', NULL::boolean,
     'null_frac', 0.1::real);
 
 -- ok: just the fixed values, with version, no stakinds
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'version', 150000::integer,
     'null_frac', 0.2::real,
@@ -290,7 +340,8 @@ AND attname = 'id';
 -- for any stat-having relation.
 --
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
+    'schemaname', 'stats_import',
+    'relname', 'test',
     'attnum', 1::smallint,
     'inherited', false::boolean,
     'null_frac', 0.4::real);
@@ -304,8 +355,9 @@ AND attname = 'id';
 
 -- warn: unrecognized argument name, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.2::real,
     'nope', 0.5::real);
@@ -319,8 +371,9 @@ AND attname = 'id';
 
 -- warn: mcv / mcf null mismatch part 1, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.21::real,
     'most_common_freqs', '{0.1,0.2,0.3}'::real[]
@@ -335,8 +388,9 @@ AND attname = 'id';
 
 -- warn: mcv / mcf null mismatch part 2, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.21::real,
     'most_common_vals', '{1,2,3}'::text
@@ -351,8 +405,9 @@ AND attname = 'id';
 
 -- warn: mcf type mismatch, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.22::real,
     'most_common_vals', '{2,1,3}'::text,
@@ -368,8 +423,9 @@ AND attname = 'id';
 
 -- warn: mcv cast failure, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.23::real,
     'most_common_vals', '{2,four,3}'::text,
@@ -385,8 +441,9 @@ AND attname = 'id';
 
 -- ok: mcv+mcf
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'most_common_vals', '{2,1,3}'::text,
     'most_common_freqs', '{0.3,0.25,0.05}'::real[]
@@ -401,8 +458,9 @@ AND attname = 'id';
 
 -- warn: NULL in histogram array, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.24::real,
     'histogram_bounds', '{1,NULL,3,4}'::text
@@ -417,8 +475,9 @@ AND attname = 'id';
 
 -- ok: histogram_bounds
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'histogram_bounds', '{1,2,3,4}'::text
     );
@@ -432,8 +491,9 @@ AND attname = 'id';
 
 -- warn: elem_count_histogram null element, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.25::real,
     'elem_count_histogram', '{1,1,NULL,1,1,1,1,1}'::real[]
@@ -448,8 +508,9 @@ AND attname = 'tags';
 
 -- ok: elem_count_histogram
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.26::real,
     'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
@@ -464,8 +525,9 @@ AND attname = 'tags';
 
 -- warn: range stats on a scalar type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.27::real,
     'range_empty_frac', 0.5::real,
@@ -481,8 +543,9 @@ AND attname = 'id';
 
 -- warn: range_empty_frac range_length_hist null mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.28::real,
     'range_length_histogram', '{399,499,Infinity}'::text
@@ -497,8 +560,9 @@ AND attname = 'arange';
 
 -- warn: range_empty_frac range_length_hist null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.29::real,
     'range_empty_frac', 0.5::real
@@ -513,8 +577,9 @@ AND attname = 'arange';
 
 -- ok: range_empty_frac + range_length_hist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'range_empty_frac', 0.5::real,
     'range_length_histogram', '{399,499,Infinity}'::text
@@ -529,8 +594,9 @@ AND attname = 'arange';
 
 -- warn: range bounds histogram on scalar, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.31::real,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
@@ -545,8 +611,9 @@ AND attname = 'id';
 
 -- ok: range_bounds_histogram
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
     );
@@ -560,8 +627,9 @@ AND attname = 'arange';
 
 -- warn: cannot set most_common_elems for range type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.32::real,
     'most_common_elems', '{3,1}'::text,
@@ -577,8 +645,9 @@ AND attname = 'arange';
 
 -- warn: scalars can't have mcelem, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.33::real,
     'most_common_elems', '{1,3}'::text,
@@ -594,8 +663,9 @@ AND attname = 'id';
 
 -- warn: mcelem / mcelem mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.34::real,
     'most_common_elems', '{one,two}'::text
@@ -610,8 +680,9 @@ AND attname = 'tags';
 
 -- warn: mcelem / mcelem null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.35::real,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
@@ -626,8 +697,9 @@ AND attname = 'tags';
 
 -- ok: mcelem
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'most_common_elems', '{one,three}'::text,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
@@ -642,8 +714,9 @@ AND attname = 'tags';
 
 -- warn: scalars can't have elem_count_histogram, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.36::real,
     'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1}'::real[]
@@ -690,8 +763,9 @@ SELECT s.schemaname, s.tablename, s.attname, s.inherited, r.*
 FROM pg_catalog.pg_stats AS s
 CROSS JOIN LATERAL
     pg_catalog.pg_restore_attribute_stats(
-        'relation', ('stats_import.' || s.tablename || '_clone')::regclass,
-        'attname', s.attname,
+        'schemaname', 'stats_import',
+        'relname', s.tablename::text || '_clone',
+        'attname', s.attname::text,
         'inherited', s.inherited,
         'version', 150000,
         'null_frac', s.null_frac,
@@ -836,9 +910,10 @@ AND inherited = false
 AND attname = 'arange';
 
 SELECT pg_catalog.pg_clear_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'arange'::name,
-    inherited => false::boolean);
+    schemaname => 'stats_import',
+    relname => 'test',
+    attname => 'arange',
+    inherited => false);
 
 SELECT COUNT(*)
 FROM pg_stats
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index bf31b1f3eee..fc5daa5c4b1 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30324,22 +30324,24 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          <structname>mytable</structname>:
 <programlisting>
  SELECT pg_restore_relation_stats(
-    'relation',  'mytable'::regclass,
-    'relpages',  173::integer,
-    'reltuples', 10000::real);
+    'schemaname', 'myschema',
+    'relname',    'mytable',
+    'relpages',   173::integer,
+    'reltuples',  10000::real);
 </programlisting>
         </para>
         <para>
-         The argument <literal>relation</literal> with a value of type
-         <type>regclass</type> is required, and specifies the table. Other
-         arguments are the names and values of statistics corresponding to
-         certain columns in <link
+         The arguments <literal>schemaname</literal> with a value of type
+         <type>regclass</type> and <literal>relname</literal> are required,
+         and specifies the table. Other arguments are the names and values
+         of statistics corresponding to certain columns in <link
          linkend="catalog-pg-class"><structname>pg_class</structname></link>.
          The currently-supported relation statistics are
          <literal>relpages</literal> with a value of type
          <type>integer</type>, <literal>reltuples</literal> with a value of
-         type <type>real</type>, and <literal>relallvisible</literal> with a
-         value of type <type>integer</type>.
+         type <type>real</type>, <literal>relallvisible</literal> with a
+         value of type <type>integer</type>, and <literal>relallfrozen</literal>
+         with a value of type <type>integer</type>.
         </para>
         <para>
          Additionally, this function accepts argument name
@@ -30367,7 +30369,7 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          <indexterm>
           <primary>pg_clear_relation_stats</primary>
          </indexterm>
-         <function>pg_clear_relation_stats</function> ( <parameter>relation</parameter> <type>regclass</type> )
+         <function>pg_clear_relation_stats</function> ( <parameter>schemaname</parameter> <type>text</type>, <parameter>relname</parameter> <type>text</type> )
          <returnvalue>void</returnvalue>
         </para>
         <para>
@@ -30416,16 +30418,18 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          <structname>mytable</structname>:
 <programlisting>
  SELECT pg_restore_attribute_stats(
-    'relation',    'mytable'::regclass,
-    'attname',     'col1'::name,
-    'inherited',   false,
-    'avg_width',   125::integer,
-    'null_frac',   0.5::real);
+    'schemaname', 'myschema',
+    'relname',    'mytable',
+    'attname',    'col1',
+    'inherited',  false,
+    'avg_width',  125::integer,
+    'null_frac',  0.5::real);
 </programlisting>
         </para>
         <para>
-         The required arguments are <literal>relation</literal> with a value
-         of type <type>regclass</type>, which specifies the table; either
+         The required arguments are <literal>schemaname</literal> with a value
+         of type <type>regclass</type> and <literal>relname</literal> with a value
+         of type <type>text</type> which specify the table; either
          <literal>attname</literal> with a value of type <type>name</type> or
          <literal>attnum</literal> with a value of type <type>smallint</type>,
          which specifies the column; and <literal>inherited</literal>, which
@@ -30461,7 +30465,8 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
           <primary>pg_clear_attribute_stats</primary>
          </indexterm>
          <function>pg_clear_attribute_stats</function> (
-         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>schemaname</parameter> <type>text</type>,
+         <parameter>relname</parameter> <type>text</type>,
          <parameter>attname</parameter> <type>name</type>,
          <parameter>inherited</parameter> <type>boolean</type> )
          <returnvalue>void</returnvalue>
-- 
2.48.1

#423Ashutosh Bapat
ashutosh.bapat.oss@gmail.com
In reply to: Jeff Davis (#421)
Re: Statistics Import and Export: difference in statistics dumped

On Tue, Mar 4, 2025 at 11:45 PM Jeff Davis <pgsql@j-davis.com> wrote:

On Tue, 2025-03-04 at 10:28 +0530, Ashutosh Bapat wrote:

What solution are you suggesting? The only one that comes to mind
is
moving everything to SECTION_POST_DATA, which is possible, but it
seems
like a big design change to satisfy a small detail.

We don't have to do that. We can manage it by making statistics of
index dependent upon the indexes on the table.

The index relstats are already dependent on the index definition. If
you have a simple database like:

CREATE TABLE t(i INT);
INSERT INTO t SELECT generate_series(1,10);
CREATE INDEX t_idx ON t (i);
ANALYZE;

and then you dump it, you get:

------- SECTION_PRE_DATA -------

CREATE TABLE public.t ...

------- SECTION_DATA -----------

COPY public.t (i) FROM stdin;
...
SELECT * FROM pg_catalog.pg_restore_relation_stats(
'version', '180000'::integer,
'relation', 'public.t'::regclass,
'relpages', '1'::integer,
'reltuples', '10'::real,
'relallvisible', '0'::integer
);
...

------- SECTION_POST_DATA ------

CREATE INDEX t_idx ON public.t USING btree (i);
SELECT * FROM pg_catalog.pg_restore_relation_stats(
'version', '180000'::integer,
'relation', 'public.t_idx'::regclass,
'relpages', '2'::integer,
'reltuples', '10'::real,
'relallvisible', '0'::integer
);

(section annotations added for clarity)

There is no problem with the index relstats, because they are already
dependent on the index definition, and will be restored after the
CREATE INDEX.

The issue is when the table's restored relstats are different from what
CREATE INDEX calculates, and then the CREATE INDEX overwrites the
table's just-restored relation stats. The easiest way to see this is
when restoring with --no-data, because CREATE INDEX will see an empty
table and overwrite the table's restored relstats with zeros.

If we view this issue as a dependency problem, then we'd have to make
the *table relstats* depend on the *index definition*. If a table has
any indexes, the relstats would need to go after the last index
definition, effectively moving most relstats to SECTION_POST_DATA. The
table's attribute stats would not be dependent on the index definition
(because CREATE INDEX doesn't touch those), so they could stay in
SECTION_DATA. And if the table doesn't have any indexes, then its
relstats could also stay in SECTION_DATA. But then we have a mess, so
we might as well just put all stats in SECTION_POST_DATA.

But I don't see it as a dependency problem. When I look at the above
SQL, it reads nicely to me and there's no obvious problem with it.

Thanks for explaining it. I

If we want stats to be stable, we need some kind of mode to tell the
server not to apply these kind of helpful optimizations, otherwise the
issue will resurface in some form no matter what we do with pg_dump. We
could invent a new mode, but autovacuum=off seems close enough to me.

Hmm. Updating the statistics without consuming more CPU is more
valuable when autovacuum is off it improves query plans with no extra
efforts. But if adding a new mode is some significant work, riding it
on top of autovacuum=off might ok. It's not documented either way, so
we could change that behaviour later if we find it troublesome.

--
Best Wishes,
Ashutosh Bapat

#424Andres Freund
andres@anarazel.de
In reply to: Corey Huinker (#370)
Re: Statistics Import and Export

Hi,

On 2025-02-25 21:29:56 -0500, Corey Huinker wrote:

On Tue, Feb 25, 2025 at 9:00 PM Jeff Davis <pgsql@j-davis.com> wrote:

On Mon, 2025-02-24 at 09:54 -0500, Andres Freund wrote:

Have you compared performance of with/without stats after these
optimizations?

On unoptimized build with asserts enabled, dumping the regression
database:

--no-statistics: 1.0s
master: 3.6s
v3j-0001: 3.0s
v3j-0002: 1.7s

I plan to commit the patches soon.

I think these have all been committed, but I still see a larger performance
difference than what you observed. I just checked because I was noticing that
the tests are still considerably slower than they used to be.

An optimized pg_dump against an unoptimized assert-enabled server:

time ./src/bin/pg_dump/pg_dump --no-data --quote-all-identifiers --binary-upgrade --format=custom --no-sync regression > /dev/null
real 0m2.778s
user 0m0.167s
sys 0m0.057s

$ time ./src/bin/pg_dump/pg_dump --no-data --quote-all-identifiers --binary-upgrade --format=custom --no-sync --no-statistics regression > /dev/null

real 0m1.290s
user 0m0.097s
sys 0m0.026s

I thought it might be interesting to look at the set of queries arriving on
the server side, so I enabled pg-stat_statements and ran a dump:

regression[4041753][1]=# SELECT total_exec_time, total_plan_time, calls, plans, substring(query, 1, 30) FROM pg_stat_statements ORDER BY calls DESC LIMIT 15;
┌─────────────────────┬─────────────────────┬───────┬───────┬────────────────────────────────┐
│ total_exec_time │ total_plan_time │ calls │ plans │ substring │
├─────────────────────┼─────────────────────┼───────┼───────┼────────────────────────────────┤
│ 239.9672189999998 │ 12.5725 │ 981 │ 6 │ PREPARE getAttributeStats(pg_c │
│ 15.330405000000004 │ 1.836712 │ 282 │ 6 │ PREPARE dumpFunc(pg_catalog.oi │
│ 10.129114000000003 │ 0.39834800000000004 │ 199 │ 6 │ PREPARE dumpTableAttach(pg_cat │
│ 9.887489000000002 │ 0.9332620000000001 │ 84 │ 84 │ SELECT pg_get_partkeydef($1) │
│ 14.350725000000006 │ 0.691071 │ 60 │ 60 │ SELECT pg_catalog.pg_get_viewd │
│ 5.1174219999999995 │ 1.4604219999999999 │ 47 │ 6 │ PREPARE dumpAgg(pg_catalog.oid │
│ 0.24036199999999996 │ 0.545125 │ 41 │ 41 │ SELECT pg_catalog.format_type( │
│ 7.099635000000002 │ 0.47031800000000007 │ 39 │ 39 │ SELECT pg_catalog.pg_get_ruled │
│ 0.672752 │ 1.9036320000000002 │ 21 │ 6 │ PREPARE dumpDomain(pg_catalog. │
│ 1.6519299999999997 │ 3.1480380000000006 │ 21 │ 22 │ PREPARE getDomainConstraints(p │
│ 1.085548 │ 3.9647630000000005 │ 16 │ 6 │ PREPARE dumpCompositeType(pg_c │
│ 0.196259 │ 0.602291 │ 11 │ 6 │ PREPARE dumpOpr(pg_catalog.oid │
│ 0.265461 │ 4.428914 │ 10 │ 10 │ SELECT amprocnum, amproc::pg_c │
│ 0.39591399999999993 │ 9.345471 │ 10 │ 10 │ SELECT amopstrategy, amopopr:: │
│ 0.35752100000000003 │ 2.128437 │ 9 │ 9 │ SELECT nspname, tmplname FROM │
└─────────────────────┴─────────────────────┴───────┴───────┴────────────────────────────────┘

It looks a lot less bad with an optimized build:
regression[4042057][1]=# SELECT total_exec_time, total_plan_time, calls, plans, substring(query, 1, 30) FROM pg_stat_statements ORDER BY calls DESC LIMIT 15;
┌─────────────────────┬─────────────────────┬───────┬───────┬────────────────────────────────┐
│ total_exec_time │ total_plan_time │ calls │ plans │ substring │
├─────────────────────┼─────────────────────┼───────┼───────┼────────────────────────────────┤
│ 50.63764299999999 │ 2.503585 │ 981 │ 6 │ PREPARE getAttributeStats(pg_c │
│ 3.5241990000000007 │ 0.478541 │ 282 │ 6 │ PREPARE dumpFunc(pg_catalog.oi │
│ 2.3170359999999985 │ 0.126379 │ 199 │ 6 │ PREPARE dumpTableAttach(pg_cat │
│ 2.291331 │ 0.25360400000000005 │ 84 │ 84 │ SELECT pg_get_partkeydef($1) │
│ 4.678433000000003 │ 0.202578 │ 60 │ 60 │ SELECT pg_catalog.pg_get_viewd │
│ 1.1288440000000004 │ 0.30976200000000004 │ 47 │ 6 │ PREPARE dumpAgg(pg_catalog.oid │
│ 0.06619 │ 0.16813600000000004 │ 41 │ 41 │ SELECT pg_catalog.format_type( │
│ 2.102865 │ 0.115169 │ 39 │ 39 │ SELECT pg_catalog.pg_get_ruled │
│ 0.16163 │ 0.439991 │ 21 │ 6 │ PREPARE dumpDomain(pg_catalog. │
│ 0.5335120000000001 │ 0.727573 │ 21 │ 22 │ PREPARE getDomainConstraints(p │
│ 0.28177 │ 0.894156 │ 16 │ 6 │ PREPARE dumpCompositeType(pg_c │
│ 0.038558 │ 0.140807 │ 11 │ 6 │ PREPARE dumpOpr(pg_catalog.oid │
│ 0.082078 │ 0.9654280000000001 │ 10 │ 10 │ SELECT amprocnum, amproc::pg_c │
│ 0.136964 │ 2.1140120000000002 │ 10 │ 10 │ SELECT amopstrategy, amopopr:: │
│ 0.11634699999999999 │ 0.48550499999999996 │ 9 │ 9 │ SELECT nspname, tmplname FROM │
└─────────────────────┴─────────────────────┴───────┴───────┴────────────────────────────────┘
(15 rows)

This isn't even *remotely* an adversarial case, there are lots of workloads
with folks have a handful of indexes on each table and many many tables.

Right now --statistics more than doubles the number of queries that pg_dump
issues. That's oviously noticeable locally, but it's going to be really
noticeable when dumping across the network.

I think we need to do more to lessen the impact. Even leaving regression test
performance aside, the time increase for the default pg_dump invocation will
be painful for folks, particularly due to this being enabled by default.

One fairly easy win would be to stop issuing getAttributeStats() for
non-expression indexes. In most cases that'll already drastically cut down on
the extra queries.

Greetings,

Andres Freund

#425Corey Huinker
corey.huinker@gmail.com
In reply to: Andres Freund (#424)
Re: Statistics Import and Export

One fairly easy win would be to stop issuing getAttributeStats() for
non-expression indexes. In most cases that'll already drastically cut down
on
the extra queries.

That does seem like an easy win, especially since we're already using
indexprs for some filters. I am concerned that, down the road, if we ever
start storing non-expression stats for, say, partial indexes, we would
overlook that a corresponding change needed to happen in pg_dump. If you
can think of any safeguards we can create for that, I'm listening.

#426Nathan Bossart
nathandbossart@gmail.com
In reply to: Andres Freund (#424)
Re: Statistics Import and Export

On Wed, Mar 05, 2025 at 08:17:53PM -0500, Andres Freund wrote:

Right now --statistics more than doubles the number of queries that pg_dump
issues. That's oviously noticeable locally, but it's going to be really
noticeable when dumping across the network.

I think we need to do more to lessen the impact. Even leaving regression test
performance aside, the time increase for the default pg_dump invocation will
be painful for folks, particularly due to this being enabled by default.

One fairly easy win would be to stop issuing getAttributeStats() for
non-expression indexes. In most cases that'll already drastically cut down on
the extra queries.

Apologies if this has already been considered upthread, but would it be
possible to use one query to gather all the required information into a
sorted table? At a glance, it looks to me like it might be feasible. I
had a lot of luck with reducing the number per-object queries with that
approach recently (e.g., commit 2329cad).

--
nathan

#427Corey Huinker
corey.huinker@gmail.com
In reply to: Nathan Bossart (#426)
Re: Statistics Import and Export

Apologies if this has already been considered upthread, but would it be
possible to use one query to gather all the required information into a
sorted table? At a glance, it looks to me like it might be feasible. I
had a lot of luck with reducing the number per-object queries with that
approach recently (e.g., commit 2329cad).

It's been considered and not ruled out, with a "let's see how the simple
thing works, first" approach. Considerations are:

* pg_stats is keyed on schemaname + tablename (which can also be indexes)
and we need to use that because of the security barrier
* Joining pg_class and pg_namespace to pg_stats was specifically singled
out as a thing to remove.
* The stats data is kinda heavy (most common value lists, most common
elements lists, esp for high stattargets), which would be a considerable
memory impact and some of those stats might not even be needed (example,
index stats for a table that is filtered out)

So it's not impossible, but it's trickier than just, say, tables or indexes.

#428Andres Freund
andres@anarazel.de
In reply to: Corey Huinker (#427)
Re: Statistics Import and Export

Hi,

On 2025-03-05 20:54:35 -0500, Corey Huinker wrote:

It's been considered and not ruled out, with a "let's see how the simple
thing works, first" approach. Considerations are:

* pg_stats is keyed on schemaname + tablename (which can also be indexes)
and we need to use that because of the security barrier

I don't think that has to be a big issue, you can just make the the query
query multiple tables at once using an = ANY(ARRAY[]) expression or such.

* The stats data is kinda heavy (most common value lists, most common
elements lists, esp for high stattargets), which would be a considerable
memory impact and some of those stats might not even be needed (example,
index stats for a table that is filtered out)

Doesn't the code currently have this problem already? Afaict the stats are
currently all stored in memory inside pg_dump.

$ for opt in '' --no-statistics; do echo "using option $opt"; for dbname in pgbench_part_100 pgbench_part_1000 pgbench_part_10000; do echo $dbname; /usr/bin/time -f 'Max RSS kB: %M' ./src/bin/pg_dump/pg_dump --no-data --quote-all-identifiers --no-sync --no-data $opt $dbname -Fp > /dev/null;done;done

using option
pgbench_part_100
Max RSS kB: 12780
pgbench_part_1000
Max RSS kB: 22700
pgbench_part_10000
Max RSS kB: 124224
using option --no-statistics
pgbench_part_100
Max RSS kB: 12648
pgbench_part_1000
Max RSS kB: 19124
pgbench_part_10000
Max RSS kB: 85068

I don't think the query itself would be a problem, a query querying all the
required stats should probably use PQsetSingleRowMode() or
PQsetChunkedRowsMode().

Greetings,

Andres Freund

#429Nathan Bossart
nathandbossart@gmail.com
In reply to: Corey Huinker (#427)
Re: Statistics Import and Export

On Wed, Mar 05, 2025 at 08:54:35PM -0500, Corey Huinker wrote:

* The stats data is kinda heavy (most common value lists, most common
elements lists, esp for high stattargets), which would be a considerable
memory impact and some of those stats might not even be needed (example,
index stats for a table that is filtered out)

Understood. Looking closer, I can see why that's a concern in this case.
You'd need 128 bytes just for the schema and table name.

--
nathan

#430Corey Huinker
corey.huinker@gmail.com
In reply to: Andres Freund (#428)
Re: Statistics Import and Export

On Wed, Mar 5, 2025 at 9:18 PM Andres Freund <andres@anarazel.de> wrote:

Hi,

On 2025-03-05 20:54:35 -0500, Corey Huinker wrote:

It's been considered and not ruled out, with a "let's see how the simple
thing works, first" approach. Considerations are:

* pg_stats is keyed on schemaname + tablename (which can also be indexes)
and we need to use that because of the security barrier

I don't think that has to be a big issue, you can just make the the query
query multiple tables at once using an = ANY(ARRAY[]) expression or such.

I'm uncertain how we'd do that with (schemaname,tablename) pairs. Are you
suggesting we back the joins from pg_stats to pg_namespace and pg_class and
then filter by oids?

* The stats data is kinda heavy (most common value lists, most common
elements lists, esp for high stattargets), which would be a considerable
memory impact and some of those stats might not even be needed (example,
index stats for a table that is filtered out)

Doesn't the code currently have this problem already? Afaict the stats are
currently all stored in memory inside pg_dump.

Each call to getAttributeStats() fetches the pg_stats for one and only one
relation and then writes the SQL call to fout, then discards the result set
once all the attributes of the relation are done.

I don't think the query itself would be a problem, a query querying all the

required stats should probably use PQsetSingleRowMode() or
PQsetChunkedRowsMode().

That makes sense if we get the attribute stats from the result set in the
order that we need them, and I don't know how we could possibly do that.
We'd still need a table to bsearch() and that would be huge.

#431Andres Freund
andres@anarazel.de
In reply to: Corey Huinker (#430)
Re: Statistics Import and Export

Hi,

On 2025-03-05 22:00:42 -0500, Corey Huinker wrote:

On Wed, Mar 5, 2025 at 9:18 PM Andres Freund <andres@anarazel.de> wrote:

On 2025-03-05 20:54:35 -0500, Corey Huinker wrote:

It's been considered and not ruled out, with a "let's see how the simple
thing works, first" approach. Considerations are:

* pg_stats is keyed on schemaname + tablename (which can also be indexes)
and we need to use that because of the security barrier

I don't think that has to be a big issue, you can just make the the query
query multiple tables at once using an = ANY(ARRAY[]) expression or such.

I'm uncertain how we'd do that with (schemaname,tablename) pairs. Are you
suggesting we back the joins from pg_stats to pg_namespace and pg_class and
then filter by oids?

I was thinking of one query per schema or something like that. But yea, a
query to pg_namespace and pg_class wouldn't be a problem if we did it far
fewer times than before. Or you could put the list of catalogs / tables to
be queried into an unnest() with two arrays or such.

Not sure how good the query plan for that would be, but it may be worth
looking at.

* The stats data is kinda heavy (most common value lists, most common
elements lists, esp for high stattargets), which would be a considerable
memory impact and some of those stats might not even be needed (example,
index stats for a table that is filtered out)

Doesn't the code currently have this problem already? Afaict the stats are
currently all stored in memory inside pg_dump.

Each call to getAttributeStats() fetches the pg_stats for one and only one
relation and then writes the SQL call to fout, then discards the result set
once all the attributes of the relation are done.

I don't think that's true. For one my example demonstrated that it increases
the peak memory usage substantially. That'd not be the case if the data was
just written out to stdout or such.

Looking at the code confirms that. The ArchiveEntry() in dumpRelationStats()
is never freed, afaict. And ArchiveEntry() strdups ->createStmt, which
contains the "SELECT pg_restore_attribute_stats(...)".

I don't think the query itself would be a problem, a query querying all the

required stats should probably use PQsetSingleRowMode() or
PQsetChunkedRowsMode().

That makes sense if we get the attribute stats from the result set in the
order that we need them, and I don't know how we could possibly do that.
We'd still need a table to bsearch() and that would be huge.

I'm not following - what would be the problem with a bsearch()? Compared to
the stats data an array to map from oid to an index in an array of stats data
data would be very small.

But with the unnest() idea from above it wouldn't even be needed, you could
use

SELECT ...
FROM unnest(schema_array, table_array) WITH ORDINALITY AS src(schemaname, tablename)
...
ORDER BY ordinality

or something along those lines.

Greetings,

Andres Freund

#432Corey Huinker
corey.huinker@gmail.com
In reply to: Andres Freund (#431)
4 attachment(s)
Re: Statistics Import and Export

I'm uncertain how we'd do that with (schemaname,tablename) pairs. Are you
suggesting we back the joins from pg_stats to pg_namespace and pg_class

and

then filter by oids?

I was thinking of one query per schema or something like that. But yea, a
query to pg_namespace and pg_class wouldn't be a problem if we did it far
fewer times than before. Or you could put the list of catalogs / tables
to
be queried into an unnest() with two arrays or such.

Not sure how good the query plan for that would be, but it may be worth
looking at.

Ok, so we're willing to take the pg_class/pg_namespace join hit for one or
a handful of queries, good to know.

Each call to getAttributeStats() fetches the pg_stats for one and only

one

relation and then writes the SQL call to fout, then discards the result

set

once all the attributes of the relation are done.

I don't think that's true. For one my example demonstrated that it
increases
the peak memory usage substantially. That'd not be the case if the data was
just written out to stdout or such.

Looking at the code confirms that. The ArchiveEntry() in
dumpRelationStats()
is never freed, afaict. And ArchiveEntry() strdups ->createStmt, which
contains the "SELECT pg_restore_attribute_stats(...)".

Pardon my inexperience, but aren't the ArchiveEntry records needed right up
until the program's run? If there's value in freeing them, why isn't it
being done already? What other thing would consume this freed memory?

I don't think the query itself would be a problem, a query querying all

the

required stats should probably use PQsetSingleRowMode() or
PQsetChunkedRowsMode().

That makes sense if we get the attribute stats from the result set in the
order that we need them, and I don't know how we could possibly do that.
We'd still need a table to bsearch() and that would be huge.

I'm not following - what would be the problem with a bsearch()? Compared to
the stats data an array to map from oid to an index in an array of stats
data
data would be very small.

If we can do oid bsearch lookups, then we might be in business, but even
then we have to maintain a data structure in memory of all pg_stats records
relevant to this dump, either in PGresult form, an intermediate data
structure like tblinfo/indxinfo, or in the resolved string of
pg_restore_attribute_stats() calls for that relation...which then get
strdup'd into the ArchiveEntry that we have to maintain anyway.

But with the unnest() idea from above it wouldn't even be needed, you could
use

SELECT ...
FROM unnest(schema_array, table_array) WITH ORDINALITY AS src(schemaname,
tablename)
...
ORDER BY ordinality

or something along those lines.

This still seems like there is some ability to generate a batch of these
rows and then discard them, and then go to the next logical batch (perhaps
by schema, as you suggested earlier), and I don't know that we have that
freedom. Perhaps we would have that freedom if stats were the absolute last
thing loaded in a dump.

Anyway, here's a rebased set of the existing up-for-consideration patches,
plus the optimization of avoiding querying on non-expression indexes.

I should add that this set presently doesn't include a patch that reverts
the set locale and strtof() call in favor of storing reltuples as a string.
As far as I know that idea is still on the table.

Attachments:

v6-0001-refactor-_tocEntryRequired-for-STATISTICS-DATA.patchtext/x-patch; charset=US-ASCII; name=v6-0001-refactor-_tocEntryRequired-for-STATISTICS-DATA.patchDownload
From 87ec2ad2f7dba7cce39a959ef02413be68caf4fb Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Tue, 25 Feb 2025 14:13:24 +0800
Subject: [PATCH v6 1/4] refactor _tocEntryRequired for STATISTICS DATA.

we don't dump STATISTICS DATA in --section=pre-data.
so in _tocEntryRequired, we evaulate "(strcmp(te->desc, "STATISTICS DATA") == 0)"
after ``switch (curSection)``.
---
 src/bin/pg_dump/pg_backup_archiver.c | 21 +++++++++------------
 1 file changed, 9 insertions(+), 12 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 632077113a4..0fc4ba04ba4 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2932,14 +2932,6 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 		strcmp(te->desc, "SEARCHPATH") == 0)
 		return REQ_SPECIAL;
 
-	if (strcmp(te->desc, "STATISTICS DATA") == 0)
-	{
-		if (!ropt->dumpStatistics)
-			return 0;
-		else
-			res = REQ_STATS;
-	}
-
 	/*
 	 * DATABASE and DATABASE PROPERTIES also have a special rule: they are
 	 * restored in createDB mode, and not restored otherwise, independently of
@@ -2984,10 +2976,6 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
-	/* If it's statistics and we don't want statistics, maybe ignore it */
-	if (!ropt->dumpStatistics && strcmp(te->desc, "STATISTICS DATA") == 0)
-		return 0;
-
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -3008,6 +2996,15 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 			return 0;
 	}
 
+	/* If it's statistics and we don't want statistics, ignore it */
+	if (strcmp(te->desc, "STATISTICS DATA") == 0)
+	{
+		if (!ropt->dumpStatistics)
+			return 0;
+		else
+			return REQ_STATS;
+	}
+
 	/* Ignore it if rejected by idWanted[] (cf. SortTocFromFile) */
 	if (ropt->idWanted && !ropt->idWanted[te->dumpId - 1])
 		return 0;

base-commit: 39de4f157d3ac0b889bb276c2487fe160578f967
-- 
2.48.1

v6-0002-Organize-and-deduplicate-statistics-import-tests.patchtext/x-patch; charset=US-ASCII; name=v6-0002-Organize-and-deduplicate-statistics-import-tests.patchDownload
From 2b4bce20e5e9f1b1fc68057e649dacb91f6bb6c9 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 26 Feb 2025 21:02:44 -0500
Subject: [PATCH v6 2/4] Organize and deduplicate statistics import tests.

Many changes, refactorings, and rebasings have taken their toll on the
statistics import tests. Now that things appear more stable and the
pg_set_* functions are gone in favor of using pg_restore_* in all cases,
it's safe to remove duplicates, combine tests where possible, and make
the test descriptions a bit more descriptive and uniform.

Additionally, parameters that were not strictly needed to demonstrate
the purpose(s) of a test were removed to reduce clutter.
---
 src/test/regress/expected/stats_import.out | 681 ++++++++-------------
 src/test/regress/sql/stats_import.sql      | 555 ++++++-----------
 2 files changed, 454 insertions(+), 782 deletions(-)

diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index 4df287e547f..ebba14c6a1d 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -13,19 +13,48 @@ CREATE TABLE stats_import.test(
     tags text[]
 ) WITH (autovacuum_enabled = false);
 CREATE INDEX test_i ON stats_import.test(id);
+--
+-- relstats tests
+--
+--- error: relation is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 0::oid,
+        'relpages', 17::integer);
+WARNING:  argument "relation" has type "oid", expected type "regclass"
+ERROR:  "relation" cannot be NULL
+-- error: relation not found
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 0::oid::regclass,
+        'relpages', 17::integer);
+ERROR:  could not open relation with OID 0
+-- error: odd number of variadic arguments cannot be pairs
+SELECT pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'relallvisible');
+ERROR:  variadic arguments must be name/value pairs
+HINT:  Provide an even number of variadic arguments that can be divided into pairs.
+-- error: argument name is NULL
+SELECT pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        NULL, '17'::integer);
+ERROR:  name at variadic position 3 is NULL
+-- error: argument name is not a text type
+SELECT pg_restore_relation_stats(
+        'relation', '0'::oid::regclass,
+        17, '17'::integer);
+ERROR:  name at variadic position 3 has type "integer", expected type "text"
 -- starting stats
 SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
+WHERE oid = 'stats_import.test_i'::regclass;
  relpages | reltuples | relallvisible | relallfrozen 
 ----------+-----------+---------------+--------------
-        0 |        -1 |             0 |            0
+        1 |         0 |             0 |            0
 (1 row)
 
-BEGIN;
 -- regular indexes have special case locking rules
-SELECT
-    pg_catalog.pg_restore_relation_stats(
+BEGIN;
+SELECT pg_catalog.pg_restore_relation_stats(
         'relation', 'stats_import.test_i'::regclass,
         'relpages', 18::integer);
  pg_restore_relation_stats 
@@ -50,32 +79,6 @@ WHERE relation = 'stats_import.test_i'::regclass AND
 (1 row)
 
 COMMIT;
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.test_i'::regclass,
-        'relpages', 19::integer );
- pg_restore_relation_stats 
----------------------------
- t
-(1 row)
-
--- clear
-SELECT
-    pg_catalog.pg_clear_relation_stats(
-        'stats_import.test'::regclass);
- pg_clear_relation_stats 
--------------------------
- 
-(1 row)
-
-SELECT relpages, reltuples, relallvisible, relallfrozen
-FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
- relpages | reltuples | relallvisible | relallfrozen 
-----------+-----------+---------------+--------------
-        0 |        -1 |             0 |            0
-(1 row)
-
 --  relpages may be -1 for partitioned tables
 CREATE TABLE stats_import.part_parent ( i integer ) PARTITION BY RANGE(i);
 CREATE TABLE stats_import.part_child_1
@@ -92,26 +95,6 @@ WHERE oid = 'stats_import.part_parent'::regclass;
        -1
 (1 row)
 
--- although partitioned tables have no storage, setting relpages to a
--- positive value is still allowed
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent_i'::regclass,
-        'relpages', 2::integer);
- pg_restore_relation_stats 
----------------------------
- t
-(1 row)
-
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent'::regclass,
-        'relpages', 2::integer);
- pg_restore_relation_stats 
----------------------------
- t
-(1 row)
-
 --
 -- Partitioned indexes aren't analyzed but it is possible to set
 -- stats. The locking rules are different from normal indexes due to
@@ -119,8 +102,7 @@ SELECT
 -- partitioned index are locked in ShareUpdateExclusive mode.
 --
 BEGIN;
-SELECT
-    pg_catalog.pg_restore_relation_stats(
+SELECT pg_catalog.pg_restore_relation_stats(
         'relation', 'stats_import.part_parent_i'::regclass,
         'relpages', 2::integer);
  pg_restore_relation_stats 
@@ -145,30 +127,19 @@ WHERE relation = 'stats_import.part_parent_i'::regclass AND
 (1 row)
 
 COMMIT;
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent_i'::regclass,
-        'relpages', 2::integer);
- pg_restore_relation_stats 
----------------------------
- t
+SELECT relpages
+FROM pg_class
+WHERE oid = 'stats_import.part_parent_i'::regclass;
+ relpages 
+----------
+        2
 (1 row)
 
--- nothing stops us from setting it to -1
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent'::regclass,
-        'relpages', -1::integer);
- pg_restore_relation_stats 
----------------------------
- t
-(1 row)
-
--- ok: set all stats
+-- ok: set all relstats, with version, no bounds checking
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
         'version', 150000::integer,
-        'relpages', '17'::integer,
+        'relpages', '-17'::integer,
         'reltuples', 400::real,
         'relallvisible', 4::integer,
         'relallfrozen', 2::integer);
@@ -182,13 +153,12 @@ FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
  relpages | reltuples | relallvisible | relallfrozen 
 ----------+-----------+---------------+--------------
-       17 |       400 |             4 |            2
+      -17 |       400 |             4 |            2
 (1 row)
 
--- ok: just relpages
+-- ok: set just relpages, rest stay same
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
-        'version', 150000::integer,
         'relpages', '16'::integer);
  pg_restore_relation_stats 
 ---------------------------
@@ -203,10 +173,9 @@ WHERE oid = 'stats_import.test'::regclass;
        16 |       400 |             4 |            2
 (1 row)
 
--- ok: just reltuples
+-- ok: set just reltuples, rest stay same
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
-        'version', 150000::integer,
         'reltuples', '500'::real);
  pg_restore_relation_stats 
 ---------------------------
@@ -221,10 +190,9 @@ WHERE oid = 'stats_import.test'::regclass;
        16 |       500 |             4 |            2
 (1 row)
 
--- ok: just relallvisible
+-- ok: set just relallvisible, rest stay same
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
-        'version', 150000::integer,
         'relallvisible', 5::integer);
  pg_restore_relation_stats 
 ---------------------------
@@ -257,10 +225,9 @@ WHERE oid = 'stats_import.test'::regclass;
        16 |       500 |             5 |            3
 (1 row)
 
--- warn: bad relpages type
+-- warn: bad relpages type, rest updated
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
-        'version', 150000::integer,
         'relpages', 'nope'::text,
         'reltuples', 400.0::real,
         'relallvisible', 4::integer,
@@ -279,20 +246,122 @@ WHERE oid = 'stats_import.test'::regclass;
        16 |       400 |             4 |            3
 (1 row)
 
+-- unrecognized argument name, rest ok
+SELECT pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'relpages', '171'::integer,
+        'nope', 10::integer);
+WARNING:  unrecognized argument name: "nope"
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+      171 |       400 |             4
+(1 row)
+
+-- ok: clear stats
+SELECT pg_catalog.pg_clear_relation_stats(
+    relation => 'stats_import.test'::regclass);
+ pg_clear_relation_stats 
+-------------------------
+ 
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+        0 |        -1 |             0
+(1 row)
+
 -- invalid relkinds for statistics
 CREATE SEQUENCE stats_import.testseq;
-CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
-SELECT
-    pg_catalog.pg_clear_relation_stats(
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.testseq'::regclass);
+ERROR:  cannot modify statistics for relation "testseq"
+DETAIL:  This operation is not supported for sequences.
+SELECT pg_catalog.pg_clear_relation_stats(
         'stats_import.testseq'::regclass);
 ERROR:  cannot modify statistics for relation "testseq"
 DETAIL:  This operation is not supported for sequences.
-SELECT
-    pg_catalog.pg_clear_relation_stats(
+CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.testview'::regclass);
+ERROR:  cannot modify statistics for relation "testview"
+DETAIL:  This operation is not supported for views.
+SELECT pg_catalog.pg_clear_relation_stats(
         'stats_import.testview'::regclass);
 ERROR:  cannot modify statistics for relation "testview"
 DETAIL:  This operation is not supported for views.
--- ok: no stakinds
+--
+-- attribute stats
+--
+-- error: object does not exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', '0'::oid::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+ERROR:  could not open relation with OID 0
+-- error: relation null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', NULL::oid::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+ERROR:  "relation" cannot be NULL
+-- error: NULL attname
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', NULL::name,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+ERROR:  must specify either attname or attnum
+-- error: attname doesn't exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'nope'::name,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ERROR:  column "nope" of relation "test" does not exist
+-- error: both attname and attnum
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'attnum', 1::smallint,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+ERROR:  cannot specify both attname and attnum
+-- error: neither attname nor attnum
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+ERROR:  must specify either attname or attnum
+-- error: attribute is system column
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'xmin'::name,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+ERROR:  cannot modify statistics on system column "xmin"
+-- error: inherited null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', NULL::boolean,
+    'null_frac', 0.1::real);
+ERROR:  "inherited" cannot be NULL
+-- ok: just the fixed values, with version, no stakinds
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
@@ -317,15 +386,16 @@ AND attname = 'id';
  stats_import | test      | id      | f         |       0.2 |         5 |        0.6 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- ok: restore by attnum
+--
+-- ok: restore by attnum, we normally reserve this for
+-- indexes, but there is no reason it shouldn't work
+-- for any stat-having relation.
+--
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attnum', 1::smallint,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.4::real,
-    'avg_width', 5::integer,
-    'n_distinct', 0.6::real);
+    'null_frac', 0.4::real);
  pg_restore_attribute_stats 
 ----------------------------
  t
@@ -342,14 +412,12 @@ AND attname = 'id';
  stats_import | test      | id      | f         |       0.4 |         5 |        0.6 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: unrecognized argument name
+-- warn: unrecognized argument name, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
     'null_frac', 0.2::real,
-    'avg_width', NULL::integer,
     'nope', 0.5::real);
 WARNING:  unrecognized argument name: "nope"
  pg_restore_attribute_stats 
@@ -368,15 +436,12 @@ AND attname = 'id';
  stats_import | test      | id      | f         |       0.2 |         5 |        0.6 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: mcv / mcf null mismatch part 1
+-- warn: mcv / mcf null mismatch part 1, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.6::real,
-    'avg_width', 7::integer,
-    'n_distinct', -0.7::real,
+    'null_frac', 0.21::real,
     'most_common_freqs', '{0.1,0.2,0.3}'::real[]
     );
 WARNING:  "most_common_vals" must be specified when "most_common_freqs" is specified
@@ -393,18 +458,15 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.6 |         7 |       -0.7 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.21 |         5 |        0.6 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: mcv / mcf null mismatch part 2
+-- warn: mcv / mcf null mismatch part 2, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.7::real,
-    'avg_width', 8::integer,
-    'n_distinct', -0.8::real,
+    'null_frac', 0.21::real,
     'most_common_vals', '{1,2,3}'::text
     );
 WARNING:  "most_common_freqs" must be specified when "most_common_vals" is specified
@@ -421,18 +483,15 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.7 |         8 |       -0.8 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.21 |         5 |        0.6 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: mcv / mcf type mismatch
+-- warn: mcf type mismatch, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.8::real,
-    'avg_width', 9::integer,
-    'n_distinct', -0.9::real,
+    'null_frac', 0.22::real,
     'most_common_vals', '{2,1,3}'::text,
     'most_common_freqs', '{0.2,0.1}'::double precision[]
     );
@@ -451,18 +510,15 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.8 |         9 |       -0.9 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.22 |         5 |        0.6 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: mcv cast failure
+-- warn: mcv cast failure, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.9::real,
-    'avg_width', 10::integer,
-    'n_distinct', -0.4::real,
+    'null_frac', 0.23::real,
     'most_common_vals', '{2,four,3}'::text,
     'most_common_freqs', '{0.3,0.25,0.05}'::real[]
     );
@@ -480,7 +536,7 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.9 |        10 |       -0.4 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.23 |         5 |        0.6 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
 -- ok: mcv+mcf
@@ -488,10 +544,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 1::integer,
-    'n_distinct', -0.1::real,
     'most_common_vals', '{2,1,3}'::text,
     'most_common_freqs', '{0.3,0.25,0.05}'::real[]
     );
@@ -508,18 +560,15 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.1 |         1 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.23 |         5 |        0.6 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: NULL in histogram array
+-- warn: NULL in histogram array, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.2::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.2::real,
+    'null_frac', 0.24::real,
     'histogram_bounds', '{1,NULL,3,4}'::text
     );
 WARNING:  "histogram_bounds" array cannot contain NULL values
@@ -536,7 +585,7 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.2 |         2 |       -0.2 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.24 |         5 |        0.6 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
 -- ok: histogram_bounds
@@ -544,11 +593,8 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.3::real,
-    'avg_width', 3::integer,
-    'n_distinct', -0.3::real,
-    'histogram_bounds', '{1,2,3,4}'::text );
+    'histogram_bounds', '{1,2,3,4}'::text
+    );
  pg_restore_attribute_stats 
 ----------------------------
  t
@@ -562,19 +608,16 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.3 |         3 |       -0.3 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.24 |         5 |        0.6 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: elem_count_histogram null element
+-- warn: elem_count_histogram null element, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.4::real,
-    'avg_width', 5::integer,
-    'n_distinct', -0.4::real,
-    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    'null_frac', 0.25::real,
+    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1}'::real[]
     );
 WARNING:  "elem_count_histogram" array cannot contain NULL values
  pg_restore_attribute_stats 
@@ -590,7 +633,7 @@ AND inherited = false
 AND attname = 'tags';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | tags    | f         |       0.4 |         5 |       -0.4 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | tags    | f         |      0.25 |         0 |          0 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
 -- ok: elem_count_histogram
@@ -598,10 +641,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.5::real,
-    'avg_width', 6::integer,
-    'n_distinct', -0.55::real,
+    'null_frac', 0.26::real,
     'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
     );
  pg_restore_attribute_stats 
@@ -617,18 +657,15 @@ AND inherited = false
 AND attname = 'tags';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
- stats_import | test      | tags    | f         |       0.5 |         6 |      -0.55 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+ stats_import | test      | tags    | f         |      0.26 |         0 |          0 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
 (1 row)
 
--- range stats on a scalar type
+-- warn: range stats on a scalar type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.6::real,
-    'avg_width', 7::integer,
-    'n_distinct', -0.15::real,
+    'null_frac', 0.27::real,
     'range_empty_frac', 0.5::real,
     'range_length_histogram', '{399,499,Infinity}'::text
     );
@@ -647,18 +684,15 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.6 |         7 |      -0.15 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.27 |         5 |        0.6 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: range_empty_frac range_length_hist null mismatch
+-- warn: range_empty_frac range_length_hist null mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.7::real,
-    'avg_width', 8::integer,
-    'n_distinct', -0.25::real,
+    'null_frac', 0.28::real,
     'range_length_histogram', '{399,499,Infinity}'::text
     );
 WARNING:  "range_empty_frac" must be specified when "range_length_histogram" is specified
@@ -675,18 +709,15 @@ AND inherited = false
 AND attname = 'arange';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | arange  | f         |       0.7 |         8 |      -0.25 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | arange  | f         |      0.28 |         0 |          0 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: range_empty_frac range_length_hist null mismatch part 2
+-- warn: range_empty_frac range_length_hist null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.8::real,
-    'avg_width', 9::integer,
-    'n_distinct', -0.35::real,
+    'null_frac', 0.29::real,
     'range_empty_frac', 0.5::real
     );
 WARNING:  "range_length_histogram" must be specified when "range_empty_frac" is specified
@@ -703,7 +734,7 @@ AND inherited = false
 AND attname = 'arange';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | arange  | f         |       0.8 |         9 |      -0.35 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | arange  | f         |      0.29 |         0 |          0 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
 (1 row)
 
 -- ok: range_empty_frac + range_length_hist
@@ -711,10 +742,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.9::real,
-    'avg_width', 1::integer,
-    'n_distinct', -0.19::real,
     'range_empty_frac', 0.5::real,
     'range_length_histogram', '{399,499,Infinity}'::text
     );
@@ -731,18 +758,15 @@ AND inherited = false
 AND attname = 'arange';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | arange  | f         |       0.9 |         1 |      -0.19 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
+ stats_import | test      | arange  | f         |      0.29 |         0 |          0 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
 (1 row)
 
--- warn: range bounds histogram on scalar
+-- warn: range bounds histogram on scalar, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.29::real,
+    'null_frac', 0.31::real,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
     );
 WARNING:  attribute "id" is not a range type
@@ -760,7 +784,7 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.1 |         2 |      -0.29 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.31 |         5 |        0.6 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
 (1 row)
 
 -- ok: range_bounds_histogram
@@ -768,10 +792,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.2::real,
-    'avg_width', 3::integer,
-    'n_distinct', -0.39::real,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
     );
  pg_restore_attribute_stats 
@@ -787,26 +807,17 @@ AND inherited = false
 AND attname = 'arange';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
- stats_import | test      | arange  | f         |       0.2 |         3 |      -0.39 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+ stats_import | test      | arange  | f         |      0.29 |         0 |          0 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
 (1 row)
 
--- warn: cannot set most_common_elems for range type
+-- warn: cannot set most_common_elems for range type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
-    'most_common_vals', '{"[2,3)","[1,2)","[3,4)"}'::text,
-    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
-    'histogram_bounds', '{"[1,2)","[2,3)","[3,4)","[4,5)"}'::text,
-    'correlation', 1.1::real,
+    'null_frac', 0.32::real,
     'most_common_elems', '{3,1}'::text,
-    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
-    'range_empty_frac', -0.5::real,
-    'range_length_histogram', '{399,499,Infinity}'::text,
-    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
     );
 WARNING:  unable to determine element type of attribute "arange"
 DETAIL:  Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.
@@ -821,19 +832,17 @@ WHERE schemaname = 'stats_import'
 AND tablename = 'test'
 AND inherited = false
 AND attname = 'arange';
-  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct |     most_common_vals      | most_common_freqs |         histogram_bounds          | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
---------------+-----------+---------+-----------+-----------+-----------+------------+---------------------------+-------------------+-----------------------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
- stats_import | test      | arange  | f         |       0.5 |         2 |       -0.1 | {"[2,3)","[1,2)","[3,4)"} | {0.3,0.25,0.05}   | {"[1,2)","[2,3)","[3,4)","[4,5)"} |         1.1 |                   |                        |                      | {399,499,Infinity}     |             -0.5 | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
+ stats_import | test      | arange  | f         |      0.32 |         0 |          0 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
 (1 row)
 
--- warn: scalars can't have mcelem
+-- warn: scalars can't have mcelem, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
+    'null_frac', 0.33::real,
     'most_common_elems', '{1,3}'::text,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
     );
@@ -852,17 +861,15 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+ stats_import | test      | id      | f         |      0.33 |         5 |        0.6 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
 (1 row)
 
--- warn: mcelem / mcelem mismatch
+-- warn: mcelem / mcelem mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
+    'null_frac', 0.34::real,
     'most_common_elems', '{one,two}'::text
     );
 WARNING:  "most_common_elem_freqs" must be specified when "most_common_elems" is specified
@@ -879,17 +886,15 @@ AND inherited = false
 AND attname = 'tags';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
- stats_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+ stats_import | test      | tags    | f         |      0.34 |         0 |          0 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
 (1 row)
 
--- warn: mcelem / mcelem null mismatch part 2
+-- warn: mcelem / mcelem null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
+    'null_frac', 0.35::real,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
     );
 WARNING:  "most_common_elems" must be specified when "most_common_elem_freqs" is specified
@@ -898,14 +903,22 @@ WARNING:  "most_common_elems" must be specified when "most_common_elem_freqs" is
  f
 (1 row)
 
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
+--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
+ stats_import | test      | tags    | f         |      0.35 |         0 |          0 |                  |                   |                  |             |                   |                        | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+(1 row)
+
 -- ok: mcelem
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
     'most_common_elems', '{one,three}'::text,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
     );
@@ -922,18 +935,16 @@ AND inherited = false
 AND attname = 'tags';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs |                                                                                            elem_count_histogram                                                                                             | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------+------------------+------------------------
- stats_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             | {one,three}       | {0.3,0.2,0.2,0.3,0}    | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
+ stats_import | test      | tags    | f         |      0.35 |         0 |          0 |                  |                   |                  |             | {one,three}       | {0.3,0.2,0.2,0.3,0}    | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} |                        |                  | 
 (1 row)
 
--- warn: scalars can't have elem_count_histogram
+-- warn: scalars can't have elem_count_histogram, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
-    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    'null_frac', 0.36::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1}'::real[]
     );
 WARNING:  unable to determine element type of attribute "id"
 DETAIL:  Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.
@@ -950,43 +961,7 @@ AND inherited = false
 AND attname = 'id';
   schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram 
 --------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
- stats_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
-(1 row)
-
--- warn: too many stat kinds
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
-    'most_common_vals', '{"[2,3)","[1,3)","[3,9)"}'::text,
-    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
-    'histogram_bounds', '{"[1,2)","[2,3)","[3,4)","[4,)"}'::text,
-    'correlation', 1.1::real,
-    'most_common_elems', '{3,1}'::text,
-    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
-    'range_empty_frac', -0.5::real,
-    'range_length_histogram', '{399,499,Infinity}'::text,
-    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text);
-WARNING:  unable to determine element type of attribute "arange"
-DETAIL:  Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.
- pg_restore_attribute_stats 
-----------------------------
- f
-(1 row)
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'arange';
-  schemaname  | tablename | attname | inherited | null_frac | avg_width | n_distinct |     most_common_vals      | most_common_freqs |         histogram_bounds         | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac |        range_bounds_histogram        
---------------+-----------+---------+-----------+-----------+-----------+------------+---------------------------+-------------------+----------------------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+--------------------------------------
- stats_import | test      | arange  | f         |       0.5 |         2 |       -0.1 | {"[2,3)","[1,3)","[3,9)"} | {0.3,0.25,0.05}   | {"[1,2)","[2,3)","[3,4)","[4,)"} |         1.1 |                   |                        |                      | {399,499,Infinity}     |             -0.5 | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+ stats_import | test      | id      | f         |      0.36 |         5 |        0.6 | {2,1,3}          | {0.3,0.25,0.05}   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
 (1 row)
 
 --
@@ -1006,20 +981,6 @@ SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import.complex_type,
 UNION ALL
 SELECT 4, 'four', NULL, int4range(0,100), NULL;
 CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
--- restoring stats on index
-SELECT * FROM pg_catalog.pg_restore_relation_stats(
-    'relation', 'stats_import.is_odd'::regclass,
-    'version', '180000'::integer,
-    'relpages', '11'::integer,
-    'reltuples', '10000'::real,
-    'relallvisible', '0'::integer,
-    'relallfrozen', '0'::integer
-);
- pg_restore_relation_stats 
----------------------------
- t
-(1 row)
-
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 CREATE TABLE stats_import.test_clone ( LIKE stats_import.test )
@@ -1197,7 +1158,18 @@ WHERE s.starelid = 'stats_import.is_odd'::regclass;
 ---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
 (0 rows)
 
--- ok
+-- attribute stats exist before a clear, but not after
+SELECT COUNT(*)
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+ count 
+-------
+     1
+(1 row)
+
 SELECT pg_catalog.pg_clear_attribute_stats(
     relation => 'stats_import.test'::regclass,
     attname => 'arange'::name,
@@ -1207,160 +1179,17 @@ SELECT pg_catalog.pg_clear_attribute_stats(
  
 (1 row)
 
---
--- Negative tests
---
---- error: relation is wrong type
-SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::oid,
-        'relpages', 17::integer,
-        'reltuples', 400.0::real,
-        'relallvisible', 4::integer,
-        'relallfrozen', 3::integer);
-WARNING:  argument "relation" has type "oid", expected type "regclass"
-ERROR:  "relation" cannot be NULL
---- error: relation not found
-SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::regclass,
-        'relpages', 17::integer,
-        'reltuples', 400.0::real,
-        'relallvisible', 4::integer,
-        'relallfrozen', 3::integer);
-ERROR:  could not open relation with OID 0
--- warn and error: unrecognized argument name
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        'relpages', '17'::integer,
-        'reltuples', 400::real,
-        'nope', 4::integer);
-WARNING:  unrecognized argument name: "nope"
-ERROR:  could not open relation with OID 0
--- error: argument name is NULL
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        NULL, '17'::integer,
-        'reltuples', 400::real,
-        'relallvisible', 4::integer,
-        'relallfrozen', 3::integer);
-ERROR:  name at variadic position 5 is NULL
--- error: argument name is an integer
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        17, '17'::integer,
-        'reltuples', 400::real,
-        'relallvisible', 4::integer,
-        'relallfrozen', 3::integer);
-ERROR:  name at variadic position 5 has type "integer", expected type "text"
--- error: odd number of variadic arguments cannot be pairs
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        'relpages', '17'::integer,
-        'reltuples', 400::real,
-        'relallfrozen', 3::integer,
-        'relallvisible');
-ERROR:  variadic arguments must be name/value pairs
-HINT:  Provide an even number of variadic arguments that can be divided into pairs.
--- error: object doesn't exist
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        'relpages', '17'::integer,
-        'reltuples', 400::real,
-        'relallvisible', 4::integer,
-        'relallfrozen', 3::integer);
-ERROR:  could not open relation with OID 0
--- error: object does not exist
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', '0'::oid::regclass,
-    'attname', 'id'::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-ERROR:  could not open relation with OID 0
--- error: relation null
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', NULL::oid,
-    'attname', 'id'::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-ERROR:  "relation" cannot be NULL
--- error: missing attname
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', NULL::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-ERROR:  must specify either attname or attnum
--- error: both attname and attnum
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
-    'attnum', 1::smallint,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-ERROR:  cannot specify both attname and attnum
--- error: attname doesn't exist
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'nope'::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-ERROR:  column "nope" of relation "test" does not exist
--- error: attribute is system column
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'xmin'::name,
-    'inherited', false::boolean,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-ERROR:  cannot modify statistics on system column "xmin"
--- error: inherited null
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
-    'inherited', NULL::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-ERROR:  "inherited" cannot be NULL
--- error: relation not found
-SELECT pg_catalog.pg_clear_relation_stats(
-    relation => 'stats_import.nope'::regclass);
-ERROR:  relation "stats_import.nope" does not exist
-LINE 2:     relation => 'stats_import.nope'::regclass);
-                        ^
--- error: attribute is system column
-SELECT pg_catalog.pg_clear_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'ctid'::name,
-    inherited => false::boolean);
-ERROR:  cannot clear statistics on system column "ctid"
--- error: attname doesn't exist
-SELECT pg_catalog.pg_clear_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'nope'::name,
-    inherited => false::boolean);
-ERROR:  column "nope" of relation "test" does not exist
+SELECT COUNT(*)
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+ count 
+-------
+     0
+(1 row)
+
 DROP SCHEMA stats_import CASCADE;
 NOTICE:  drop cascades to 6 other objects
 DETAIL:  drop cascades to type stats_import.complex_type
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index febda3d18d9..8d04ff4f378 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -17,15 +17,43 @@ CREATE TABLE stats_import.test(
 
 CREATE INDEX test_i ON stats_import.test(id);
 
+--
+-- relstats tests
+--
+
+--- error: relation is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 0::oid,
+        'relpages', 17::integer);
+
+-- error: relation not found
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 0::oid::regclass,
+        'relpages', 17::integer);
+
+-- error: odd number of variadic arguments cannot be pairs
+SELECT pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'relallvisible');
+
+-- error: argument name is NULL
+SELECT pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        NULL, '17'::integer);
+
+-- error: argument name is not a text type
+SELECT pg_restore_relation_stats(
+        'relation', '0'::oid::regclass,
+        17, '17'::integer);
+
 -- starting stats
 SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
+WHERE oid = 'stats_import.test_i'::regclass;
 
-BEGIN;
 -- regular indexes have special case locking rules
-SELECT
-    pg_catalog.pg_restore_relation_stats(
+BEGIN;
+SELECT pg_catalog.pg_restore_relation_stats(
         'relation', 'stats_import.test_i'::regclass,
         'relpages', 18::integer);
 
@@ -39,20 +67,6 @@ WHERE relation = 'stats_import.test_i'::regclass AND
 
 COMMIT;
 
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.test_i'::regclass,
-        'relpages', 19::integer );
-
--- clear
-SELECT
-    pg_catalog.pg_clear_relation_stats(
-        'stats_import.test'::regclass);
-
-SELECT relpages, reltuples, relallvisible, relallfrozen
-FROM pg_class
-WHERE oid = 'stats_import.test'::regclass;
-
 --  relpages may be -1 for partitioned tables
 CREATE TABLE stats_import.part_parent ( i integer ) PARTITION BY RANGE(i);
 CREATE TABLE stats_import.part_child_1
@@ -68,18 +82,6 @@ SELECT relpages
 FROM pg_class
 WHERE oid = 'stats_import.part_parent'::regclass;
 
--- although partitioned tables have no storage, setting relpages to a
--- positive value is still allowed
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent_i'::regclass,
-        'relpages', 2::integer);
-
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent'::regclass,
-        'relpages', 2::integer);
-
 --
 -- Partitioned indexes aren't analyzed but it is possible to set
 -- stats. The locking rules are different from normal indexes due to
@@ -88,8 +90,7 @@ SELECT
 --
 BEGIN;
 
-SELECT
-    pg_catalog.pg_restore_relation_stats(
+SELECT pg_catalog.pg_restore_relation_stats(
         'relation', 'stats_import.part_parent_i'::regclass,
         'relpages', 2::integer);
 
@@ -103,22 +104,15 @@ WHERE relation = 'stats_import.part_parent_i'::regclass AND
 
 COMMIT;
 
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent_i'::regclass,
-        'relpages', 2::integer);
+SELECT relpages
+FROM pg_class
+WHERE oid = 'stats_import.part_parent_i'::regclass;
 
--- nothing stops us from setting it to -1
-SELECT
-    pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent'::regclass,
-        'relpages', -1::integer);
-
--- ok: set all stats
+-- ok: set all relstats, with version, no bounds checking
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
         'version', 150000::integer,
-        'relpages', '17'::integer,
+        'relpages', '-17'::integer,
         'reltuples', 400::real,
         'relallvisible', 4::integer,
         'relallfrozen', 2::integer);
@@ -127,30 +121,27 @@ SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
--- ok: just relpages
+-- ok: set just relpages, rest stay same
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
-        'version', 150000::integer,
         'relpages', '16'::integer);
 
 SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
--- ok: just reltuples
+-- ok: set just reltuples, rest stay same
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
-        'version', 150000::integer,
         'reltuples', '500'::real);
 
 SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
--- ok: just relallvisible
+-- ok: set just relallvisible, rest stay same
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
-        'version', 150000::integer,
         'relallvisible', 5::integer);
 
 SELECT relpages, reltuples, relallvisible, relallfrozen
@@ -167,10 +158,9 @@ SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
--- warn: bad relpages type
+-- warn: bad relpages type, rest updated
 SELECT pg_restore_relation_stats(
         'relation', 'stats_import.test'::regclass,
-        'version', 150000::integer,
         'relpages', 'nope'::text,
         'reltuples', 400.0::real,
         'relallvisible', 4::integer,
@@ -180,17 +170,104 @@ SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
+-- unrecognized argument name, rest ok
+SELECT pg_restore_relation_stats(
+        'relation', 'stats_import.test'::regclass,
+        'relpages', '171'::integer,
+        'nope', 10::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
+-- ok: clear stats
+SELECT pg_catalog.pg_clear_relation_stats(
+    relation => 'stats_import.test'::regclass);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_import.test'::regclass;
+
 -- invalid relkinds for statistics
 CREATE SEQUENCE stats_import.testseq;
-CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
-SELECT
-    pg_catalog.pg_clear_relation_stats(
+
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.testseq'::regclass);
+
+SELECT pg_catalog.pg_clear_relation_stats(
         'stats_import.testseq'::regclass);
-SELECT
-    pg_catalog.pg_clear_relation_stats(
+
+CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
+
+SELECT pg_catalog.pg_restore_relation_stats(
+        'relation', 'stats_import.testview'::regclass);
+
+SELECT pg_catalog.pg_clear_relation_stats(
         'stats_import.testview'::regclass);
 
--- ok: no stakinds
+--
+-- attribute stats
+--
+
+-- error: object does not exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', '0'::oid::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: relation null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', NULL::oid::regclass,
+    'attname', 'id'::name,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: NULL attname
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', NULL::name,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: attname doesn't exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'nope'::name,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: both attname and attnum
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'attnum', 1::smallint,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: neither attname nor attnum
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: attribute is system column
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'xmin'::name,
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: inherited null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relation', 'stats_import.test'::regclass,
+    'attname', 'id'::name,
+    'inherited', NULL::boolean,
+    'null_frac', 0.1::real);
+
+-- ok: just the fixed values, with version, no stakinds
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
@@ -207,15 +284,16 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- ok: restore by attnum
+--
+-- ok: restore by attnum, we normally reserve this for
+-- indexes, but there is no reason it shouldn't work
+-- for any stat-having relation.
+--
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attnum', 1::smallint,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.4::real,
-    'avg_width', 5::integer,
-    'n_distinct', 0.6::real);
+    'null_frac', 0.4::real);
 
 SELECT *
 FROM pg_stats
@@ -224,14 +302,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: unrecognized argument name
+-- warn: unrecognized argument name, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
     'null_frac', 0.2::real,
-    'avg_width', NULL::integer,
     'nope', 0.5::real);
 
 SELECT *
@@ -241,15 +317,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: mcv / mcf null mismatch part 1
+-- warn: mcv / mcf null mismatch part 1, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.6::real,
-    'avg_width', 7::integer,
-    'n_distinct', -0.7::real,
+    'null_frac', 0.21::real,
     'most_common_freqs', '{0.1,0.2,0.3}'::real[]
     );
 
@@ -260,15 +333,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: mcv / mcf null mismatch part 2
+-- warn: mcv / mcf null mismatch part 2, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.7::real,
-    'avg_width', 8::integer,
-    'n_distinct', -0.8::real,
+    'null_frac', 0.21::real,
     'most_common_vals', '{1,2,3}'::text
     );
 
@@ -279,15 +349,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: mcv / mcf type mismatch
+-- warn: mcf type mismatch, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.8::real,
-    'avg_width', 9::integer,
-    'n_distinct', -0.9::real,
+    'null_frac', 0.22::real,
     'most_common_vals', '{2,1,3}'::text,
     'most_common_freqs', '{0.2,0.1}'::double precision[]
     );
@@ -299,15 +366,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: mcv cast failure
+-- warn: mcv cast failure, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.9::real,
-    'avg_width', 10::integer,
-    'n_distinct', -0.4::real,
+    'null_frac', 0.23::real,
     'most_common_vals', '{2,four,3}'::text,
     'most_common_freqs', '{0.3,0.25,0.05}'::real[]
     );
@@ -324,10 +388,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 1::integer,
-    'n_distinct', -0.1::real,
     'most_common_vals', '{2,1,3}'::text,
     'most_common_freqs', '{0.3,0.25,0.05}'::real[]
     );
@@ -339,15 +399,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: NULL in histogram array
+-- warn: NULL in histogram array, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.2::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.2::real,
+    'null_frac', 0.24::real,
     'histogram_bounds', '{1,NULL,3,4}'::text
     );
 
@@ -363,11 +420,8 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.3::real,
-    'avg_width', 3::integer,
-    'n_distinct', -0.3::real,
-    'histogram_bounds', '{1,2,3,4}'::text );
+    'histogram_bounds', '{1,2,3,4}'::text
+    );
 
 SELECT *
 FROM pg_stats
@@ -376,16 +430,13 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: elem_count_histogram null element
+-- warn: elem_count_histogram null element, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.4::real,
-    'avg_width', 5::integer,
-    'n_distinct', -0.4::real,
-    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    'null_frac', 0.25::real,
+    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1}'::real[]
     );
 
 SELECT *
@@ -400,10 +451,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.5::real,
-    'avg_width', 6::integer,
-    'n_distinct', -0.55::real,
+    'null_frac', 0.26::real,
     'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
     );
 
@@ -414,15 +462,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'tags';
 
--- range stats on a scalar type
+-- warn: range stats on a scalar type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.6::real,
-    'avg_width', 7::integer,
-    'n_distinct', -0.15::real,
+    'null_frac', 0.27::real,
     'range_empty_frac', 0.5::real,
     'range_length_histogram', '{399,499,Infinity}'::text
     );
@@ -434,15 +479,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: range_empty_frac range_length_hist null mismatch
+-- warn: range_empty_frac range_length_hist null mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.7::real,
-    'avg_width', 8::integer,
-    'n_distinct', -0.25::real,
+    'null_frac', 0.28::real,
     'range_length_histogram', '{399,499,Infinity}'::text
     );
 
@@ -453,15 +495,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'arange';
 
--- warn: range_empty_frac range_length_hist null mismatch part 2
+-- warn: range_empty_frac range_length_hist null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.8::real,
-    'avg_width', 9::integer,
-    'n_distinct', -0.35::real,
+    'null_frac', 0.29::real,
     'range_empty_frac', 0.5::real
     );
 
@@ -477,10 +516,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.9::real,
-    'avg_width', 1::integer,
-    'n_distinct', -0.19::real,
     'range_empty_frac', 0.5::real,
     'range_length_histogram', '{399,499,Infinity}'::text
     );
@@ -492,15 +527,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'arange';
 
--- warn: range bounds histogram on scalar
+-- warn: range bounds histogram on scalar, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.29::real,
+    'null_frac', 0.31::real,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
     );
 
@@ -516,10 +548,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.2::real,
-    'avg_width', 3::integer,
-    'n_distinct', -0.39::real,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
     );
 
@@ -530,23 +558,14 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'arange';
 
--- warn: cannot set most_common_elems for range type
+-- warn: cannot set most_common_elems for range type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'arange'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
-    'most_common_vals', '{"[2,3)","[1,2)","[3,4)"}'::text,
-    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
-    'histogram_bounds', '{"[1,2)","[2,3)","[3,4)","[4,5)"}'::text,
-    'correlation', 1.1::real,
+    'null_frac', 0.32::real,
     'most_common_elems', '{3,1}'::text,
-    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
-    'range_empty_frac', -0.5::real,
-    'range_length_histogram', '{399,499,Infinity}'::text,
-    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
     );
 
 SELECT *
@@ -556,14 +575,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'arange';
 
--- warn: scalars can't have mcelem
+-- warn: scalars can't have mcelem, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
+    'null_frac', 0.33::real,
     'most_common_elems', '{1,3}'::text,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
     );
@@ -575,14 +592,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: mcelem / mcelem mismatch
+-- warn: mcelem / mcelem mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
+    'null_frac', 0.34::real,
     'most_common_elems', '{one,two}'::text
     );
 
@@ -593,25 +608,27 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'tags';
 
--- warn: mcelem / mcelem null mismatch part 2
+-- warn: mcelem / mcelem null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
+    'null_frac', 0.35::real,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
     );
 
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
 -- ok: mcelem
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'tags'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
     'most_common_elems', '{one,three}'::text,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
     );
@@ -623,15 +640,13 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'tags';
 
--- warn: scalars can't have elem_count_histogram
+-- warn: scalars can't have elem_count_histogram, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relation', 'stats_import.test'::regclass,
     'attname', 'id'::name,
     'inherited', false::boolean,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
-    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    'null_frac', 0.36::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1}'::real[]
     );
 
 SELECT *
@@ -641,32 +656,6 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
--- warn: too many stat kinds
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.5::real,
-    'avg_width', 2::integer,
-    'n_distinct', -0.1::real,
-    'most_common_vals', '{"[2,3)","[1,3)","[3,9)"}'::text,
-    'most_common_freqs', '{0.3,0.25,0.05}'::real[],
-    'histogram_bounds', '{"[1,2)","[2,3)","[3,4)","[4,)"}'::text,
-    'correlation', 1.1::real,
-    'most_common_elems', '{3,1}'::text,
-    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
-    'range_empty_frac', -0.5::real,
-    'range_length_histogram', '{399,499,Infinity}'::text,
-    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text);
-
-SELECT *
-FROM pg_stats
-WHERE schemaname = 'stats_import'
-AND tablename = 'test'
-AND inherited = false
-AND attname = 'arange';
-
 --
 -- Test the ability to exactly copy data from one table to an identical table,
 -- correctly reconstructing the stakind order as well as the staopN and
@@ -686,16 +675,6 @@ SELECT 4, 'four', NULL, int4range(0,100), NULL;
 
 CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
 
--- restoring stats on index
-SELECT * FROM pg_catalog.pg_restore_relation_stats(
-    'relation', 'stats_import.is_odd'::regclass,
-    'version', '180000'::integer,
-    'relpages', '11'::integer,
-    'reltuples', '10000'::real,
-    'relallvisible', '0'::integer,
-    'relallfrozen', '0'::integer
-);
-
 -- Generate statistics on table with data
 ANALYZE stats_import.test;
 
@@ -848,160 +827,24 @@ FROM pg_statistic s
 JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
 WHERE s.starelid = 'stats_import.is_odd'::regclass;
 
--- ok
+-- attribute stats exist before a clear, but not after
+SELECT COUNT(*)
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
 SELECT pg_catalog.pg_clear_attribute_stats(
     relation => 'stats_import.test'::regclass,
     attname => 'arange'::name,
     inherited => false::boolean);
 
---
--- Negative tests
---
-
---- error: relation is wrong type
-SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::oid,
-        'relpages', 17::integer,
-        'reltuples', 400.0::real,
-        'relallvisible', 4::integer,
-        'relallfrozen', 3::integer);
-
---- error: relation not found
-SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::regclass,
-        'relpages', 17::integer,
-        'reltuples', 400.0::real,
-        'relallvisible', 4::integer,
-        'relallfrozen', 3::integer);
-
--- warn and error: unrecognized argument name
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        'relpages', '17'::integer,
-        'reltuples', 400::real,
-        'nope', 4::integer);
-
--- error: argument name is NULL
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        NULL, '17'::integer,
-        'reltuples', 400::real,
-        'relallvisible', 4::integer,
-        'relallfrozen', 3::integer);
-
--- error: argument name is an integer
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        17, '17'::integer,
-        'reltuples', 400::real,
-        'relallvisible', 4::integer,
-        'relallfrozen', 3::integer);
-
--- error: odd number of variadic arguments cannot be pairs
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        'relpages', '17'::integer,
-        'reltuples', 400::real,
-        'relallfrozen', 3::integer,
-        'relallvisible');
-
--- error: object doesn't exist
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        'version', 150000::integer,
-        'relpages', '17'::integer,
-        'reltuples', 400::real,
-        'relallvisible', 4::integer,
-        'relallfrozen', 3::integer);
-
--- error: object does not exist
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', '0'::oid::regclass,
-    'attname', 'id'::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-
--- error: relation null
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', NULL::oid,
-    'attname', 'id'::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-
--- error: missing attname
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', NULL::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-
--- error: both attname and attnum
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
-    'attnum', 1::smallint,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-
--- error: attname doesn't exist
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'nope'::name,
-    'inherited', false::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-
--- error: attribute is system column
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'xmin'::name,
-    'inherited', false::boolean,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-
--- error: inherited null
-SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
-    'inherited', NULL::boolean,
-    'version', 150000::integer,
-    'null_frac', 0.1::real,
-    'avg_width', 2::integer,
-    'n_distinct', 0.3::real);
-
--- error: relation not found
-SELECT pg_catalog.pg_clear_relation_stats(
-    relation => 'stats_import.nope'::regclass);
-
--- error: attribute is system column
-SELECT pg_catalog.pg_clear_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'ctid'::name,
-    inherited => false::boolean);
-
--- error: attname doesn't exist
-SELECT pg_catalog.pg_clear_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'nope'::name,
-    inherited => false::boolean);
+SELECT COUNT(*)
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
 
 DROP SCHEMA stats_import CASCADE;
-- 
2.48.1

v6-0004-Avoid-getAttributeStats-call-for-indexes-without-.patchtext/x-patch; charset=US-ASCII; name=v6-0004-Avoid-getAttributeStats-call-for-indexes-without-.patchDownload
From 5ea8a35a1d6db14046bb2fb76dfcdddb1bcf9479 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 5 Mar 2025 22:40:40 -0500
Subject: [PATCH v6 4/4] Avoid getAttributeStats() call for indexes without an
 expression column.

---
 src/bin/pg_dump/pg_dump.c | 229 ++++++++++++++++++++------------------
 1 file changed, 122 insertions(+), 107 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 6cf2c7d1fe4..0cc2a65caa3 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -10487,7 +10487,6 @@ static void
 dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 {
 	const DumpableObject *dobj = &rsinfo->dobj;
-	PGresult   *res;
 	PQExpBuffer query;
 	PQExpBuffer out;
 	DumpId	   *deps = NULL;
@@ -10508,11 +10507,22 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 	int			i_range_length_histogram;
 	int			i_range_empty_frac;
 	int			i_range_bounds_histogram;
+	bool		can_have_attrstats = true;
 
 	/* nothing to do if we are not dumping statistics */
 	if (!fout->dopt->dumpStatistics)
 		return;
 
+	/*
+	 * Indexes can only have statistics for expression columns.
+	 */
+	if ((rsinfo->relkind == RELKIND_INDEX) ||
+		(rsinfo->relkind == RELKIND_PARTITIONED_INDEX))
+	{
+		if (rsinfo->nindAttNames == 0)
+			can_have_attrstats = false;
+	}
+
 	/* dependent on the relation definition, if doing schema */
 	if (fout->dopt->dumpSchema)
 	{
@@ -10573,127 +10583,132 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 	appendPQExpBuffer(out, "\t'relallvisible', '%d'::integer\n);\n",
 					  rsinfo->relallvisible);
 
-	/* fetch attribute stats */
-	appendPQExpBufferStr(query, "EXECUTE getAttributeStats(");
-	appendStringLiteralAH(query, dobj->namespace->dobj.name, fout);
-	appendPQExpBufferStr(query, ", ");
-	appendStringLiteralAH(query, dobj->name, fout);
-	appendPQExpBufferStr(query, ");");
-
-	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
-
-	i_attname = PQfnumber(res, "attname");
-	i_inherited = PQfnumber(res, "inherited");
-	i_null_frac = PQfnumber(res, "null_frac");
-	i_avg_width = PQfnumber(res, "avg_width");
-	i_n_distinct = PQfnumber(res, "n_distinct");
-	i_most_common_vals = PQfnumber(res, "most_common_vals");
-	i_most_common_freqs = PQfnumber(res, "most_common_freqs");
-	i_histogram_bounds = PQfnumber(res, "histogram_bounds");
-	i_correlation = PQfnumber(res, "correlation");
-	i_most_common_elems = PQfnumber(res, "most_common_elems");
-	i_most_common_elem_freqs = PQfnumber(res, "most_common_elem_freqs");
-	i_elem_count_histogram = PQfnumber(res, "elem_count_histogram");
-	i_range_length_histogram = PQfnumber(res, "range_length_histogram");
-	i_range_empty_frac = PQfnumber(res, "range_empty_frac");
-	i_range_bounds_histogram = PQfnumber(res, "range_bounds_histogram");
-
-	/* restore attribute stats */
-	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	if (can_have_attrstats)
 	{
-		const char *attname;
+		PGresult   *res;
 
-		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
-		appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
-						  fout->remoteVersion);
-		appendPQExpBufferStr(out, "\t'schemaname', ");
-		appendStringLiteralAH(out, rsinfo->dobj.namespace->dobj.name, fout);
-		appendPQExpBufferStr(out, ",\n\t'relname', ");
-		appendStringLiteralAH(out, rsinfo->dobj.name, fout);
+		/* fetch attribute stats */
+		appendPQExpBufferStr(query, "EXECUTE getAttributeStats(");
+		appendStringLiteralAH(query, dobj->namespace->dobj.name, fout);
+		appendPQExpBufferStr(query, ", ");
+		appendStringLiteralAH(query, dobj->name, fout);
+		appendPQExpBufferStr(query, ");");
 
-		if (PQgetisnull(res, rownum, i_attname))
-			pg_fatal("attname cannot be NULL");
-		attname = PQgetvalue(res, rownum, i_attname);
+		res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
 
-		/*
-		 * Indexes look up attname in indAttNames to derive attnum, all others
-		 * use attname directly.  We must specify attnum for indexes, since
-		 * their attnames are not necessarily stable across dump/reload.
-		 */
-		if (rsinfo->nindAttNames == 0)
+		i_attname = PQfnumber(res, "attname");
+		i_inherited = PQfnumber(res, "inherited");
+		i_null_frac = PQfnumber(res, "null_frac");
+		i_avg_width = PQfnumber(res, "avg_width");
+		i_n_distinct = PQfnumber(res, "n_distinct");
+		i_most_common_vals = PQfnumber(res, "most_common_vals");
+		i_most_common_freqs = PQfnumber(res, "most_common_freqs");
+		i_histogram_bounds = PQfnumber(res, "histogram_bounds");
+		i_correlation = PQfnumber(res, "correlation");
+		i_most_common_elems = PQfnumber(res, "most_common_elems");
+		i_most_common_elem_freqs = PQfnumber(res, "most_common_elem_freqs");
+		i_elem_count_histogram = PQfnumber(res, "elem_count_histogram");
+		i_range_length_histogram = PQfnumber(res, "range_length_histogram");
+		i_range_empty_frac = PQfnumber(res, "range_empty_frac");
+		i_range_bounds_histogram = PQfnumber(res, "range_bounds_histogram");
+
+		/* restore attribute stats */
+		for (int rownum = 0; rownum < PQntuples(res); rownum++)
 		{
-			appendPQExpBuffer(out, ",\n\t'attname', ");
-			appendStringLiteralAH(out, attname, fout);
-		}
-		else
-		{
-			bool		found = false;
+			const char *attname;
 
-			for (int i = 0; i < rsinfo->nindAttNames; i++)
+			appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+			appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
+							fout->remoteVersion);
+			appendPQExpBufferStr(out, "\t'schemaname', ");
+			appendStringLiteralAH(out, rsinfo->dobj.namespace->dobj.name, fout);
+			appendPQExpBufferStr(out, ",\n\t'relname', ");
+			appendStringLiteralAH(out, rsinfo->dobj.name, fout);
+
+			if (PQgetisnull(res, rownum, i_attname))
+				pg_fatal("attname cannot be NULL");
+			attname = PQgetvalue(res, rownum, i_attname);
+
+			/*
+			* Indexes look up attname in indAttNames to derive attnum, all others
+			* use attname directly.  We must specify attnum for indexes, since
+			* their attnames are not necessarily stable across dump/reload.
+			*/
+			if (rsinfo->nindAttNames == 0)
 			{
-				if (strcmp(attname, rsinfo->indAttNames[i]) == 0)
+				appendPQExpBuffer(out, ",\n\t'attname', ");
+				appendStringLiteralAH(out, attname, fout);
+			}
+			else
+			{
+				bool		found = false;
+
+				for (int i = 0; i < rsinfo->nindAttNames; i++)
 				{
-					appendPQExpBuffer(out, ",\n\t'attnum', '%d'::smallint",
-									  i + 1);
-					found = true;
-					break;
+					if (strcmp(attname, rsinfo->indAttNames[i]) == 0)
+					{
+						appendPQExpBuffer(out, ",\n\t'attnum', '%d'::smallint",
+										i + 1);
+						found = true;
+						break;
+					}
 				}
+
+				if (!found)
+					pg_fatal("could not find index attname \"%s\"", attname);
 			}
 
-			if (!found)
-				pg_fatal("could not find index attname \"%s\"", attname);
+			if (!PQgetisnull(res, rownum, i_inherited))
+				appendNamedArgument(out, fout, "inherited", "boolean",
+									PQgetvalue(res, rownum, i_inherited));
+			if (!PQgetisnull(res, rownum, i_null_frac))
+				appendNamedArgument(out, fout, "null_frac", "real",
+									PQgetvalue(res, rownum, i_null_frac));
+			if (!PQgetisnull(res, rownum, i_avg_width))
+				appendNamedArgument(out, fout, "avg_width", "integer",
+									PQgetvalue(res, rownum, i_avg_width));
+			if (!PQgetisnull(res, rownum, i_n_distinct))
+				appendNamedArgument(out, fout, "n_distinct", "real",
+									PQgetvalue(res, rownum, i_n_distinct));
+			if (!PQgetisnull(res, rownum, i_most_common_vals))
+				appendNamedArgument(out, fout, "most_common_vals", "text",
+									PQgetvalue(res, rownum, i_most_common_vals));
+			if (!PQgetisnull(res, rownum, i_most_common_freqs))
+				appendNamedArgument(out, fout, "most_common_freqs", "real[]",
+									PQgetvalue(res, rownum, i_most_common_freqs));
+			if (!PQgetisnull(res, rownum, i_histogram_bounds))
+				appendNamedArgument(out, fout, "histogram_bounds", "text",
+									PQgetvalue(res, rownum, i_histogram_bounds));
+			if (!PQgetisnull(res, rownum, i_correlation))
+				appendNamedArgument(out, fout, "correlation", "real",
+									PQgetvalue(res, rownum, i_correlation));
+			if (!PQgetisnull(res, rownum, i_most_common_elems))
+				appendNamedArgument(out, fout, "most_common_elems", "text",
+									PQgetvalue(res, rownum, i_most_common_elems));
+			if (!PQgetisnull(res, rownum, i_most_common_elem_freqs))
+				appendNamedArgument(out, fout, "most_common_elem_freqs", "real[]",
+									PQgetvalue(res, rownum, i_most_common_elem_freqs));
+			if (!PQgetisnull(res, rownum, i_elem_count_histogram))
+				appendNamedArgument(out, fout, "elem_count_histogram", "real[]",
+									PQgetvalue(res, rownum, i_elem_count_histogram));
+			if (fout->remoteVersion >= 170000)
+			{
+				if (!PQgetisnull(res, rownum, i_range_length_histogram))
+					appendNamedArgument(out, fout, "range_length_histogram", "text",
+										PQgetvalue(res, rownum, i_range_length_histogram));
+				if (!PQgetisnull(res, rownum, i_range_empty_frac))
+					appendNamedArgument(out, fout, "range_empty_frac", "real",
+										PQgetvalue(res, rownum, i_range_empty_frac));
+				if (!PQgetisnull(res, rownum, i_range_bounds_histogram))
+					appendNamedArgument(out, fout, "range_bounds_histogram", "text",
+										PQgetvalue(res, rownum, i_range_bounds_histogram));
+			}
+			appendPQExpBufferStr(out, "\n);\n");
 		}
 
-		if (!PQgetisnull(res, rownum, i_inherited))
-			appendNamedArgument(out, fout, "inherited", "boolean",
-								PQgetvalue(res, rownum, i_inherited));
-		if (!PQgetisnull(res, rownum, i_null_frac))
-			appendNamedArgument(out, fout, "null_frac", "real",
-								PQgetvalue(res, rownum, i_null_frac));
-		if (!PQgetisnull(res, rownum, i_avg_width))
-			appendNamedArgument(out, fout, "avg_width", "integer",
-								PQgetvalue(res, rownum, i_avg_width));
-		if (!PQgetisnull(res, rownum, i_n_distinct))
-			appendNamedArgument(out, fout, "n_distinct", "real",
-								PQgetvalue(res, rownum, i_n_distinct));
-		if (!PQgetisnull(res, rownum, i_most_common_vals))
-			appendNamedArgument(out, fout, "most_common_vals", "text",
-								PQgetvalue(res, rownum, i_most_common_vals));
-		if (!PQgetisnull(res, rownum, i_most_common_freqs))
-			appendNamedArgument(out, fout, "most_common_freqs", "real[]",
-								PQgetvalue(res, rownum, i_most_common_freqs));
-		if (!PQgetisnull(res, rownum, i_histogram_bounds))
-			appendNamedArgument(out, fout, "histogram_bounds", "text",
-								PQgetvalue(res, rownum, i_histogram_bounds));
-		if (!PQgetisnull(res, rownum, i_correlation))
-			appendNamedArgument(out, fout, "correlation", "real",
-								PQgetvalue(res, rownum, i_correlation));
-		if (!PQgetisnull(res, rownum, i_most_common_elems))
-			appendNamedArgument(out, fout, "most_common_elems", "text",
-								PQgetvalue(res, rownum, i_most_common_elems));
-		if (!PQgetisnull(res, rownum, i_most_common_elem_freqs))
-			appendNamedArgument(out, fout, "most_common_elem_freqs", "real[]",
-								PQgetvalue(res, rownum, i_most_common_elem_freqs));
-		if (!PQgetisnull(res, rownum, i_elem_count_histogram))
-			appendNamedArgument(out, fout, "elem_count_histogram", "real[]",
-								PQgetvalue(res, rownum, i_elem_count_histogram));
-		if (fout->remoteVersion >= 170000)
-		{
-			if (!PQgetisnull(res, rownum, i_range_length_histogram))
-				appendNamedArgument(out, fout, "range_length_histogram", "text",
-									PQgetvalue(res, rownum, i_range_length_histogram));
-			if (!PQgetisnull(res, rownum, i_range_empty_frac))
-				appendNamedArgument(out, fout, "range_empty_frac", "real",
-									PQgetvalue(res, rownum, i_range_empty_frac));
-			if (!PQgetisnull(res, rownum, i_range_bounds_histogram))
-				appendNamedArgument(out, fout, "range_bounds_histogram", "text",
-									PQgetvalue(res, rownum, i_range_bounds_histogram));
-		}
-		appendPQExpBufferStr(out, "\n);\n");
+		PQclear(res);
 	}
 
-	PQclear(res);
-
 	ArchiveEntry(fout, nilCatalogId, createDumpId(),
 				 ARCHIVE_OPTS(.tag = dobj->name,
 							  .namespace = dobj->namespace->dobj.name,
-- 
2.48.1

v6-0003-Split-relation-into-schemaname-and-relname.patchtext/x-patch; charset=US-ASCII; name=v6-0003-Split-relation-into-schemaname-and-relname.patchDownload
From cb22c5782c4652d76885cb8dd3a2753ccac01b24 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Tue, 4 Mar 2025 22:16:52 -0500
Subject: [PATCH v6 3/4] Split relation into schemaname and relname.

In order to further reduce potential error-failures in restores and
upgrades, replace the numerous casts of fully qualified relation names
into their schema+relname text components.

Further remove the ::name casts on attname and change the expected
datatype to text.
---
 src/include/catalog/pg_proc.dat            |   8 +-
 src/backend/statistics/attribute_stats.c   |  98 +++++--
 src/backend/statistics/relation_stats.c    |  70 +++--
 src/bin/pg_dump/pg_dump.c                  |  25 +-
 src/bin/pg_dump/t/002_pg_dump.pl           |   6 +-
 src/test/regress/expected/stats_import.out | 302 +++++++++++++--------
 src/test/regress/sql/stats_import.sql      | 271 +++++++++++-------
 doc/src/sgml/func.sgml                     |  41 +--
 8 files changed, 537 insertions(+), 284 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 134b3dd8689..53aa4bc4df3 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12424,8 +12424,8 @@
   descr => 'clear statistics on relation',
   proname => 'pg_clear_relation_stats', provolatile => 'v', proisstrict => 'f',
   proparallel => 'u', prorettype => 'void',
-  proargtypes => 'regclass',
-  proargnames => '{relation}',
+  proargtypes => 'text text',
+  proargnames => '{schemaname,relname}',
   prosrc => 'pg_clear_relation_stats' },
 { oid => '8461',
   descr => 'restore statistics on attribute',
@@ -12440,8 +12440,8 @@
   descr => 'clear statistics on attribute',
   proname => 'pg_clear_attribute_stats', provolatile => 'v', proisstrict => 'f',
   proparallel => 'u', prorettype => 'void',
-  proargtypes => 'regclass name bool',
-  proargnames => '{relation,attname,inherited}',
+  proargtypes => 'text text text bool',
+  proargnames => '{schemaname,relname,attname,inherited}',
   prosrc => 'pg_clear_attribute_stats' },
 
 # GiST stratnum implementations
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index 6bcbee0edba..65456a04ae5 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -19,6 +19,7 @@
 
 #include "access/heapam.h"
 #include "catalog/indexing.h"
+#include "catalog/namespace.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_operator.h"
 #include "nodes/nodeFuncs.h"
@@ -36,7 +37,8 @@
 
 enum attribute_stats_argnum
 {
-	ATTRELATION_ARG = 0,
+	ATTRELSCHEMA_ARG = 0,
+	ATTRELNAME_ARG,
 	ATTNAME_ARG,
 	ATTNUM_ARG,
 	INHERITED_ARG,
@@ -58,8 +60,9 @@ enum attribute_stats_argnum
 
 static struct StatsArgInfo attarginfo[] =
 {
-	[ATTRELATION_ARG] = {"relation", REGCLASSOID},
-	[ATTNAME_ARG] = {"attname", NAMEOID},
+	[ATTRELSCHEMA_ARG] = {"schemaname", TEXTOID},
+	[ATTRELNAME_ARG] = {"relname", TEXTOID},
+	[ATTNAME_ARG] = {"attname", TEXTOID},
 	[ATTNUM_ARG] = {"attnum", INT2OID},
 	[INHERITED_ARG] = {"inherited", BOOLOID},
 	[NULL_FRAC_ARG] = {"null_frac", FLOAT4OID},
@@ -80,7 +83,8 @@ static struct StatsArgInfo attarginfo[] =
 
 enum clear_attribute_stats_argnum
 {
-	C_ATTRELATION_ARG = 0,
+	C_ATTRELSCHEMA_ARG = 0,
+	C_ATTRELNAME_ARG,
 	C_ATTNAME_ARG,
 	C_INHERITED_ARG,
 	C_NUM_ATTRIBUTE_STATS_ARGS
@@ -88,8 +92,9 @@ enum clear_attribute_stats_argnum
 
 static struct StatsArgInfo cleararginfo[] =
 {
-	[C_ATTRELATION_ARG] = {"relation", REGCLASSOID},
-	[C_ATTNAME_ARG] = {"attname", NAMEOID},
+	[C_ATTRELSCHEMA_ARG] = {"relation", TEXTOID},
+	[C_ATTRELNAME_ARG] = {"relation", TEXTOID},
+	[C_ATTNAME_ARG] = {"attname", TEXTOID},
 	[C_INHERITED_ARG] = {"inherited", BOOLOID},
 	[C_NUM_ATTRIBUTE_STATS_ARGS] = {0}
 };
@@ -133,6 +138,9 @@ static void init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
 static bool
 attribute_statistics_update(FunctionCallInfo fcinfo)
 {
+	char	   *nspname;
+	Oid			nspoid;
+	char	   *relname;
 	Oid			reloid;
 	char	   *attname;
 	AttrNumber	attnum;
@@ -170,8 +178,28 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 	bool		result = true;
 
-	stats_check_required_arg(fcinfo, attarginfo, ATTRELATION_ARG);
-	reloid = PG_GETARG_OID(ATTRELATION_ARG);
+	stats_check_required_arg(fcinfo, attarginfo, ATTRELSCHEMA_ARG);
+	stats_check_required_arg(fcinfo, attarginfo, ATTRELNAME_ARG);
+
+	nspname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELSCHEMA_ARG));
+	nspoid = get_namespace_oid(nspname, true);
+	if (nspoid == InvalidOid)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("Namespace \"%s\" not found.", nspname)));
+		return false;
+	}
+
+	relname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELNAME_ARG));
+	reloid = get_relname_relid(relname, nspoid);
+	if (reloid == InvalidOid)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("Relation \"%s\".\"%s\" not found.", nspname, relname)));
+		return false;
+	}
 
 	if (RecoveryInProgress())
 		ereport(ERROR,
@@ -185,21 +213,18 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	/* user can specify either attname or attnum, but not both */
 	if (!PG_ARGISNULL(ATTNAME_ARG))
 	{
-		Name		attnamename;
-
 		if (!PG_ARGISNULL(ATTNUM_ARG))
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("cannot specify both attname and attnum")));
-		attnamename = PG_GETARG_NAME(ATTNAME_ARG);
-		attname = NameStr(*attnamename);
+		attname = TextDatumGetCString(PG_GETARG_DATUM(ATTNAME_ARG));
 		attnum = get_attnum(reloid, attname);
 		/* note that this test covers attisdropped cases too: */
 		if (attnum == InvalidAttrNumber)
 			ereport(ERROR,
 					(errcode(ERRCODE_UNDEFINED_COLUMN),
-					 errmsg("column \"%s\" of relation \"%s\" does not exist",
-							attname, get_rel_name(reloid))));
+					 errmsg("column \"%s\" of relation \"%s\".\"%s\" does not exist",
+							attname, nspname, relname)));
 	}
 	else if (!PG_ARGISNULL(ATTNUM_ARG))
 	{
@@ -210,8 +235,8 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 			!SearchSysCacheExistsAttName(reloid, attname))
 			ereport(ERROR,
 					(errcode(ERRCODE_UNDEFINED_COLUMN),
-					 errmsg("column %d of relation \"%s\" does not exist",
-							attnum, get_rel_name(reloid))));
+					 errmsg("column %d of relation \"%s\".\"%s\" does not exist",
+							attnum, nspname, relname)));
 	}
 	else
 	{
@@ -900,13 +925,38 @@ init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
 Datum
 pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 {
+	char	   *nspname;
+	Oid			nspoid;
+	char	   *relname;
 	Oid			reloid;
-	Name		attname;
+	char	   *attname;
 	AttrNumber	attnum;
 	bool		inherited;
 
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELATION_ARG);
-	reloid = PG_GETARG_OID(C_ATTRELATION_ARG);
+	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELSCHEMA_ARG);
+	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELNAME_ARG);
+	stats_check_required_arg(fcinfo, cleararginfo, C_ATTNAME_ARG);
+	stats_check_required_arg(fcinfo, cleararginfo, C_INHERITED_ARG);
+
+	nspname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELSCHEMA_ARG));
+	nspoid = get_namespace_oid(nspname, true);
+	if (nspoid == InvalidOid)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("Namespace \"%s\" not found.", nspname)));
+		return false;
+	}
+
+	relname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELNAME_ARG));
+	reloid = get_relname_relid(relname, nspoid);
+	if (reloid == InvalidOid)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("Relation \"%s\".\"%s\" not found.", nspname, relname)));
+		return false;
+	}
 
 	if (RecoveryInProgress())
 		ereport(ERROR,
@@ -916,23 +966,21 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 
 	stats_lock_check_privileges(reloid);
 
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTNAME_ARG);
-	attname = PG_GETARG_NAME(C_ATTNAME_ARG);
-	attnum = get_attnum(reloid, NameStr(*attname));
+	attname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTNAME_ARG));
+	attnum = get_attnum(reloid, attname);
 
 	if (attnum < 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot clear statistics on system column \"%s\"",
-						NameStr(*attname))));
+						attname)));
 
 	if (attnum == InvalidAttrNumber)
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 errmsg("column \"%s\" of relation \"%s\" does not exist",
-						NameStr(*attname), get_rel_name(reloid))));
+						attname, get_rel_name(reloid))));
 
-	stats_check_required_arg(fcinfo, cleararginfo, C_INHERITED_ARG);
 	inherited = PG_GETARG_BOOL(C_INHERITED_ARG);
 
 	delete_pg_statistic(reloid, attnum, inherited);
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index 2c1cea3fc80..4c1e75a3c78 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -19,9 +19,12 @@
 
 #include "access/heapam.h"
 #include "catalog/indexing.h"
+#include "catalog/namespace.h"
 #include "statistics/stat_utils.h"
+#include "utils/builtins.h"
 #include "utils/fmgroids.h"
 #include "utils/fmgrprotos.h"
+#include "utils/lsyscache.h"
 #include "utils/syscache.h"
 
 
@@ -32,7 +35,8 @@
 
 enum relation_stats_argnum
 {
-	RELATION_ARG = 0,
+	RELSCHEMA_ARG = 0,
+	RELNAME_ARG,
 	RELPAGES_ARG,
 	RELTUPLES_ARG,
 	RELALLVISIBLE_ARG,
@@ -42,7 +46,8 @@ enum relation_stats_argnum
 
 static struct StatsArgInfo relarginfo[] =
 {
-	[RELATION_ARG] = {"relation", REGCLASSOID},
+	[RELSCHEMA_ARG] = {"schemaname", TEXTOID},
+	[RELNAME_ARG] = {"relname", TEXTOID},
 	[RELPAGES_ARG] = {"relpages", INT4OID},
 	[RELTUPLES_ARG] = {"reltuples", FLOAT4OID},
 	[RELALLVISIBLE_ARG] = {"relallvisible", INT4OID},
@@ -59,6 +64,9 @@ static bool
 relation_statistics_update(FunctionCallInfo fcinfo)
 {
 	bool		result = true;
+	char	   *nspname;
+	Oid			nspoid;
+	char	   *relname;
 	Oid			reloid;
 	Relation	crel;
 	BlockNumber relpages = 0;
@@ -76,6 +84,37 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 	bool		nulls[4] = {0};
 	int			nreplaces = 0;
 
+	stats_check_required_arg(fcinfo, relarginfo, RELSCHEMA_ARG);
+	stats_check_required_arg(fcinfo, relarginfo, RELNAME_ARG);
+
+	nspname = TextDatumGetCString(PG_GETARG_DATUM(RELSCHEMA_ARG));
+	nspoid = get_namespace_oid(nspname, true);
+	if (nspoid == InvalidOid)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("Namespace \"%s\" not found.", nspname)));
+		return false;
+	}
+
+	relname = TextDatumGetCString(PG_GETARG_DATUM(RELNAME_ARG));
+	reloid = get_relname_relid(relname, nspoid);
+	if (reloid == InvalidOid)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("Relation \"%s\".\"%s\" not found.", nspname, relname)));
+		return false;
+	}
+
+	if (RecoveryInProgress())
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("recovery is in progress"),
+				 errhint("Statistics cannot be modified during recovery.")));
+
+	stats_lock_check_privileges(reloid);
+
 	if (!PG_ARGISNULL(RELPAGES_ARG))
 	{
 		relpages = PG_GETARG_UINT32(RELPAGES_ARG);
@@ -108,17 +147,6 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 		update_relallfrozen = true;
 	}
 
-	stats_check_required_arg(fcinfo, relarginfo, RELATION_ARG);
-	reloid = PG_GETARG_OID(RELATION_ARG);
-
-	if (RecoveryInProgress())
-		ereport(ERROR,
-				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
-				 errmsg("recovery is in progress"),
-				 errhint("Statistics cannot be modified during recovery.")));
-
-	stats_lock_check_privileges(reloid);
-
 	/*
 	 * Take RowExclusiveLock on pg_class, consistent with
 	 * vac_update_relstats().
@@ -193,20 +221,22 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 Datum
 pg_clear_relation_stats(PG_FUNCTION_ARGS)
 {
-	LOCAL_FCINFO(newfcinfo, 5);
+	LOCAL_FCINFO(newfcinfo, 6);
 
-	InitFunctionCallInfoData(*newfcinfo, NULL, 5, InvalidOid, NULL, NULL);
+	InitFunctionCallInfoData(*newfcinfo, NULL, 6, InvalidOid, NULL, NULL);
 
-	newfcinfo->args[0].value = PG_GETARG_OID(0);
+	newfcinfo->args[0].value = PG_GETARG_DATUM(0);
 	newfcinfo->args[0].isnull = PG_ARGISNULL(0);
-	newfcinfo->args[1].value = UInt32GetDatum(0);
-	newfcinfo->args[1].isnull = false;
-	newfcinfo->args[2].value = Float4GetDatum(-1.0);
+	newfcinfo->args[1].value = PG_GETARG_DATUM(1);
+	newfcinfo->args[1].isnull = PG_ARGISNULL(1);
+	newfcinfo->args[2].value = UInt32GetDatum(0);
 	newfcinfo->args[2].isnull = false;
-	newfcinfo->args[3].value = UInt32GetDatum(0);
+	newfcinfo->args[3].value = Float4GetDatum(-1.0);
 	newfcinfo->args[3].isnull = false;
 	newfcinfo->args[4].value = UInt32GetDatum(0);
 	newfcinfo->args[4].isnull = false;
+	newfcinfo->args[5].value = UInt32GetDatum(0);
+	newfcinfo->args[5].isnull = false;
 
 	relation_statistics_update(newfcinfo);
 	PG_RETURN_VOID();
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 4f4ad2ee150..6cf2c7d1fe4 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -10492,7 +10492,6 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 	PQExpBuffer out;
 	DumpId	   *deps = NULL;
 	int			ndeps = 0;
-	char	   *qualified_name;
 	char		reltuples_str[FLOAT_SHORTEST_DECIMAL_LEN];
 	int			i_attname;
 	int			i_inherited;
@@ -10558,15 +10557,16 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 
 	out = createPQExpBuffer();
 
-	qualified_name = pg_strdup(fmtQualifiedDumpable(rsinfo));
-
 	/* restore relation stats */
 	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
 	appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
 					  fout->remoteVersion);
-	appendPQExpBufferStr(out, "\t'relation', ");
-	appendStringLiteralAH(out, qualified_name, fout);
-	appendPQExpBufferStr(out, "::regclass,\n");
+	appendPQExpBufferStr(out, "\t'schemaname', ");
+	appendStringLiteralAH(out, rsinfo->dobj.namespace->dobj.name, fout);
+	appendPQExpBufferStr(out, ",\n");
+	appendPQExpBufferStr(out, "\t'relname', ");
+	appendStringLiteralAH(out, rsinfo->dobj.name, fout);
+	appendPQExpBufferStr(out, ",\n");
 	appendPQExpBuffer(out, "\t'relpages', '%d'::integer,\n", rsinfo->relpages);
 	float_to_shortest_decimal_buf(rsinfo->reltuples, reltuples_str);
 	appendPQExpBuffer(out, "\t'reltuples', '%s'::real,\n", reltuples_str);
@@ -10606,9 +10606,10 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
 		appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
 						  fout->remoteVersion);
-		appendPQExpBufferStr(out, "\t'relation', ");
-		appendStringLiteralAH(out, qualified_name, fout);
-		appendPQExpBufferStr(out, "::regclass");
+		appendPQExpBufferStr(out, "\t'schemaname', ");
+		appendStringLiteralAH(out, rsinfo->dobj.namespace->dobj.name, fout);
+		appendPQExpBufferStr(out, ",\n\t'relname', ");
+		appendStringLiteralAH(out, rsinfo->dobj.name, fout);
 
 		if (PQgetisnull(res, rownum, i_attname))
 			pg_fatal("attname cannot be NULL");
@@ -10620,7 +10621,10 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 		 * their attnames are not necessarily stable across dump/reload.
 		 */
 		if (rsinfo->nindAttNames == 0)
-			appendNamedArgument(out, fout, "attname", "name", attname);
+		{
+			appendPQExpBuffer(out, ",\n\t'attname', ");
+			appendStringLiteralAH(out, attname, fout);
+		}
 		else
 		{
 			bool		found = false;
@@ -10700,7 +10704,6 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 							  .deps = deps,
 							  .nDeps = ndeps));
 
-	free(qualified_name);
 	destroyPQExpBuffer(out);
 	destroyPQExpBuffer(query);
 }
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index c7bffc1b045..b037f239136 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -4725,14 +4725,16 @@ my %tests = (
 		regexp => qr/^
 			\QSELECT * FROM pg_catalog.pg_restore_relation_stats(\E\s+
 			'version',\s'\d+'::integer,\s+
-			'relation',\s'dump_test.dup_test_post_data_ix'::regclass,\s+
+			'schemaname',\s'dump_test',\s+
+			'relname',\s'dup_test_post_data_ix',\s+
 			'relpages',\s'\d+'::integer,\s+
 			'reltuples',\s'\d+'::real,\s+
 			'relallvisible',\s'\d+'::integer\s+
 			\);\s+
 			\QSELECT * FROM pg_catalog.pg_restore_attribute_stats(\E\s+
 			'version',\s'\d+'::integer,\s+
-			'relation',\s'dump_test.dup_test_post_data_ix'::regclass,\s+
+			'schemaname',\s'dump_test',\s+
+			'relname',\s'dup_test_post_data_ix',\s+
 			'attnum',\s'2'::smallint,\s+
 			'inherited',\s'f'::boolean,\s+
 			'null_frac',\s'0'::real,\s+
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index ebba14c6a1d..ca7fe39660e 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -16,33 +16,54 @@ CREATE INDEX test_i ON stats_import.test(id);
 --
 -- relstats tests
 --
---- error: relation is wrong type
+-- error: schemaname missing
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::oid,
+        'relname', 'test',
         'relpages', 17::integer);
-WARNING:  argument "relation" has type "oid", expected type "regclass"
-ERROR:  "relation" cannot be NULL
+ERROR:  "schemaname" cannot be NULL
+-- error: relname missing
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relpages', 17::integer);
+ERROR:  "relname" cannot be NULL
+--- error: schemaname is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 3.6::float,
+        'relname', 'test',
+        'relpages', 17::integer);
+WARNING:  argument "schemaname" has type "double precision", expected type "text"
+ERROR:  "schemaname" cannot be NULL
+--- error: relname is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relname', 0::oid,
+        'relpages', 17::integer);
+WARNING:  argument "relname" has type "oid", expected type "text"
+ERROR:  "relname" cannot be NULL
 -- error: relation not found
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::oid::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'nope',
         'relpages', 17::integer);
-ERROR:  could not open relation with OID 0
+WARNING:  Relation "stats_import"."nope" not found.
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
 -- error: odd number of variadic arguments cannot be pairs
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relallvisible');
 ERROR:  variadic arguments must be name/value pairs
 HINT:  Provide an even number of variadic arguments that can be divided into pairs.
 -- error: argument name is NULL
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         NULL, '17'::integer);
-ERROR:  name at variadic position 3 is NULL
--- error: argument name is not a text type
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        17, '17'::integer);
-ERROR:  name at variadic position 3 has type "integer", expected type "text"
+ERROR:  name at variadic position 5 is NULL
 -- starting stats
 SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
@@ -55,7 +76,8 @@ WHERE oid = 'stats_import.test_i'::regclass;
 -- regular indexes have special case locking rules
 BEGIN;
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.test_i'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test_i',
         'relpages', 18::integer);
  pg_restore_relation_stats 
 ---------------------------
@@ -103,7 +125,8 @@ WHERE oid = 'stats_import.part_parent'::regclass;
 --
 BEGIN;
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent_i'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'part_parent_i',
         'relpages', 2::integer);
  pg_restore_relation_stats 
 ---------------------------
@@ -137,7 +160,8 @@ WHERE oid = 'stats_import.part_parent_i'::regclass;
 
 -- ok: set all relstats, with version, no bounds checking
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'version', 150000::integer,
         'relpages', '-17'::integer,
         'reltuples', 400::real,
@@ -158,7 +182,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just relpages, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', '16'::integer);
  pg_restore_relation_stats 
 ---------------------------
@@ -175,7 +200,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just reltuples, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'reltuples', '500'::real);
  pg_restore_relation_stats 
 ---------------------------
@@ -192,7 +218,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just relallvisible, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relallvisible', 5::integer);
  pg_restore_relation_stats 
 ---------------------------
@@ -209,7 +236,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: just relallfrozen
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'version', 150000::integer,
         'relallfrozen', 3::integer);
  pg_restore_relation_stats 
@@ -227,7 +255,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- warn: bad relpages type, rest updated
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', 'nope'::text,
         'reltuples', 400.0::real,
         'relallvisible', 4::integer,
@@ -248,7 +277,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- unrecognized argument name, rest ok
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', '171'::integer,
         'nope', 10::integer);
 WARNING:  unrecognized argument name: "nope"
@@ -266,8 +296,7 @@ WHERE oid = 'stats_import.test'::regclass;
 (1 row)
 
 -- ok: clear stats
-SELECT pg_catalog.pg_clear_relation_stats(
-    relation => 'stats_import.test'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'test');
  pg_clear_relation_stats 
 -------------------------
  
@@ -284,87 +313,123 @@ WHERE oid = 'stats_import.test'::regclass;
 -- invalid relkinds for statistics
 CREATE SEQUENCE stats_import.testseq;
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.testseq'::regclass);
+        'schemaname', 'stats_import',
+        'relname', 'testseq');
 ERROR:  cannot modify statistics for relation "testseq"
 DETAIL:  This operation is not supported for sequences.
-SELECT pg_catalog.pg_clear_relation_stats(
-        'stats_import.testseq'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testseq');
 ERROR:  cannot modify statistics for relation "testseq"
 DETAIL:  This operation is not supported for sequences.
 CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
-SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.testview'::regclass);
-ERROR:  cannot modify statistics for relation "testview"
-DETAIL:  This operation is not supported for views.
-SELECT pg_catalog.pg_clear_relation_stats(
-        'stats_import.testview'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testview');
 ERROR:  cannot modify statistics for relation "testview"
 DETAIL:  This operation is not supported for views.
 --
 -- attribute stats
 --
--- error: object does not exist
+-- error: schemaname missing
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', '0'::oid::regclass,
-    'attname', 'id'::name,
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  could not open relation with OID 0
--- error: relation null
+ERROR:  "schemaname" cannot be NULL
+-- error: schema does not exist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', NULL::oid::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'nope',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  "relation" cannot be NULL
+WARNING:  Namespace "nope" not found.
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- error: relname missing
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+ERROR:  "relname" cannot be NULL
+-- error: relname does not exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'relname', 'nope',
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+WARNING:  Relation "stats_import"."nope" not found.
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- error: relname null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'relname', NULL,
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+ERROR:  "relname" cannot be NULL
 -- error: NULL attname
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', NULL::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', NULL,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 ERROR:  must specify either attname or attnum
 -- error: attname doesn't exist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'nope'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'nope',
     'inherited', false::boolean,
     'null_frac', 0.1::real,
     'avg_width', 2::integer,
     'n_distinct', 0.3::real);
-ERROR:  column "nope" of relation "test" does not exist
+ERROR:  column "nope" of relation "stats_import"."test" does not exist
 -- error: both attname and attnum
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'attnum', 1::smallint,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 ERROR:  cannot specify both attname and attnum
 -- error: neither attname nor attnum
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
+    'schemaname', 'stats_import',
+    'relname', 'test',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 ERROR:  must specify either attname or attnum
 -- error: attribute is system column
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'xmin'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'xmin',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 ERROR:  cannot modify statistics on system column "xmin"
 -- error: inherited null
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', NULL::boolean,
     'null_frac', 0.1::real);
 ERROR:  "inherited" cannot be NULL
 -- ok: just the fixed values, with version, no stakinds
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'version', 150000::integer,
     'null_frac', 0.2::real,
@@ -392,7 +457,8 @@ AND attname = 'id';
 -- for any stat-having relation.
 --
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
+    'schemaname', 'stats_import',
+    'relname', 'test',
     'attnum', 1::smallint,
     'inherited', false::boolean,
     'null_frac', 0.4::real);
@@ -414,8 +480,9 @@ AND attname = 'id';
 
 -- warn: unrecognized argument name, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.2::real,
     'nope', 0.5::real);
@@ -438,8 +505,9 @@ AND attname = 'id';
 
 -- warn: mcv / mcf null mismatch part 1, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.21::real,
     'most_common_freqs', '{0.1,0.2,0.3}'::real[]
@@ -463,8 +531,9 @@ AND attname = 'id';
 
 -- warn: mcv / mcf null mismatch part 2, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.21::real,
     'most_common_vals', '{1,2,3}'::text
@@ -488,8 +557,9 @@ AND attname = 'id';
 
 -- warn: mcf type mismatch, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.22::real,
     'most_common_vals', '{2,1,3}'::text,
@@ -515,8 +585,9 @@ AND attname = 'id';
 
 -- warn: mcv cast failure, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.23::real,
     'most_common_vals', '{2,four,3}'::text,
@@ -541,8 +612,9 @@ AND attname = 'id';
 
 -- ok: mcv+mcf
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'most_common_vals', '{2,1,3}'::text,
     'most_common_freqs', '{0.3,0.25,0.05}'::real[]
@@ -565,8 +637,9 @@ AND attname = 'id';
 
 -- warn: NULL in histogram array, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.24::real,
     'histogram_bounds', '{1,NULL,3,4}'::text
@@ -590,8 +663,9 @@ AND attname = 'id';
 
 -- ok: histogram_bounds
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'histogram_bounds', '{1,2,3,4}'::text
     );
@@ -613,8 +687,9 @@ AND attname = 'id';
 
 -- warn: elem_count_histogram null element, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.25::real,
     'elem_count_histogram', '{1,1,NULL,1,1,1,1,1}'::real[]
@@ -638,8 +713,9 @@ AND attname = 'tags';
 
 -- ok: elem_count_histogram
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.26::real,
     'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
@@ -662,8 +738,9 @@ AND attname = 'tags';
 
 -- warn: range stats on a scalar type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.27::real,
     'range_empty_frac', 0.5::real,
@@ -689,8 +766,9 @@ AND attname = 'id';
 
 -- warn: range_empty_frac range_length_hist null mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.28::real,
     'range_length_histogram', '{399,499,Infinity}'::text
@@ -714,8 +792,9 @@ AND attname = 'arange';
 
 -- warn: range_empty_frac range_length_hist null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.29::real,
     'range_empty_frac', 0.5::real
@@ -739,8 +818,9 @@ AND attname = 'arange';
 
 -- ok: range_empty_frac + range_length_hist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'range_empty_frac', 0.5::real,
     'range_length_histogram', '{399,499,Infinity}'::text
@@ -763,8 +843,9 @@ AND attname = 'arange';
 
 -- warn: range bounds histogram on scalar, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.31::real,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
@@ -789,8 +870,9 @@ AND attname = 'id';
 
 -- ok: range_bounds_histogram
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
     );
@@ -812,8 +894,9 @@ AND attname = 'arange';
 
 -- warn: cannot set most_common_elems for range type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.32::real,
     'most_common_elems', '{3,1}'::text,
@@ -839,8 +922,9 @@ AND attname = 'arange';
 
 -- warn: scalars can't have mcelem, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.33::real,
     'most_common_elems', '{1,3}'::text,
@@ -866,8 +950,9 @@ AND attname = 'id';
 
 -- warn: mcelem / mcelem mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.34::real,
     'most_common_elems', '{one,two}'::text
@@ -891,8 +976,9 @@ AND attname = 'tags';
 
 -- warn: mcelem / mcelem null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.35::real,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
@@ -916,8 +1002,9 @@ AND attname = 'tags';
 
 -- ok: mcelem
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'most_common_elems', '{one,three}'::text,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
@@ -940,8 +1027,9 @@ AND attname = 'tags';
 
 -- warn: scalars can't have elem_count_histogram, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.36::real,
     'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1}'::real[]
@@ -993,8 +1081,9 @@ SELECT s.schemaname, s.tablename, s.attname, s.inherited, r.*
 FROM pg_catalog.pg_stats AS s
 CROSS JOIN LATERAL
     pg_catalog.pg_restore_attribute_stats(
-        'relation', ('stats_import.' || s.tablename || '_clone')::regclass,
-        'attname', s.attname,
+        'schemaname', 'stats_import',
+        'relname', s.tablename::text || '_clone',
+        'attname', s.attname::text,
         'inherited', s.inherited,
         'version', 150000,
         'null_frac', s.null_frac,
@@ -1171,9 +1260,10 @@ AND attname = 'arange';
 (1 row)
 
 SELECT pg_catalog.pg_clear_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'arange'::name,
-    inherited => false::boolean);
+    schemaname => 'stats_import',
+    relname => 'test',
+    attname => 'arange',
+    inherited => false);
  pg_clear_attribute_stats 
 --------------------------
  
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index 8d04ff4f378..3c330387243 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -21,31 +21,46 @@ CREATE INDEX test_i ON stats_import.test(id);
 -- relstats tests
 --
 
---- error: relation is wrong type
+-- error: schemaname missing
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::oid,
+        'relname', 'test',
+        'relpages', 17::integer);
+
+-- error: relname missing
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relpages', 17::integer);
+
+--- error: schemaname is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 3.6::float,
+        'relname', 'test',
+        'relpages', 17::integer);
+
+--- error: relname is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relname', 0::oid,
         'relpages', 17::integer);
 
 -- error: relation not found
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::oid::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'nope',
         'relpages', 17::integer);
 
 -- error: odd number of variadic arguments cannot be pairs
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relallvisible');
 
 -- error: argument name is NULL
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         NULL, '17'::integer);
 
--- error: argument name is not a text type
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        17, '17'::integer);
-
 -- starting stats
 SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
@@ -54,7 +69,8 @@ WHERE oid = 'stats_import.test_i'::regclass;
 -- regular indexes have special case locking rules
 BEGIN;
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.test_i'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test_i',
         'relpages', 18::integer);
 
 SELECT mode FROM pg_locks
@@ -91,7 +107,8 @@ WHERE oid = 'stats_import.part_parent'::regclass;
 BEGIN;
 
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent_i'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'part_parent_i',
         'relpages', 2::integer);
 
 SELECT mode FROM pg_locks
@@ -110,7 +127,8 @@ WHERE oid = 'stats_import.part_parent_i'::regclass;
 
 -- ok: set all relstats, with version, no bounds checking
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'version', 150000::integer,
         'relpages', '-17'::integer,
         'reltuples', 400::real,
@@ -123,7 +141,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just relpages, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', '16'::integer);
 
 SELECT relpages, reltuples, relallvisible, relallfrozen
@@ -132,7 +151,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just reltuples, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'reltuples', '500'::real);
 
 SELECT relpages, reltuples, relallvisible, relallfrozen
@@ -141,7 +161,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just relallvisible, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relallvisible', 5::integer);
 
 SELECT relpages, reltuples, relallvisible, relallfrozen
@@ -150,7 +171,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: just relallfrozen
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'version', 150000::integer,
         'relallfrozen', 3::integer);
 
@@ -160,7 +182,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- warn: bad relpages type, rest updated
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', 'nope'::text,
         'reltuples', 400.0::real,
         'relallvisible', 4::integer,
@@ -172,7 +195,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- unrecognized argument name, rest ok
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', '171'::integer,
         'nope', 10::integer);
 
@@ -181,8 +205,7 @@ FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: clear stats
-SELECT pg_catalog.pg_clear_relation_stats(
-    relation => 'stats_import.test'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'test');
 
 SELECT relpages, reltuples, relallvisible
 FROM pg_class
@@ -192,48 +215,70 @@ WHERE oid = 'stats_import.test'::regclass;
 CREATE SEQUENCE stats_import.testseq;
 
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.testseq'::regclass);
+        'schemaname', 'stats_import',
+        'relname', 'testseq');
 
-SELECT pg_catalog.pg_clear_relation_stats(
-        'stats_import.testseq'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testseq');
 
 CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
 
-SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.testview'::regclass);
-
-SELECT pg_catalog.pg_clear_relation_stats(
-        'stats_import.testview'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testview');
 
 --
 -- attribute stats
 --
 
--- error: object does not exist
+-- error: schemaname missing
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', '0'::oid::regclass,
-    'attname', 'id'::name,
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: relation null
+-- error: schema does not exist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', NULL::oid::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'nope',
+    'relname', 'test',
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: relname missing
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: relname does not exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'relname', 'nope',
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: relname null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'relname', NULL,
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
 -- error: NULL attname
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', NULL::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', NULL,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
 -- error: attname doesn't exist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'nope'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'nope',
     'inherited', false::boolean,
     'null_frac', 0.1::real,
     'avg_width', 2::integer,
@@ -241,36 +286,41 @@ SELECT pg_catalog.pg_restore_attribute_stats(
 
 -- error: both attname and attnum
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'attnum', 1::smallint,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
 -- error: neither attname nor attnum
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
+    'schemaname', 'stats_import',
+    'relname', 'test',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
 -- error: attribute is system column
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'xmin'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'xmin',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
 -- error: inherited null
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', NULL::boolean,
     'null_frac', 0.1::real);
 
 -- ok: just the fixed values, with version, no stakinds
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'version', 150000::integer,
     'null_frac', 0.2::real,
@@ -290,7 +340,8 @@ AND attname = 'id';
 -- for any stat-having relation.
 --
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
+    'schemaname', 'stats_import',
+    'relname', 'test',
     'attnum', 1::smallint,
     'inherited', false::boolean,
     'null_frac', 0.4::real);
@@ -304,8 +355,9 @@ AND attname = 'id';
 
 -- warn: unrecognized argument name, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.2::real,
     'nope', 0.5::real);
@@ -319,8 +371,9 @@ AND attname = 'id';
 
 -- warn: mcv / mcf null mismatch part 1, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.21::real,
     'most_common_freqs', '{0.1,0.2,0.3}'::real[]
@@ -335,8 +388,9 @@ AND attname = 'id';
 
 -- warn: mcv / mcf null mismatch part 2, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.21::real,
     'most_common_vals', '{1,2,3}'::text
@@ -351,8 +405,9 @@ AND attname = 'id';
 
 -- warn: mcf type mismatch, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.22::real,
     'most_common_vals', '{2,1,3}'::text,
@@ -368,8 +423,9 @@ AND attname = 'id';
 
 -- warn: mcv cast failure, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.23::real,
     'most_common_vals', '{2,four,3}'::text,
@@ -385,8 +441,9 @@ AND attname = 'id';
 
 -- ok: mcv+mcf
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'most_common_vals', '{2,1,3}'::text,
     'most_common_freqs', '{0.3,0.25,0.05}'::real[]
@@ -401,8 +458,9 @@ AND attname = 'id';
 
 -- warn: NULL in histogram array, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.24::real,
     'histogram_bounds', '{1,NULL,3,4}'::text
@@ -417,8 +475,9 @@ AND attname = 'id';
 
 -- ok: histogram_bounds
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'histogram_bounds', '{1,2,3,4}'::text
     );
@@ -432,8 +491,9 @@ AND attname = 'id';
 
 -- warn: elem_count_histogram null element, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.25::real,
     'elem_count_histogram', '{1,1,NULL,1,1,1,1,1}'::real[]
@@ -448,8 +508,9 @@ AND attname = 'tags';
 
 -- ok: elem_count_histogram
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.26::real,
     'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
@@ -464,8 +525,9 @@ AND attname = 'tags';
 
 -- warn: range stats on a scalar type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.27::real,
     'range_empty_frac', 0.5::real,
@@ -481,8 +543,9 @@ AND attname = 'id';
 
 -- warn: range_empty_frac range_length_hist null mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.28::real,
     'range_length_histogram', '{399,499,Infinity}'::text
@@ -497,8 +560,9 @@ AND attname = 'arange';
 
 -- warn: range_empty_frac range_length_hist null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.29::real,
     'range_empty_frac', 0.5::real
@@ -513,8 +577,9 @@ AND attname = 'arange';
 
 -- ok: range_empty_frac + range_length_hist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'range_empty_frac', 0.5::real,
     'range_length_histogram', '{399,499,Infinity}'::text
@@ -529,8 +594,9 @@ AND attname = 'arange';
 
 -- warn: range bounds histogram on scalar, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.31::real,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
@@ -545,8 +611,9 @@ AND attname = 'id';
 
 -- ok: range_bounds_histogram
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
     );
@@ -560,8 +627,9 @@ AND attname = 'arange';
 
 -- warn: cannot set most_common_elems for range type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.32::real,
     'most_common_elems', '{3,1}'::text,
@@ -577,8 +645,9 @@ AND attname = 'arange';
 
 -- warn: scalars can't have mcelem, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.33::real,
     'most_common_elems', '{1,3}'::text,
@@ -594,8 +663,9 @@ AND attname = 'id';
 
 -- warn: mcelem / mcelem mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.34::real,
     'most_common_elems', '{one,two}'::text
@@ -610,8 +680,9 @@ AND attname = 'tags';
 
 -- warn: mcelem / mcelem null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.35::real,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
@@ -626,8 +697,9 @@ AND attname = 'tags';
 
 -- ok: mcelem
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'most_common_elems', '{one,three}'::text,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
@@ -642,8 +714,9 @@ AND attname = 'tags';
 
 -- warn: scalars can't have elem_count_histogram, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.36::real,
     'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1}'::real[]
@@ -690,8 +763,9 @@ SELECT s.schemaname, s.tablename, s.attname, s.inherited, r.*
 FROM pg_catalog.pg_stats AS s
 CROSS JOIN LATERAL
     pg_catalog.pg_restore_attribute_stats(
-        'relation', ('stats_import.' || s.tablename || '_clone')::regclass,
-        'attname', s.attname,
+        'schemaname', 'stats_import',
+        'relname', s.tablename::text || '_clone',
+        'attname', s.attname::text,
         'inherited', s.inherited,
         'version', 150000,
         'null_frac', s.null_frac,
@@ -836,9 +910,10 @@ AND inherited = false
 AND attname = 'arange';
 
 SELECT pg_catalog.pg_clear_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'arange'::name,
-    inherited => false::boolean);
+    schemaname => 'stats_import',
+    relname => 'test',
+    attname => 'arange',
+    inherited => false);
 
 SELECT COUNT(*)
 FROM pg_stats
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index f97f0ce570a..4c5ef0604de 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30331,22 +30331,24 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          <structname>mytable</structname>:
 <programlisting>
  SELECT pg_restore_relation_stats(
-    'relation',  'mytable'::regclass,
-    'relpages',  173::integer,
-    'reltuples', 10000::real);
+    'schemaname', 'myschema',
+    'relname',    'mytable',
+    'relpages',   173::integer,
+    'reltuples',  10000::real);
 </programlisting>
         </para>
         <para>
-         The argument <literal>relation</literal> with a value of type
-         <type>regclass</type> is required, and specifies the table. Other
-         arguments are the names and values of statistics corresponding to
-         certain columns in <link
+         The arguments <literal>schemaname</literal> with a value of type
+         <type>regclass</type> and <literal>relname</literal> are required,
+         and specifies the table. Other arguments are the names and values
+         of statistics corresponding to certain columns in <link
          linkend="catalog-pg-class"><structname>pg_class</structname></link>.
          The currently-supported relation statistics are
          <literal>relpages</literal> with a value of type
          <type>integer</type>, <literal>reltuples</literal> with a value of
-         type <type>real</type>, and <literal>relallvisible</literal> with a
-         value of type <type>integer</type>.
+         type <type>real</type>, <literal>relallvisible</literal> with a
+         value of type <type>integer</type>, and <literal>relallfrozen</literal>
+         with a value of type <type>integer</type>.
         </para>
         <para>
          Additionally, this function accepts argument name
@@ -30374,7 +30376,7 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          <indexterm>
           <primary>pg_clear_relation_stats</primary>
          </indexterm>
-         <function>pg_clear_relation_stats</function> ( <parameter>relation</parameter> <type>regclass</type> )
+         <function>pg_clear_relation_stats</function> ( <parameter>schemaname</parameter> <type>text</type>, <parameter>relname</parameter> <type>text</type> )
          <returnvalue>void</returnvalue>
         </para>
         <para>
@@ -30423,16 +30425,18 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          <structname>mytable</structname>:
 <programlisting>
  SELECT pg_restore_attribute_stats(
-    'relation',    'mytable'::regclass,
-    'attname',     'col1'::name,
-    'inherited',   false,
-    'avg_width',   125::integer,
-    'null_frac',   0.5::real);
+    'schemaname', 'myschema',
+    'relname',    'mytable',
+    'attname',    'col1',
+    'inherited',  false,
+    'avg_width',  125::integer,
+    'null_frac',  0.5::real);
 </programlisting>
         </para>
         <para>
-         The required arguments are <literal>relation</literal> with a value
-         of type <type>regclass</type>, which specifies the table; either
+         The required arguments are <literal>schemaname</literal> with a value
+         of type <type>regclass</type> and <literal>relname</literal> with a value
+         of type <type>text</type> which specify the table; either
          <literal>attname</literal> with a value of type <type>name</type> or
          <literal>attnum</literal> with a value of type <type>smallint</type>,
          which specifies the column; and <literal>inherited</literal>, which
@@ -30468,7 +30472,8 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
           <primary>pg_clear_attribute_stats</primary>
          </indexterm>
          <function>pg_clear_attribute_stats</function> (
-         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>schemaname</parameter> <type>text</type>,
+         <parameter>relname</parameter> <type>text</type>,
          <parameter>attname</parameter> <type>name</type>,
          <parameter>inherited</parameter> <type>boolean</type> )
          <returnvalue>void</returnvalue>
-- 
2.48.1

#433Jeff Davis
pgsql@j-davis.com
In reply to: Ashutosh Bapat (#423)
Re: Statistics Import and Export: difference in statistics dumped

On Wed, 2025-03-05 at 15:22 +0530, Ashutosh Bapat wrote:

Hmm. Updating the statistics without consuming more CPU is more
valuable when autovacuum is off it improves query plans with no extra
efforts. But if adding a new mode is some significant work, riding it
on top of autovacuum=off might ok. It's not documented either way, so
we could change that behaviour later if we find it troublesome.

Sounds good. I will commit something like the v2 patch then soon, and
if we need a different condition we can change it later.

Regards,
Jeff Davis

#434Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#432)
Re: Statistics Import and Export

On Wed, 2025-03-05 at 23:04 -0500, Corey Huinker wrote:

Anyway, here's a rebased set of the existing up-for-consideration
patches, plus the optimization of avoiding querying on non-expression
indexes.

Patch 0001 contains a bug: it returns REQ_STATS early, before doing any
exclusions.

But I agree the previous code was hard to read in one place, and
redundant in another, so I will commit a fixup.

Regards,
Jeff Davis

#435Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#432)
Re: Statistics Import and Export

On Wed, 2025-03-05 at 23:04 -0500, Corey Huinker wrote:

Anyway, here's a rebased set of the existing up-for-consideration
patches, plus the optimization of avoiding querying on non-expression
indexes.

Comments on 0003:

* All the argument names for pg_restore_attribute_stats match pg_stats,
except relname vs tablename. There doesn't appear to be a great answer
here, because "relname" is the natural name to use for
pg_restore_relation_stats(), so either the two restore functions will
be inconsistent, or the argument name of one of them will be
inconsistent with its respective catalog. I assume that's the
reasoning?

* it decides to only issue a WARNING, rather than an ERROR, if the
table can't be found, which seems fine

* Now that it's doing a namespace lookup, we should also check for the
USAGE privilege on the namespace, right?

Based on the other changes we've made to this feature, I think 0003
makes sense, so I'm inclined to move ahead with it, but I'm open to
opinions.

0004 looks straightforward, though perhaps we should move some of the
code into a static function rather than indenting so many lines.

Did you collect performance results for 0004?

Regards,
Jeff Davis

#436Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#435)
Re: Statistics Import and Export

On Thu, Mar 6, 2025 at 3:48 AM Jeff Davis <pgsql@j-davis.com> wrote:

On Wed, 2025-03-05 at 23:04 -0500, Corey Huinker wrote:

Anyway, here's a rebased set of the existing up-for-consideration
patches, plus the optimization of avoiding querying on non-expression
indexes.

Comments on 0003:

* All the argument names for pg_restore_attribute_stats match pg_stats,
except relname vs tablename. There doesn't appear to be a great answer
here, because "relname" is the natural name to use for
pg_restore_relation_stats(), so either the two restore functions will
be inconsistent, or the argument name of one of them will be
inconsistent with its respective catalog. I assume that's the
reasoning?

Correct, either we use 'tablename' to describe indexes as well, or we
diverge from the system view's naming.

* Now that it's doing a namespace lookup, we should also check for the
USAGE privilege on the namespace, right?

Unless some check was being done by the 'foo.bar'::regclass cast, I
understand why we should add one.

Based on the other changes we've made to this feature, I think 0003
makes sense, so I'm inclined to move ahead with it, but I'm open to
opinions.

If we do, we'll want to change downgrade the following errors to
warn+return false:

* stats_check_required_arg()
* stats_lock_check_privileges()
* RecoveryInProgress
* specified both attnum and argnum
* attname/attnum does not exist, or is system column

0004 looks straightforward, though perhaps we should move some of the
code into a static function rather than indenting so many lines.

I agree, but the thread conversation had already shifted to doing just one
single call to pg_stats, so this was just a demonstration.

Did you collect performance results for 0004?

No, as I wasn't sure that I could replicate Andres' setup, and the
conversation was quickly moving to the aforementioned single-query idea.

#437Andres Freund
andres@anarazel.de
In reply to: Corey Huinker (#432)
Re: Statistics Import and Export

Hi,

On 2025-03-05 23:04:29 -0500, Corey Huinker wrote:

I'm uncertain how we'd do that with (schemaname,tablename) pairs. Are you
suggesting we back the joins from pg_stats to pg_namespace and pg_class

and

then filter by oids?

I was thinking of one query per schema or something like that. But yea, a
query to pg_namespace and pg_class wouldn't be a problem if we did it far
fewer times than before. Or you could put the list of catalogs / tables
to
be queried into an unnest() with two arrays or such.

Not sure how good the query plan for that would be, but it may be worth
looking at.

Ok, so we're willing to take the pg_class/pg_namespace join hit for one or
a handful of queries, good to know.

It's a tradeoff that needs to be evaluated. But I'd be rather surprised if it
weren't faster to run one query with the additional joins than hundreds of
queries without them.

Each call to getAttributeStats() fetches the pg_stats for one and only

one

relation and then writes the SQL call to fout, then discards the result

set

once all the attributes of the relation are done.

I don't think that's true. For one my example demonstrated that it
increases
the peak memory usage substantially. That'd not be the case if the data was
just written out to stdout or such.

Looking at the code confirms that. The ArchiveEntry() in
dumpRelationStats()
is never freed, afaict. And ArchiveEntry() strdups ->createStmt, which
contains the "SELECT pg_restore_attribute_stats(...)".

Pardon my inexperience, but aren't the ArchiveEntry records needed right up
until the program's run?

s/the/the end of the/?

If there's value in freeing them, why isn't it being done already? What
other thing would consume this freed memory?

I'm not saying that they can be freed, they can't right now. My point is just
that we *already* keep all the stats in memory, so the fact that fetching all
stats in a single query would also require keeping them in memory is not an
issue.

But TBH, I do wonder how much the current memory usage of the statistics
dump/restore support is going to bite us. In some cases this will dramatically
increase pg_dump/pg_upgrade's memory usage, my tests were with tiny amounts of
data and very simple scalar datatypes and you already could see a substantial
increase. With something like postgis or even just a lot of jsonb columns
this is going to be way worse.

Greetings,

Andres Freund

#438Robert Haas
robertmhaas@gmail.com
In reply to: Andres Freund (#437)
Re: Statistics Import and Export

On Thu, Mar 6, 2025 at 9:29 AM Andres Freund <andres@anarazel.de> wrote:

But TBH, I do wonder how much the current memory usage of the statistics
dump/restore support is going to bite us. In some cases this will dramatically
increase pg_dump/pg_upgrade's memory usage, my tests were with tiny amounts of
data and very simple scalar datatypes and you already could see a substantial
increase. With something like postgis or even just a lot of jsonb columns
this is going to be way worse.

To be honest, I am a bit surprised that we decided to enable this by
default. It's not obvious to me that statistics should be regarded as
part of the database in the same way that table definitions or table
data are. That said, I'm not overwhelmingly opposed to that choice.
However, even if it's the right choice in theory, we should maybe
rethink if it's going to be too slow or use too much memory.

--
Robert Haas
EDB: http://www.enterprisedb.com

#439Corey Huinker
corey.huinker@gmail.com
In reply to: Andres Freund (#437)
Re: Statistics Import and Export

Pardon my inexperience, but aren't the ArchiveEntry records needed right

up

until the program's run?

s/the/the end of the/?

yes

If there's value in freeing them, why isn't it being done already? What
other thing would consume this freed memory?

I'm not saying that they can be freed, they can't right now. My point is
just
that we *already* keep all the stats in memory, so the fact that fetching
all
stats in a single query would also require keeping them in memory is not an
issue.

That's true in cases where we're not filtering schemas or tables. We fetch
the pg_class stats as a part of getTables, but those are small, and not a
part of the query in question.

Fetching all the pg_stats for a db when we only want one table could be a
nasty performance regression, and we can't just filter on the oids of the
tables we want, because those tables can have expression indexes, so the
oid filter would get complicated quickly.

But TBH, I do wonder how much the current memory usage of the statistics
dump/restore support is going to bite us. In some cases this will
dramatically
increase pg_dump/pg_upgrade's memory usage, my tests were with tiny
amounts of
data and very simple scalar datatypes and you already could see a
substantial
increase. With something like postgis or even just a lot of jsonb columns
this is going to be way worse.

Yes, it will cost us in pg_dump, but it will save customers from some long
ANALYZE operations.

#440Andres Freund
andres@anarazel.de
In reply to: Corey Huinker (#439)
Re: Statistics Import and Export

Hi,

On 2025-03-06 12:04:25 -0500, Corey Huinker wrote:

If there's value in freeing them, why isn't it being done already? What
other thing would consume this freed memory?

I'm not saying that they can be freed, they can't right now. My point is
just
that we *already* keep all the stats in memory, so the fact that fetching
all
stats in a single query would also require keeping them in memory is not an
issue.

That's true in cases where we're not filtering schemas or tables. We fetch
the pg_class stats as a part of getTables, but those are small, and not a
part of the query in question.

Fetching all the pg_stats for a db when we only want one table could be a
nasty performance regression

I don't think anybody argued that we should fetch all stats regardless of
filtering for the to-be-dumped tables.

and we can't just filter on the oids of the tables we want, because those
tables can have expression indexes, so the oid filter would get complicated
quickly.

I don't follow. We already have the tablenames, schemanames and oids of the
to-be-dumped tables/indexes collected in pg_dump, all that's needed is to send
a list of those to the server to filter there?

But TBH, I do wonder how much the current memory usage of the statistics
dump/restore support is going to bite us. In some cases this will
dramatically
increase pg_dump/pg_upgrade's memory usage, my tests were with tiny
amounts of
data and very simple scalar datatypes and you already could see a
substantial
increase. With something like postgis or even just a lot of jsonb columns
this is going to be way worse.

Yes, it will cost us in pg_dump, but it will save customers from some long
ANALYZE operations.

My concern is that it might prevent some upgrades from *ever* completing,
because of pg_dump running out of memory.

Greetings,

Andres Freund

#441Corey Huinker
corey.huinker@gmail.com
In reply to: Robert Haas (#438)
Re: Statistics Import and Export

To be honest, I am a bit surprised that we decided to enable this by
default. It's not obvious to me that statistics should be regarded as
part of the database in the same way that table definitions or table
data are. That said, I'm not overwhelmingly opposed to that choice.
However, even if it's the right choice in theory, we should maybe
rethink if it's going to be too slow or use too much memory.

I'm strongly in favor of the choice to make it default. This is reducing
the impact of a post-upgrade customer footgun wherein heavy workloads are
applied to a database post-upgrade but before analyze/vacuumdb have had a
chance to do their magic [1]In that situation, the workload queries have no stats, get terrible plans, everything becomes a sequential scan. Sequential scans swamp the system, starving the analyze commands of the I/O they need to get the badly needed statistics. Even after the stats are in place, the system is still swamped with queries that were in flight before the stats were in place. Even well intentioned customers [2] can fall prey to this when their microservices detect that the database is online again, and automatically resume work..

It seems to me that we're fretting over seconds when the feature is
potentially saving the customer hours of reduced availability if not
outright downtime.

[1]: In that situation, the workload queries have no stats, get terrible plans, everything becomes a sequential scan. Sequential scans swamp the system, starving the analyze commands of the I/O they need to get the badly needed statistics. Even after the stats are in place, the system is still swamped with queries that were in flight before the stats were in place. Even well intentioned customers [2] can fall prey to this when their microservices detect that the database is online again, and automatically resume work.
plans, everything becomes a sequential scan. Sequential scans swamp the
system, starving the analyze commands of the I/O they need to get the badly
needed statistics. Even after the stats are in place, the system is still
swamped with queries that were in flight before the stats were in place.
Even well intentioned customers [2]This exact situation happened at a place where I was consulting. The microservices all restarted work automatically despite assurances that they would not. That bad experience was my primary motivator for implementing theis feature. can fall prey to this when their
microservices detect that the database is online again, and automatically
resume work.

[2]: This exact situation happened at a place where I was consulting. The microservices all restarted work automatically despite assurances that they would not. That bad experience was my primary motivator for implementing theis feature.
microservices all restarted work automatically despite assurances that they
would not. That bad experience was my primary motivator for implementing
theis feature.

#442Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#441)
Re: Statistics Import and Export

On Thu, 2025-03-06 at 12:16 -0500, Corey Huinker wrote:

I'm strongly in favor of the choice to make it default. This is
reducing the impact of a post-upgrade

There are potentially two different defaults: pg_dump and pg_upgrade.

In any case, let's see what improvements we can make to memory usage
and performance, and take it from there.

Regards,
Jeff Davis

#443Corey Huinker
corey.huinker@gmail.com
In reply to: Andres Freund (#440)
Re: Statistics Import and Export

I don't follow. We already have the tablenames, schemanames and oids of the
to-be-dumped tables/indexes collected in pg_dump, all that's needed is to
send
a list of those to the server to filter there?

Do we have something that currently does that? All of the collect functions
(collectComments, etc) take an unfiltered approach. Seems like we'd have to
collect the stats sometime after ProcessArchiveRestoreOptions, which is
significantly after the rest of them.

My concern is that it might prevent some upgrades from *ever* completing,
because of pg_dump running out of memory.

Obviously a valid concern.

#444Andres Freund
andres@anarazel.de
In reply to: Corey Huinker (#441)
Re: Statistics Import and Export

Hi,

On 2025-03-06 12:16:44 -0500, Corey Huinker wrote:

To be honest, I am a bit surprised that we decided to enable this by
default. It's not obvious to me that statistics should be regarded as
part of the database in the same way that table definitions or table
data are. That said, I'm not overwhelmingly opposed to that choice.
However, even if it's the right choice in theory, we should maybe
rethink if it's going to be too slow or use too much memory.

I'm strongly in favor of the choice to make it default. This is reducing
the impact of a post-upgrade customer footgun wherein heavy workloads are
applied to a database post-upgrade but before analyze/vacuumdb have had a
chance to do their magic [1].

To be clear, I think this is a very important improvement that most people
should use. I just don't think it's quite there yet.

It seems to me that we're fretting over seconds when the feature is
potentially saving the customer hours of reduced availability if not
outright downtime.

FWIW, I care about the performance for two reasons:

1) It's a difference of seconds in the regression database, which has a few
hundred tables, few columns, very little data and thus small stats. In a
database with a lot of tables and columns with complicated datatypes the
difference will be far larger.

And in contrast to analyzing the database in parallel, the pg_dump/restore
work to restore stats afaict happens single-threaded for each database.

2) The changes initially substantially increased the time a test cycle takes
for me locally. I run the tests 10s to 100s time a day, that really adds
up.

002_pg_upgrade is the test that dominates the overall test time for me, so
it getting slower by a good bit means the overall test time increased.

1fd1bd87101^:
total test time: 1m27.010s
003_pg_upgrade alone: 1m6.309s

1fd1bd87101:
total test time: 1m45.945s
003_pg_upgrade alone: 1m24.597s

master at 0f21db36d66:
total test time: 1m34.576s
003_pg_upgrade alone: 1m12.550s

It clearly got a lot better since 1fd1bd87101, but it's still ~9% slower
than before...

I care about the memory usage effects because I've seen plenty systems where
pg_statistics is many gigabytes (after toast compression!), and I am really
worried that pg_dump having all the serialized strings in memory will cause a
lot of previously working pg_dump invocations and pg_upgrades to fail. That'd
also be a really bad experience.

The more I think about it, the less correct it seems to me to have the
statement to restore statistics tracked via ArchiveOpts->createStmt. We use
that for DDL, but this really is data, not DDL. Because we store it in
->createStmt it's stored in-memory for the runtime of pg_dump, which means the
peak memory usage will inherently be quite high.

I think the stats need to be handled much more like we handle the actual table
data, which are obviously *not* stored in memory for the whole run of pg_dump.

Greetings,

Andres Freund

#445Jeff Davis
pgsql@j-davis.com
In reply to: Andres Freund (#440)
Re: Statistics Import and Export

On Thu, 2025-03-06 at 12:16 -0500, Andres Freund wrote:

I don't follow. We already have the tablenames, schemanames and oids
of the
to-be-dumped tables/indexes collected in pg_dump, all that's needed
is to send
a list of those to the server to filter there?

Would it be appropriate to create a temp table? I wouldn't normally
expect pg_dump to create temp tables, but I can't think of a major
reason not to.

If not, did you have in mind a CTE with a large VALUES expression, or
just a giant IN() list?

Regards,
Jeff Davis

#446Andres Freund
andres@anarazel.de
In reply to: Corey Huinker (#443)
Re: Statistics Import and Export

Hi,

On 2025-03-06 13:00:07 -0500, Corey Huinker wrote:

I don't follow. We already have the tablenames, schemanames and oids of the
to-be-dumped tables/indexes collected in pg_dump, all that's needed is to
send
a list of those to the server to filter there?

Do we have something that currently does that?

Yes. Afaict there's at least:
- getPolicies()
- getIndexes()
- getConstraints()
- getTriggers(),
- getTableAttrs()

They all send an array of oids as part of the query and then join an
unnest()ed version of the array against whatever they're collecting. See
e.g. getPolicies():

"FROM unnest('%s'::pg_catalog.oid[]) AS src(tbloid)\n"
"JOIN pg_catalog.pg_policy pol ON (src.tbloid = pol.polrelid)",

Greetings,

Andres Freund

#447Nathan Bossart
nathandbossart@gmail.com
In reply to: Andres Freund (#444)
Re: Statistics Import and Export

On Thu, Mar 06, 2025 at 01:04:55PM -0500, Andres Freund wrote:

To be clear, I think this is a very important improvement that most people
should use.

+1

I just don't think it's quite there yet.

I agree that we should continue working on the performance/memory stuff.

1) It's a difference of seconds in the regression database, which has a few
hundred tables, few columns, very little data and thus small stats. In a
database with a lot of tables and columns with complicated datatypes the
difference will be far larger.

And in contrast to analyzing the database in parallel, the pg_dump/restore
work to restore stats afaict happens single-threaded for each database.

Yeah, I did a lot of work in v18 to rein in pg_dump --binary-upgrade
runtime, and I'm a bit worried that this will undo much of that. Obviously
it's going to increase runtime by some amount, which is acceptable, but it
needs to be within reason. I'm optimistic this is within reach for v18 by
reducing the number of queries.

I care about the memory usage effects because I've seen plenty systems where
pg_statistics is many gigabytes (after toast compression!), and I am really
worried that pg_dump having all the serialized strings in memory will cause a
lot of previously working pg_dump invocations and pg_upgrades to fail. That'd
also be a really bad experience.

I think it is entirely warranted to consider these cases. IME cases of "a
million tables" or "a million sequences" are far more common than you might
think.

--
nathan

#448Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#445)
Re: Statistics Import and Export

Would it be appropriate to create a temp table? I wouldn't normally
expect pg_dump to create temp tables, but I can't think of a major
reason not to.

I think we can't - the db might be a replica.

If not, did you have in mind a CTE with a large VALUES expression, or
just a giant IN() list?

getPolicies - as Andres cited right after your post.

#449Andres Freund
andres@anarazel.de
In reply to: Jeff Davis (#445)
Re: Statistics Import and Export

Hi,

On 2025-03-06 10:07:43 -0800, Jeff Davis wrote:

On Thu, 2025-03-06 at 12:16 -0500, Andres Freund wrote:

I don't follow. We already have the tablenames, schemanames and oids
of the
to-be-dumped tables/indexes collected in pg_dump, all that's needed
is to send
a list of those to the server to filter there?

Would it be appropriate to create a temp table? I wouldn't normally
expect pg_dump to create temp tables, but I can't think of a major
reason not to.

It doesn't work on a standby.

If not, did you have in mind a CTE with a large VALUES expression, or
just a giant IN() list?

An array, with a server-side unnest(), like we do in a bunch of other
places. E.g.

/* need left join to pg_type to not fail on dropped columns ... */
appendPQExpBuffer(q,
"FROM unnest('%s'::pg_catalog.oid[]) AS src(tbloid)\n"
"JOIN pg_catalog.pg_attribute a ON (src.tbloid = a.attrelid) "
"LEFT JOIN pg_catalog.pg_type t "
"ON (a.atttypid = t.oid)\n",
tbloids->data);

Greetings,

Andres Freund

#450Tom Lane
tgl@sss.pgh.pa.us
In reply to: Andres Freund (#444)
Re: Statistics Import and Export

Andres Freund <andres@anarazel.de> writes:

And in contrast to analyzing the database in parallel, the pg_dump/restore
work to restore stats afaict happens single-threaded for each database.

In principle we should be able to do stats dump/restore parallelized
just as we do for data. There are some stumbling blocks in the way
of that:

1. pg_upgrade has made a policy judgement to apply parallelism across
databases not within a database, ie it will launch concurrent dump/
restore tasks in different DBs but not authorize any one of them to
eat multiple CPUs. That needs to be re-thought probably, as I think
that decision dates to before we had useful parallelism in pg_dump and
pg_restore. I wonder if we could just rip out pg_upgrade's support
for DB-level parallelism, which is not terribly pretty anyway, and
simply pass the -j switch straight to pg_dump and pg_restore.

2. pg_restore should already be able to perform stats restores in
parallel (if authorized to use multiple threads), but I'm less clear
on whether that works right now for pg_dump.

3. Also, parallel restore depends critically on the TOC entries'
dependencies being sane, and right now I do not think they are.
I looked at "pg_restore -l -v" output for the regression DB, and it
seems like it's not taking care to ensure that table/MV data is loaded
before the table/MV's stats. (Maybe that accounts for some of the
complaints we've seen about stats getting mangled??)

I think the stats need to be handled much more like we handle the actual table
data, which are obviously *not* stored in memory for the whole run of pg_dump.

+1

regards, tom lane

#451Corey Huinker
corey.huinker@gmail.com
In reply to: Andres Freund (#444)
Re: Statistics Import and Export

The more I think about it, the less correct it seems to me to have the
statement to restore statistics tracked via ArchiveOpts->createStmt. We
use
that for DDL, but this really is data, not DDL. Because we store it in
->createStmt it's stored in-memory for the runtime of pg_dump, which means
the
peak memory usage will inherently be quite high.

I think the stats need to be handled much more like we handle the actual
table
data, which are obviously *not* stored in memory for the whole run of
pg_dump.

I'm at the same conclusion. This would mean keeping the one
getAttributeStats query perrelation, but at least we'd be able to free up
the result after we write it to disk.

#452Andres Freund
andres@anarazel.de
In reply to: Corey Huinker (#451)
Re: Statistics Import and Export

On 2025-03-06 13:47:51 -0500, Corey Huinker wrote:

I'm at the same conclusion. This would mean keeping the one
getAttributeStats query perrelation,

Why does it have to mean that? It surely would be easier with separate
queries, but I don't think there's anything inherently blocking us from doing
something in a more batch-y fashion.

#453Tom Lane
tgl@sss.pgh.pa.us
In reply to: Andres Freund (#452)
Re: Statistics Import and Export

Andres Freund <andres@anarazel.de> writes:

On 2025-03-06 13:47:51 -0500, Corey Huinker wrote:

I'm at the same conclusion. This would mean keeping the one
getAttributeStats query perrelation,

Why does it have to mean that? It surely would be easier with separate
queries, but I don't think there's anything inherently blocking us from doing
something in a more batch-y fashion.

Complexity? pg_dump doesn't have anything like that at the moment,
and I'm loath to start inventing such facilities at this point in
the release cycle. Let's deal with the blockers for parallelizing
dump and restore of stats, and then see where we are performance-wise.

regards, tom lane

#454Nathan Bossart
nathandbossart@gmail.com
In reply to: Tom Lane (#450)
Re: Statistics Import and Export

On Thu, Mar 06, 2025 at 01:47:34PM -0500, Tom Lane wrote:

1. pg_upgrade has made a policy judgement to apply parallelism across
databases not within a database, ie it will launch concurrent dump/
restore tasks in different DBs but not authorize any one of them to
eat multiple CPUs. That needs to be re-thought probably, as I think
that decision dates to before we had useful parallelism in pg_dump and
pg_restore. I wonder if we could just rip out pg_upgrade's support
for DB-level parallelism, which is not terribly pretty anyway, and
simply pass the -j switch straight to pg_dump and pg_restore.

That would certainly help for clusters with one big database with many LOs
or something, but I worry it would hurt the many database case quite a bit.
Maybe we could add a --jobs-per-db option that indicates how to parallelize
dump/restore. If you set --jobs=8 --jobs-per-db=8, the databases would be
dumped serially, but pg_dump would get -j8. If you set --jobs=8 and
--jobs-per-db=2, we'd process 4 databases at a time, each with -j2.

--
nathan

#455Andres Freund
andres@anarazel.de
In reply to: Tom Lane (#450)
Re: Statistics Import and Export

Hi,

On 2025-03-06 13:47:34 -0500, Tom Lane wrote:

Andres Freund <andres@anarazel.de> writes:

And in contrast to analyzing the database in parallel, the pg_dump/restore
work to restore stats afaict happens single-threaded for each database.

In principle we should be able to do stats dump/restore parallelized
just as we do for data.

Yea.

Whether the gains are worth the cost isn't clear to me though. Issuing
individual queries for each relation needs a fair bit of parallelism to catch
up to doing the dumping in a single statement, if it ever can.

1. pg_upgrade has made a policy judgement to apply parallelism across
databases not within a database, ie it will launch concurrent dump/
restore tasks in different DBs but not authorize any one of them to
eat multiple CPUs. That needs to be re-thought probably, as I think
that decision dates to before we had useful parallelism in pg_dump and
pg_restore. I wonder if we could just rip out pg_upgrade's support
for DB-level parallelism, which is not terribly pretty anyway, and
simply pass the -j switch straight to pg_dump and pg_restore.

I don't think that'd work well, right now pg_dump only handles a single
database (pg_dumpall doesn't yet support -Fc) *and* pg_dump is still serial
for the bulk of the work that pg_upgrade cares about.

I think the only parallelism that'd actually happen for pg_upgrade would be
dumping of large objects?

Greetings,

Andres Freund

#456Tom Lane
tgl@sss.pgh.pa.us
In reply to: Nathan Bossart (#454)
Re: Statistics Import and Export

Nathan Bossart <nathandbossart@gmail.com> writes:

On Thu, Mar 06, 2025 at 01:47:34PM -0500, Tom Lane wrote:

... I wonder if we could just rip out pg_upgrade's support
for DB-level parallelism, which is not terribly pretty anyway, and
simply pass the -j switch straight to pg_dump and pg_restore.

That would certainly help for clusters with one big database with many LOs
or something, but I worry it would hurt the many database case quite a bit.

I'm very skeptical of that. How many DBs do you know with just one table?
I think most have enough that they could keep a reasonable number of
CPUs busy with pg_dump's internal parallelism.

Maybe we could add a --jobs-per-db option that indicates how to parallelize
dump/restore. If you set --jobs=8 --jobs-per-db=8, the databases would be
dumped serially, but pg_dump would get -j8. If you set --jobs=8 and
--jobs-per-db=2, we'd process 4 databases at a time, each with -j2.

I specifically didn't propose such a thing because I think it will be
a sucky user experience. In the first place, users are unlikely to
take the time to puzzle out exactly how they should slice that up;
in the second place, if they try they won't necessarily find that
there's a good solution with those knobs; in the third place,
pg_upgrade is commonly invoked through packager-supplied scripts that
might not give access to those switches anyway.

In the short term I think repurposing -j as meaning within-DB
parallelism rather than cross-DB parallelism would be a win for the
vast majority of users. We could imagine some future feature that
lets pg_upgrade try to slice up the available jobs on its own
(say, based on a preliminary survey of how many tables in each DB).
But I don't want to build that today, and maybe we won't ever.

regards, tom lane

#457Tom Lane
tgl@sss.pgh.pa.us
In reply to: Andres Freund (#455)
Re: Statistics Import and Export

Andres Freund <andres@anarazel.de> writes:

On 2025-03-06 13:47:34 -0500, Tom Lane wrote:

... I wonder if we could just rip out pg_upgrade's support
for DB-level parallelism, which is not terribly pretty anyway, and
simply pass the -j switch straight to pg_dump and pg_restore.

I don't think that'd work well, right now pg_dump only handles a single
database (pg_dumpall doesn't yet support -Fc) *and* pg_dump is still serial
for the bulk of the work that pg_upgrade cares about.
I think the only parallelism that'd actually happen for pg_upgrade would be
dumping of large objects?

Uh ... the entire point here is that we'd be trying to parallelize its
dumping of stats, no? Most DBs will have enough of those to be
interesting, I should think.

regards, tom lane

#458Andres Freund
andres@anarazel.de
In reply to: Tom Lane (#456)
Re: Statistics Import and Export

Hi,

On 2025-03-06 14:47:08 -0500, Tom Lane wrote:

Nathan Bossart <nathandbossart@gmail.com> writes:

On Thu, Mar 06, 2025 at 01:47:34PM -0500, Tom Lane wrote:

... I wonder if we could just rip out pg_upgrade's support
for DB-level parallelism, which is not terribly pretty anyway, and
simply pass the -j switch straight to pg_dump and pg_restore.

That would certainly help for clusters with one big database with many LOs
or something, but I worry it would hurt the many database case quite a bit.

I'm very skeptical of that. How many DBs do you know with just one table?
I think most have enough that they could keep a reasonable number of
CPUs busy with pg_dump's internal parallelism.

pg_dump as used by pg_upgrade doesn't need to dump table data. Afaict we only
do parallelism in pg_dump for table data and large objects. Outside of the
many-LOs case, there's nothing pg_dump's internal parallelism can accelerate?

So the number of tables in a database is irrelevant, no?

Greetings,

Andres Freund

#459Andres Freund
andres@anarazel.de
In reply to: Tom Lane (#457)
Re: Statistics Import and Export

Hi,

On 2025-03-06 14:51:26 -0500, Tom Lane wrote:

Andres Freund <andres@anarazel.de> writes:

On 2025-03-06 13:47:34 -0500, Tom Lane wrote:

... I wonder if we could just rip out pg_upgrade's support
for DB-level parallelism, which is not terribly pretty anyway, and
simply pass the -j switch straight to pg_dump and pg_restore.

I don't think that'd work well, right now pg_dump only handles a single
database (pg_dumpall doesn't yet support -Fc) *and* pg_dump is still serial
for the bulk of the work that pg_upgrade cares about.
I think the only parallelism that'd actually happen for pg_upgrade would be
dumping of large objects?

Uh ... the entire point here is that we'd be trying to parallelize its
dumping of stats, no? Most DBs will have enough of those to be
interesting, I should think.

Well, we added concurrent-pg-dump runs to pg_upgrade for a reason,
presumably. Before stats got dumped, there wasn't any benefit of pg_dump level
parallelism, unless large objects are used. Presumably we validated that there
*is* gain from running pg_dump on multiple databases concurrently.

There are many systems with hundreds of databases, removing all parallelism
for those from pg_upgrade would likely hurt way more than what we can gain
here.

Greetings,

Andres Freund

#460Nathan Bossart
nathandbossart@gmail.com
In reply to: Andres Freund (#459)
Re: Statistics Import and Export

On Thu, Mar 06, 2025 at 03:20:16PM -0500, Andres Freund wrote:

There are many systems with hundreds of databases, removing all parallelism
for those from pg_upgrade would likely hurt way more than what we can gain
here.

I just did a quick test on a freshly analyzed database with 1,000 sequences
and 10,000 tables with 1,000 rows and 2 unique constraints apiece.

~/pgdata$ time pg_dump postgres --no-data --binary-upgrade > /dev/null
0.29s user 0.09s system 21% cpu 1.777 total

~/pgdata$ time pg_dump postgres --no-data --no-statistics --binary-upgrade > /dev/null
0.14s user 0.02s system 25% cpu 0.603 total

So about 1.174 seconds goes to statistics. Even if we do all sorts of work
to make dumping statistics really fast, dumping 8 in succession would still
take upwards of 4.8 seconds or more. Even with the current code, dumping 8
in parallel would probably take closer to 2 seconds, and I bet reducing the
number of statistics queries could drive it below 1. Granted, I'm waving
my hands vigorously with those last two estimates.

That being said, I do think in-database parallelism would be useful in some
cases. I frequently hear about problems with huge numbers of large objects
on a cluster with one big database. But that's probably less likely than
the many database case.

--
nathan

#461Robert Haas
robertmhaas@gmail.com
In reply to: Nathan Bossart (#460)
Re: Statistics Import and Export

On Thu, Mar 6, 2025 at 3:50 PM Nathan Bossart <nathandbossart@gmail.com> wrote:

That being said, I do think in-database parallelism would be useful in some
cases. I frequently hear about problems with huge numbers of large objects
on a cluster with one big database. But that's probably less likely than
the many database case.

I could believe they're equally likely, or even that the
many-large-objects case is more likely. At any rate, they're both
things that can happen.

--
Robert Haas
EDB: http://www.enterprisedb.com

#462Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#436)
Re: Statistics Import and Export

On Thu, 2025-03-06 at 08:49 -0500, Corey Huinker wrote:

Unless some check was being done by the 'foo.bar'::regclass cast, I
understand why we should add one.

"For schemas, allows access to objects contained in the schema
(assuming that the objects' own privilege requirements are also met).
Essentially this allows the grantee to “look up” objects within the
schema. Without this permission, it is still possible to see the object
names, e.g., by querying system catalogs. Also, after revoking this
permission, existing sessions might have statements that have
previously performed this lookup, so this is not a completely secure
way to prevent object access."

https://www.postgresql.org/docs/current/ddl-priv.html

The above text indicates that we should do the check, but also that
it's not terribly important for actual security.

If we do, we'll want to change downgrade the following errors to
warn+return false:

Perhaps we should consider the schemaname/relname change as one patch,
which maintains relation lookup failures as hard ERRORs, and a
"downgrade errors to warnings" as a separate patch.

I agree, but the thread conversation had already shifted to doing
just one single call to pg_stats, so this was just a demonstration.

It's a simple patch and the discussion seems to be shifting toward
parallelism[1]/messages/by-id/714295.1741286854@sss.pgh.pa.us rather than batching[2] /messages/by-id/716907.1741288132@sss.pgh.pa.us. In that case it still seems
like a good change to me, so I'm inclined to commit it after I verify
that it improves performance.

Regards,
Jeff Davis

[1]: /messages/by-id/714295.1741286854@sss.pgh.pa.us
/messages/by-id/714295.1741286854@sss.pgh.pa.us

[2]:  /messages/by-id/716907.1741288132@sss.pgh.pa.us

#463Jeff Davis
pgsql@j-davis.com
In reply to: Robert Haas (#438)
1 attachment(s)
Re: Statistics Import and Export

On Thu, 2025-03-06 at 11:15 -0500, Robert Haas wrote:

To be honest, I am a bit surprised that we decided to enable this by
default. It's not obvious to me that statistics should be regarded as
part of the database in the same way that table definitions or table
data are. That said, I'm not overwhelmingly opposed to that choice.
However, even if it's the right choice in theory, we should maybe
rethink if it's going to be too slow or use too much memory.

I don't have a strong opinion about whether stats will be opt-out or
opt-in for v18, but if they are opt-in, we would need to adjust the
available options a bit.

At minimum, we would need to at least add the option "--with-
statistics", because right now the only way to explicitly request stats
is to say "--statistics-only".

To generalize this concept: for each of {schema, data, stats} users
might want "yes", "no", or "only".

If we use this options scheme, it would be easy to change the default
for stats independently of the other options, if necessary, without
surprising consequences.

Patch attached. This patch does NOT change the default; stats are still
opt-out. But it makes it easier for users to start specifying what they
want or not explicitly, or to rely on the defaults if they prefer.

Note that the patch would mean we go from 2 options in v17:
--{schema|data}-only

to 9 options in v18:
--{with|no}-{schema|data|stats} and
--{schema|data|stats}-only

I suggest we adjust the options now with something resembling the
attached patch and decide on changing the default sometime during beta.

Regards,
Jeff Davis

Attachments:

v1-0001-Add-pg_dump-with-X-options.patchtext/x-patch; charset=UTF-8; name=v1-0001-Add-pg_dump-with-X-options.patchDownload
From c47fc9e570ddd083097f4bfc708465cf644f48c2 Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Thu, 6 Mar 2025 17:35:41 -0800
Subject: [PATCH v1] Add pg_dump --with-X options.

---
 doc/src/sgml/ref/pg_dump.sgml    | 27 +++++++++++++++++++
 doc/src/sgml/ref/pg_dumpall.sgml | 27 +++++++++++++++++++
 doc/src/sgml/ref/pg_restore.sgml | 27 +++++++++++++++++++
 src/bin/pg_dump/pg_dump.c        | 46 +++++++++++++++++++++++++++++---
 src/bin/pg_dump/pg_dumpall.c     | 12 +++++++++
 src/bin/pg_dump/pg_restore.c     | 44 +++++++++++++++++++++++++-----
 6 files changed, 173 insertions(+), 10 deletions(-)

diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index 1975054d7bf..9eba285687e 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -1223,6 +1223,33 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--with-data</option></term>
+      <listitem>
+       <para>
+        Dump data. This is the default.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--with-schema</option></term>
+      <listitem>
+       <para>
+        Dump schema (data definitions). This is the default.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--with-statistics</option></term>
+      <listitem>
+       <para>
+        Dump statistics. This is the default.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--on-conflict-do-nothing</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_dumpall.sgml b/doc/src/sgml/ref/pg_dumpall.sgml
index c2fa5be9519..45f127f0dc9 100644
--- a/doc/src/sgml/ref/pg_dumpall.sgml
+++ b/doc/src/sgml/ref/pg_dumpall.sgml
@@ -551,6 +551,33 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--with-data</option></term>
+      <listitem>
+       <para>
+        Dump data. This is the default.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--with-schema</option></term>
+      <listitem>
+       <para>
+        Dump schema (data definitions). This is the default.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--with-statistics</option></term>
+      <listitem>
+       <para>
+        Dump statistics. This is the default.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-unlogged-table-data</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index 199ea3345f3..51e6411c8fe 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -795,6 +795,33 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--with-data</option></term>
+      <listitem>
+       <para>
+        Dump data. This is the default.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--with-schema</option></term>
+      <listitem>
+       <para>
+        Dump schema (data definitions). This is the default.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--with-statistics</option></term>
+      <listitem>
+       <para>
+        Dump statistics. This is the default.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
        <term><option>--section=<replaceable class="parameter">sectionname</replaceable></option></term>
        <listitem>
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 4f4ad2ee150..31c4ac1ee57 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -433,6 +433,9 @@ main(int argc, char **argv)
 	bool		data_only = false;
 	bool		schema_only = false;
 	bool		statistics_only = false;
+	bool		with_data = false;
+	bool		with_schema = false;
+	bool		with_statistics = false;
 	bool		no_data = false;
 	bool		no_schema = false;
 	bool		no_statistics = false;
@@ -508,6 +511,9 @@ main(int argc, char **argv)
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
 		{"no-sync", no_argument, NULL, 7},
+		{"with-data", no_argument, NULL, 22},
+		{"with-schema", no_argument, NULL, 23},
+		{"with-statistics", no_argument, NULL, 24},
 		{"on-conflict-do-nothing", no_argument, &dopt.do_nothing, 1},
 		{"rows-per-insert", required_argument, NULL, 10},
 		{"include-foreign-data", required_argument, NULL, 11},
@@ -776,6 +782,18 @@ main(int argc, char **argv)
 				no_statistics = true;
 				break;
 
+			case 22:
+				with_data = true;
+				break;
+
+			case 23:
+				with_schema = true;
+				break;
+
+			case 24:
+				with_statistics = true;
+				break;
+
 			default:
 				/* getopt_long already emitted a complaint */
 				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -811,6 +829,7 @@ main(int argc, char **argv)
 	if (dopt.binary_upgrade)
 		dopt.sequence_data = 1;
 
+	/* reject conflicting "-only" options */
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
 	if (schema_only && statistics_only)
@@ -818,6 +837,7 @@ main(int argc, char **argv)
 	if (data_only && statistics_only)
 		pg_fatal("options -a/--data-only and --statistics-only cannot be used together");
 
+	/* reject conflicting "-only" and "no-" options */
 	if (data_only && no_data)
 		pg_fatal("options -a/--data-only and --no-data cannot be used together");
 	if (schema_only && no_schema)
@@ -825,6 +845,14 @@ main(int argc, char **argv)
 	if (statistics_only && no_statistics)
 		pg_fatal("options --statistics-only and --no-statistics cannot be used together");
 
+	/* reject conflicting "with-" and "no-" options */
+	if (with_data && no_data)
+		pg_fatal("options --with-data and --no-data cannot be used together");
+	if (with_schema && no_schema)
+		pg_fatal("options --with-schema and --no-schema cannot be used together");
+	if (with_statistics && no_statistics)
+		pg_fatal("options --with-statistics and --no-statistics cannot be used together");
+
 	if (schema_only && foreign_servers_include_patterns.head != NULL)
 		pg_fatal("options -s/--schema-only and --include-foreign-data cannot be used together");
 
@@ -837,10 +865,20 @@ main(int argc, char **argv)
 	if (dopt.if_exists && !dopt.outputClean)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
-	/* set derivative flags */
-	dopt.dumpData = data_only || (!schema_only && !statistics_only && !no_data);
-	dopt.dumpSchema = schema_only || (!data_only && !statistics_only && !no_schema);
-	dopt.dumpStatistics = statistics_only || (!data_only && !schema_only && !no_statistics);
+	/*
+	 * Set derivative flags. An "-only" option may be overridden by an
+	 * explicit "with-" option; e.g. "--schema-only --with-statistics" will
+	 * include schema and statistics. Other ambiguous or nonsensical
+	 * combinations, e.g. "--schema-only --no-schema", will have already
+	 * caused an error in one of the checks above.
+	 */
+	dopt.dumpData = ((dopt.dumpData && !schema_only && !statistics_only) ||
+					 (data_only || with_data)) && !no_data;
+	dopt.dumpSchema = ((dopt.dumpSchema && !data_only && !statistics_only) ||
+					   (schema_only || with_schema)) && !no_schema;
+	dopt.dumpStatistics = ((dopt.dumpStatistics && !schema_only && !data_only) ||
+						   (statistics_only || with_statistics)) && !no_statistics;
+
 
 	/*
 	 * --inserts are already implied above if --column-inserts or
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index e0867242526..a7e8c0d2ad5 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -110,6 +110,9 @@ static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
 static int	no_role_passwords = 0;
+static int	with_data = 0;
+static int	with_schema = 0;
+static int	with_statistics = 0;
 static int	server_version;
 static int	load_via_partition_root = 0;
 static int	on_conflict_do_nothing = 0;
@@ -182,6 +185,9 @@ main(int argc, char *argv[])
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
+		{"with-data", no_argument, &with_data, 1},
+		{"with-schema", no_argument, &with_schema, 1},
+		{"with-statistics", no_argument, &with_statistics, 1},
 		{"on-conflict-do-nothing", no_argument, &on_conflict_do_nothing, 1},
 		{"rows-per-insert", required_argument, NULL, 7},
 		{"statistics-only", no_argument, &statistics_only, 1},
@@ -471,6 +477,12 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-toast-compression");
 	if (no_unlogged_table_data)
 		appendPQExpBufferStr(pgdumpopts, " --no-unlogged-table-data");
+	if (with_data)
+		appendPQExpBufferStr(pgdumpopts, " --with-data");
+	if (with_schema)
+		appendPQExpBufferStr(pgdumpopts, " --with-schema");
+	if (with_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --with-statistics");
 	if (on_conflict_do_nothing)
 		appendPQExpBufferStr(pgdumpopts, " --on-conflict-do-nothing");
 	if (statistics_only)
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 13e4dc507e0..f22046127b7 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -81,6 +81,9 @@ main(int argc, char **argv)
 	static int	no_subscriptions = 0;
 	static int	strict_names = 0;
 	static int	statistics_only = 0;
+	static int	with_data = 0;
+	static int	with_schema = 0;
+	static int	with_statistics = 0;
 
 	struct option cmdopts[] = {
 		{"clean", 0, NULL, 'c'},
@@ -134,6 +137,9 @@ main(int argc, char **argv)
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"no-statistics", no_argument, &no_statistics, 1},
+		{"with-data", no_argument, &with_data, 1},
+		{"with-schema", no_argument, &with_schema, 1},
+		{"with-statistics", no_argument, &with_statistics, 1},
 		{"statistics-only", no_argument, &statistics_only, 1},
 		{"filter", required_argument, NULL, 4},
 
@@ -349,12 +355,29 @@ main(int argc, char **argv)
 		opts->useDB = 1;
 	}
 
+	/* reject conflicting "-only" options */
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
-	if (data_only && statistics_only)
-		pg_fatal("options -a/--data-only and --statistics-only cannot be used together");
 	if (schema_only && statistics_only)
 		pg_fatal("options -s/--schema-only and --statistics-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and --statistics-only cannot be used together");
+
+	/* reject conflicting "-only" and "no-" options */
+	if (data_only && no_data)
+		pg_fatal("options -a/--data-only and --no-data cannot be used together");
+	if (schema_only && no_schema)
+		pg_fatal("options -s/--schema-only and --no-schema cannot be used together");
+	if (statistics_only && no_statistics)
+		pg_fatal("options --statistics-only and --no-statistics cannot be used together");
+
+	/* reject conflicting "with-" and "no-" options */
+	if (with_data && no_data)
+		pg_fatal("options --with-data and --no-data cannot be used together");
+	if (with_schema && no_schema)
+		pg_fatal("options --with-schema and --no-schema cannot be used together");
+	if (with_statistics && no_statistics)
+		pg_fatal("options --with-statistics and --no-statistics cannot be used together");
 
 	if (data_only && opts->dropSchema)
 		pg_fatal("options -c/--clean and -a/--data-only cannot be used together");
@@ -373,10 +396,19 @@ main(int argc, char **argv)
 	if (opts->single_txn && numWorkers > 1)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
-	/* set derivative flags */
-	opts->dumpData = data_only || (!no_data && !schema_only && !statistics_only);
-	opts->dumpSchema = schema_only || (!no_schema && !data_only && !statistics_only);
-	opts->dumpStatistics = statistics_only || (!no_statistics && !data_only && !schema_only);
+	/*
+	 * Set derivative flags. An "-only" option may be overridden by an
+	 * explicit "with-" option; e.g. "--schema-only --with-statistics" will
+	 * include schema and statistics. Other ambiguous or nonsensical
+	 * combinations, e.g. "--schema-only --no-schema", will have already
+	 * caused an error in one of the checks above.
+	 */
+	opts->dumpData = ((opts->dumpData && !schema_only && !statistics_only) ||
+					  (data_only || with_data)) && !no_data;
+	opts->dumpSchema = ((opts->dumpSchema && !data_only && !statistics_only) ||
+						(schema_only || with_schema)) && !no_schema;
+	opts->dumpStatistics = ((opts->dumpStatistics && !schema_only && !data_only) ||
+							(statistics_only || with_statistics)) && !no_statistics;
 
 	opts->disable_triggers = disable_triggers;
 	opts->enable_row_security = enable_row_security;
-- 
2.34.1

#464Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#462)
Re: Statistics Import and Export

https://www.postgresql.org/docs/current/ddl-priv.html

The above text indicates that we should do the check, but also that
it's not terribly important for actual security.

Ok, I'm convinced.

If we do, we'll want to change downgrade the following errors to
warn+return false:

Perhaps we should consider the schemaname/relname change as one patch,
which maintains relation lookup failures as hard ERRORs, and a
"downgrade errors to warnings" as a separate patch.

+1

#465Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#463)
Re: Statistics Import and Export

Patch attached. This patch does NOT change the default; stats are still
opt-out. But it makes it easier for users to start specifying what they
want or not explicitly, or to rely on the defaults if they prefer.

Note that the patch would mean we go from 2 options in v17:
--{schema|data}-only

to 9 options in v18:
--{with|no}-{schema|data|stats} and
--{schema|data|stats}-only

I suggest we adjust the options now with something resembling the
attached patch and decide on changing the default sometime during beta.

Patch is straightforward. Comments are very clear as are docs. I can't see
anything that needs to be changed.

#466Andres Freund
andres@anarazel.de
In reply to: Jeff Davis (#463)
Re: Statistics Import and Export

Hi,

On 2025-03-06 17:42:30 -0800, Jeff Davis wrote:

At minimum, we would need to at least add the option "--with-
statistics", because right now the only way to explicitly request stats
is to say "--statistics-only".

+1, this has been annoying me while testing.

I did get confused for a while because I used --statistics, as the opposite of
--no-statistics, while going back and forth between the two. Kinda appears to
work, but actually means --statistics-only, something rather different...

To generalize this concept: for each of {schema, data, stats} users
might want "yes", "no", or "only".

If we use this options scheme, it would be easy to change the default
for stats independently of the other options, if necessary, without
surprising consequences.

Patch attached. This patch does NOT change the default; stats are still
opt-out. But it makes it easier for users to start specifying what they
want or not explicitly, or to rely on the defaults if they prefer.

Note that the patch would mean we go from 2 options in v17:
--{schema|data}-only

to 9 options in v18:
--{with|no}-{schema|data|stats} and
--{schema|data|stats}-only

Could we, instead of having --with-$foo, just use --$foo?

Greetings,

Andres Freund

#467Jeff Davis
pgsql@j-davis.com
In reply to: Andres Freund (#466)
Re: Statistics Import and Export

On Fri, 2025-03-07 at 11:22 -0500, Andres Freund wrote:

+1, this has been annoying me while testing.

IIRC, originally someone had questioned the need for options that
expressed what was already the default, but I can't find it right now.
Regardless, now the need is clear enough.

Could we, instead of having --with-$foo, just use --$foo?

That creates a conflict with the existing --schema option, which is a
namespace filter.

Another idea: we could use --definitions/--data/--statistics.

Regards,
Jeff Davis

#468Robert Treat
rob@xzilla.net
In reply to: Jeff Davis (#463)
Re: Statistics Import and Export

On Thu, Mar 6, 2025 at 8:42 PM Jeff Davis <pgsql@j-davis.com> wrote:

On Thu, 2025-03-06 at 11:15 -0500, Robert Haas wrote:
Patch attached. This patch does NOT change the default; stats are still
opt-out. But it makes it easier for users to start specifying what they
want or not explicitly, or to rely on the defaults if they prefer.

Note that the patch would mean we go from 2 options in v17:
--{schema|data}-only

to 9 options in v18:
--{with|no}-{schema|data|stats} and
--{schema|data|stats}-only

Ugh... this feels like a bit of the combinatorial explosion,
especially if we ever need to add another option. I wonder if it would
be possible to do something simple like just providing
"--include={schema|data|stats}" where you specify only what you want,
and leave out what you don't. At the risk of not providing as many
typing shortcuts, if the logic is simpler and more extensible for
future options...

Robert Treat
https://xzilla.net

#469Jeff Davis
pgsql@j-davis.com
In reply to: Robert Treat (#468)
Re: Statistics Import and Export

On Fri, 2025-03-07 at 12:41 -0500, Robert Treat wrote:

Ugh... this feels like a bit of the combinatorial explosion,
especially if we ever need to add another option.

Not quite that bad, because ideally the yes/no/only would not be
expanding as well. But I agree that it feels like a lot of options.

I wonder if it would
be possible to do something simple like just providing
"--include={schema|data|stats}" where you specify only what you want,
and leave out what you don't.

Can you explain the idea in a bit more detail? Does --
include=statistics mean include statistics also or statistics only? Can
you explicitly request that data be included but rely on the default
for statistics? What options would it override or conflict with?

Regards,
Jeff Davis

#470Tom Lane
tgl@sss.pgh.pa.us
In reply to: Jeff Davis (#433)
Re: Statistics Import and Export: difference in statistics dumped

Jeff Davis <pgsql@j-davis.com> writes:

Sounds good. I will commit something like the v2 patch then soon, and
if we need a different condition we can change it later.

Sadly, this made things worse not better: crake is failing
cross-version-upgrade tests again [1]https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=crake&amp;dt=2025-03-07%2018%3A19%3A14, with dump diffs like

@@ -270836,8 +270836,8 @@
 --
 SELECT * FROM pg_catalog.pg_restore_relation_stats(	'version', '000000'::integer,
 	'relation', 'public.hash_f8_index'::regclass,
-	'relpages', '66'::integer,
-	'reltuples', '10000'::real,
+	'relpages', '0'::integer,
+	'reltuples', '-1'::real,
 	'relallvisible', '0'::integer
 );

I think what is happening is that the patch shut off CREATE
INDEX's update of not only the table's stats but also the
index's stats. This seems unhelpful: the index's empty
stats can never be what's wanted.

We could band-aid over this by making AdjustUpgrade.pm
lobotomize the comparisons of all three stats fields,
but I think it's just wrong as-is. Perhaps fix by
checking the relation's relkind before applying the
autovacuum heuristic?

regards, tom lane

[1]: https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=crake&amp;dt=2025-03-07%2018%3A19%3A14

#471Robert Treat
rob@xzilla.net
In reply to: Jeff Davis (#469)
Re: Statistics Import and Export

On Fri, Mar 7, 2025 at 1:41 PM Jeff Davis <pgsql@j-davis.com> wrote:

On Fri, 2025-03-07 at 12:41 -0500, Robert Treat wrote:

Ugh... this feels like a bit of the combinatorial explosion,
especially if we ever need to add another option.

Not quite that bad, because ideally the yes/no/only would not be
expanding as well. But I agree that it feels like a lot of options.

I wonder if it would
be possible to do something simple like just providing
"--include={schema|data|stats}" where you specify only what you want,
and leave out what you don't.

Can you explain the idea in a bit more detail? Does --
include=statistics mean include statistics also or statistics only? Can
you explicitly request that data be included but rely on the default
for statistics? What options would it override or conflict with?

There might be some variability depending on the default behavior, but
if we assume that default means "output everything" (which is the v17
behavior), then use of --include would mean to only include items that
are listed, so:

if you want everything --include=schema,data,statistics (presumably
redundant with the default behavior)
if you want schema only --include=schema
if you want "everything except schema" --include=data,statistics

So it's pretty easy to extrapolate data only or statistics only, and
pretty easy to work up any combo of 2 of the 3.

And if someday, for example, there is ever agreement on including role
information with normal pg_dump, you add "roles" as an option to be
parsed via --include without having to create any new flags.

Robert Treat
https://xzilla.net

#472Jeff Davis
pgsql@j-davis.com
In reply to: Robert Treat (#471)
Re: Statistics Import and Export

On Fri, 2025-03-07 at 15:46 -0500, Robert Treat wrote:

There might be some variability depending on the default behavior,
but
if we assume that default means "output everything"

The reason I posted this patch is that, depending on performance
characteristics in v18 and a decision to be made during beta, the
default may not output statistics.

So we need whatever set of options we choose to have the freedom to
change statistics to be either opt-in or opt-out, without needing to
reconsider the overall set of options.

I tried to generalize that requirement to all of
{schema|data|statistics} for consistency, but that resulted in 9
options.

We don't need the options to be perfectly consistent at the expense of
usability, though, so if 9 options is too many we can just have three
new options for stats, for a total of 5 options:

--data-only
--schema-only
--statistics-only
--statistics (stats also, regardless of default)
--no-statistics (no stats, regardless of default)

which would allow combinations like "--schema-only --statistics" to
mean "schema and statistics but not data". There would be a bit of
weirdness because --statistics can combine with --data-only and --
schema-only, but nothing can combine with --statistics-only.

if you want everything --include=schema,data,statistics (presumably
redundant with the default behavior)
if you want schema only --include=schema
if you want "everything except schema" --include=data,statistics

That could work. Comparing to the options above yields:

--include=statistics <=> --statistics-only
--include=schema,data,statistics <=> --statistics
--include=schema,statistics <=> --schema-only --statistics
--include=data,statistics <=> --data-only --statistics
--include=schema,data <=> --no-statistics

Not sure which approach is better.

Regards,
Jeff Davis

#473Corey Huinker
corey.huinker@gmail.com
In reply to: Robert Treat (#471)
Re: Statistics Import and Export

if you want everything --include=schema,data,statistics (presumably
redundant with the default behavior)
if you want schema only --include=schema
if you want "everything except schema" --include=data,statistics

Until we add a fourth option, and then it becomes completely ambiguous as
to whether you wanted data+statstics, or you not-wanted schema.

And if someday, for example, there is ever agreement on including role

information with normal pg_dump, you add "roles" as an option to be
parsed via --include without having to create any new flags.

This is pushing a burden onto our customers for a parsing convenience.

-1.

#474Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#472)
Re: Statistics Import and Export

I tried to generalize that requirement to all of
{schema|data|statistics} for consistency, but that resulted in 9
options.

9 options that resolve to 3 boolean variables. It's not that hard.

And if we add a fourth option set, then we have 12 options. So it's O(3N),
not O(N^2).

People have scripts now that rely on the existing -only flags, and nearly
every other potentially optional thing has a -no flag. Let's leverage that.

#475Hari Krishna Sunder
hari.db.pg@gmail.com
In reply to: Corey Huinker (#474)
Re: Statistics Import and Export

To improve the performance of pg_dump can we add a new sql function that
can operate more efficiently than the pg_stats view? It could also take in
an optional list of oids to filter on.
This will help speed up the dump and restore within pg18 and future
upgrades to higher pg versions.

Thanks
Hari Krishna Sunder

On Fri, Mar 7, 2025 at 7:43 PM Corey Huinker <corey.huinker@gmail.com>
wrote:

Show quoted text

I tried to generalize that requirement to all of

{schema|data|statistics} for consistency, but that resulted in 9
options.

9 options that resolve to 3 boolean variables. It's not that hard.

And if we add a fourth option set, then we have 12 options. So it's O(3N),
not O(N^2).

People have scripts now that rely on the existing -only flags, and nearly
every other potentially optional thing has a -no flag. Let's leverage that.

#476Corey Huinker
corey.huinker@gmail.com
In reply to: Hari Krishna Sunder (#475)
Re: Statistics Import and Export

On Sat, Mar 8, 2025 at 12:52 AM Hari Krishna Sunder <hari.db.pg@gmail.com>
wrote:

To improve the performance of pg_dump can we add a new sql function that
can operate more efficiently than the pg_stats view? It could also take in
an optional list of oids to filter on.
This will help speed up the dump and restore within pg18 and future
upgrades to higher pg versions.

We can't install functions on the source database - it might be a read
replica.

#477Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#476)
2 attachment(s)
Re: Statistics Import and Export

Updated and rebase patches.

0001 is the same as v6-0002, but with proper ACL checks on schemas after
cache lookup

0002 attempts to replace all possible ERRORs in the restore/clear functions
with WARNINGs. This is done with an eye towards reducing the set of things
that could potentially cause an upgrade to fail.

Spoke with Nathan about how best to batch the pg_stats fetches. I'll be
working on that now. Given that, the patch that optimized out
getAttributeStats() calls on indexes without expressions has been
withdrawn. It's a clear incremental gain, and we're looking for a couple
orders of magnitude gain.

Attachments:

v7-0001-Split-relation-into-schemaname-and-relname.patchtext/x-patch; charset=US-ASCII; name=v7-0001-Split-relation-into-schemaname-and-relname.patchDownload
From 9cd4b4e0e280d0fd8cb120ac105d6e65a491cd7e Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Tue, 4 Mar 2025 22:16:52 -0500
Subject: [PATCH v7 1/2] Split relation into schemaname and relname.

In order to further reduce potential error-failures in restores and
upgrades, replace the numerous casts of fully qualified relation names
into their schema+relname text components.

Further remove the ::name casts on attname and change the expected
datatype to text.

Add an ACL_USAGE check on the namespace oid after it is looked up.
---
 src/include/catalog/pg_proc.dat            |   8 +-
 src/include/statistics/stat_utils.h        |   2 +
 src/backend/statistics/attribute_stats.c   |  87 ++++--
 src/backend/statistics/relation_stats.c    |  65 +++--
 src/backend/statistics/stat_utils.c        |  37 +++
 src/bin/pg_dump/pg_dump.c                  |  25 +-
 src/bin/pg_dump/t/002_pg_dump.pl           |   6 +-
 src/test/regress/expected/stats_import.out | 307 +++++++++++++--------
 src/test/regress/sql/stats_import.sql      | 276 +++++++++++-------
 doc/src/sgml/func.sgml                     |  41 +--
 10 files changed, 566 insertions(+), 288 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index cede992b6e2..fdd4b8d7dba 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12443,8 +12443,8 @@
   descr => 'clear statistics on relation',
   proname => 'pg_clear_relation_stats', provolatile => 'v', proisstrict => 'f',
   proparallel => 'u', prorettype => 'void',
-  proargtypes => 'regclass',
-  proargnames => '{relation}',
+  proargtypes => 'text text',
+  proargnames => '{schemaname,relname}',
   prosrc => 'pg_clear_relation_stats' },
 { oid => '8461',
   descr => 'restore statistics on attribute',
@@ -12459,8 +12459,8 @@
   descr => 'clear statistics on attribute',
   proname => 'pg_clear_attribute_stats', provolatile => 'v', proisstrict => 'f',
   proparallel => 'u', prorettype => 'void',
-  proargtypes => 'regclass name bool',
-  proargnames => '{relation,attname,inherited}',
+  proargtypes => 'text text text bool',
+  proargnames => '{schemaname,relname,attname,inherited}',
   prosrc => 'pg_clear_attribute_stats' },
 
 # GiST stratnum implementations
diff --git a/src/include/statistics/stat_utils.h b/src/include/statistics/stat_utils.h
index 0eb4decfcac..cad042c8e4a 100644
--- a/src/include/statistics/stat_utils.h
+++ b/src/include/statistics/stat_utils.h
@@ -32,6 +32,8 @@ extern bool stats_check_arg_pair(FunctionCallInfo fcinfo,
 
 extern void stats_lock_check_privileges(Oid reloid);
 
+extern Oid stats_schema_check_privileges(const char *nspname);
+
 extern bool stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
 											 FunctionCallInfo positional_fcinfo,
 											 struct StatsArgInfo *arginfo);
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index 6bcbee0edba..f87db2d6102 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -36,7 +36,8 @@
 
 enum attribute_stats_argnum
 {
-	ATTRELATION_ARG = 0,
+	ATTRELSCHEMA_ARG = 0,
+	ATTRELNAME_ARG,
 	ATTNAME_ARG,
 	ATTNUM_ARG,
 	INHERITED_ARG,
@@ -58,8 +59,9 @@ enum attribute_stats_argnum
 
 static struct StatsArgInfo attarginfo[] =
 {
-	[ATTRELATION_ARG] = {"relation", REGCLASSOID},
-	[ATTNAME_ARG] = {"attname", NAMEOID},
+	[ATTRELSCHEMA_ARG] = {"schemaname", TEXTOID},
+	[ATTRELNAME_ARG] = {"relname", TEXTOID},
+	[ATTNAME_ARG] = {"attname", TEXTOID},
 	[ATTNUM_ARG] = {"attnum", INT2OID},
 	[INHERITED_ARG] = {"inherited", BOOLOID},
 	[NULL_FRAC_ARG] = {"null_frac", FLOAT4OID},
@@ -80,7 +82,8 @@ static struct StatsArgInfo attarginfo[] =
 
 enum clear_attribute_stats_argnum
 {
-	C_ATTRELATION_ARG = 0,
+	C_ATTRELSCHEMA_ARG = 0,
+	C_ATTRELNAME_ARG,
 	C_ATTNAME_ARG,
 	C_INHERITED_ARG,
 	C_NUM_ATTRIBUTE_STATS_ARGS
@@ -88,8 +91,9 @@ enum clear_attribute_stats_argnum
 
 static struct StatsArgInfo cleararginfo[] =
 {
-	[C_ATTRELATION_ARG] = {"relation", REGCLASSOID},
-	[C_ATTNAME_ARG] = {"attname", NAMEOID},
+	[C_ATTRELSCHEMA_ARG] = {"relation", TEXTOID},
+	[C_ATTRELNAME_ARG] = {"relation", TEXTOID},
+	[C_ATTNAME_ARG] = {"attname", TEXTOID},
 	[C_INHERITED_ARG] = {"inherited", BOOLOID},
 	[C_NUM_ATTRIBUTE_STATS_ARGS] = {0}
 };
@@ -133,6 +137,9 @@ static void init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
 static bool
 attribute_statistics_update(FunctionCallInfo fcinfo)
 {
+	char	   *nspname;
+	Oid			nspoid;
+	char	   *relname;
 	Oid			reloid;
 	char	   *attname;
 	AttrNumber	attnum;
@@ -170,8 +177,23 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 	bool		result = true;
 
-	stats_check_required_arg(fcinfo, attarginfo, ATTRELATION_ARG);
-	reloid = PG_GETARG_OID(ATTRELATION_ARG);
+	stats_check_required_arg(fcinfo, attarginfo, ATTRELSCHEMA_ARG);
+	stats_check_required_arg(fcinfo, attarginfo, ATTRELNAME_ARG);
+
+	nspname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELSCHEMA_ARG));
+	nspoid = stats_schema_check_privileges(nspname);
+	if (nspoid == InvalidOid)
+		return false;
+
+	relname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELNAME_ARG));
+	reloid = get_relname_relid(relname, nspoid);
+	if (reloid == InvalidOid)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("Relation \"%s\".\"%s\" not found.", nspname, relname)));
+		return false;
+	}
 
 	if (RecoveryInProgress())
 		ereport(ERROR,
@@ -185,21 +207,18 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	/* user can specify either attname or attnum, but not both */
 	if (!PG_ARGISNULL(ATTNAME_ARG))
 	{
-		Name		attnamename;
-
 		if (!PG_ARGISNULL(ATTNUM_ARG))
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("cannot specify both attname and attnum")));
-		attnamename = PG_GETARG_NAME(ATTNAME_ARG);
-		attname = NameStr(*attnamename);
+		attname = TextDatumGetCString(PG_GETARG_DATUM(ATTNAME_ARG));
 		attnum = get_attnum(reloid, attname);
 		/* note that this test covers attisdropped cases too: */
 		if (attnum == InvalidAttrNumber)
 			ereport(ERROR,
 					(errcode(ERRCODE_UNDEFINED_COLUMN),
-					 errmsg("column \"%s\" of relation \"%s\" does not exist",
-							attname, get_rel_name(reloid))));
+					 errmsg("column \"%s\" of relation \"%s\".\"%s\" does not exist",
+							attname, nspname, relname)));
 	}
 	else if (!PG_ARGISNULL(ATTNUM_ARG))
 	{
@@ -210,8 +229,8 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 			!SearchSysCacheExistsAttName(reloid, attname))
 			ereport(ERROR,
 					(errcode(ERRCODE_UNDEFINED_COLUMN),
-					 errmsg("column %d of relation \"%s\" does not exist",
-							attnum, get_rel_name(reloid))));
+					 errmsg("column %d of relation \"%s\".\"%s\" does not exist",
+							attnum, nspname, relname)));
 	}
 	else
 	{
@@ -900,13 +919,33 @@ init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
 Datum
 pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 {
+	char	   *nspname;
+	Oid			nspoid;
+	char	   *relname;
 	Oid			reloid;
-	Name		attname;
+	char	   *attname;
 	AttrNumber	attnum;
 	bool		inherited;
 
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELATION_ARG);
-	reloid = PG_GETARG_OID(C_ATTRELATION_ARG);
+	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELSCHEMA_ARG);
+	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELNAME_ARG);
+	stats_check_required_arg(fcinfo, cleararginfo, C_ATTNAME_ARG);
+	stats_check_required_arg(fcinfo, cleararginfo, C_INHERITED_ARG);
+
+	nspname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELSCHEMA_ARG));
+	nspoid = stats_schema_check_privileges(nspname);
+	if (!OidIsValid(nspoid))
+		return false;
+
+	relname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELNAME_ARG));
+	reloid = get_relname_relid(relname, nspoid);
+	if (!OidIsValid(reloid))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("Relation \"%s\".\"%s\" not found.", nspname, relname)));
+		return false;
+	}
 
 	if (RecoveryInProgress())
 		ereport(ERROR,
@@ -916,23 +955,21 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 
 	stats_lock_check_privileges(reloid);
 
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTNAME_ARG);
-	attname = PG_GETARG_NAME(C_ATTNAME_ARG);
-	attnum = get_attnum(reloid, NameStr(*attname));
+	attname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTNAME_ARG));
+	attnum = get_attnum(reloid, attname);
 
 	if (attnum < 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot clear statistics on system column \"%s\"",
-						NameStr(*attname))));
+						attname)));
 
 	if (attnum == InvalidAttrNumber)
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 errmsg("column \"%s\" of relation \"%s\" does not exist",
-						NameStr(*attname), get_rel_name(reloid))));
+						attname, get_rel_name(reloid))));
 
-	stats_check_required_arg(fcinfo, cleararginfo, C_INHERITED_ARG);
 	inherited = PG_GETARG_BOOL(C_INHERITED_ARG);
 
 	delete_pg_statistic(reloid, attnum, inherited);
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index 52dfa477187..fdc69bc93e2 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -19,9 +19,12 @@
 
 #include "access/heapam.h"
 #include "catalog/indexing.h"
+#include "catalog/namespace.h"
 #include "statistics/stat_utils.h"
+#include "utils/builtins.h"
 #include "utils/fmgroids.h"
 #include "utils/fmgrprotos.h"
+#include "utils/lsyscache.h"
 #include "utils/syscache.h"
 
 
@@ -32,7 +35,8 @@
 
 enum relation_stats_argnum
 {
-	RELATION_ARG = 0,
+	RELSCHEMA_ARG = 0,
+	RELNAME_ARG,
 	RELPAGES_ARG,
 	RELTUPLES_ARG,
 	RELALLVISIBLE_ARG,
@@ -42,7 +46,8 @@ enum relation_stats_argnum
 
 static struct StatsArgInfo relarginfo[] =
 {
-	[RELATION_ARG] = {"relation", REGCLASSOID},
+	[RELSCHEMA_ARG] = {"schemaname", TEXTOID},
+	[RELNAME_ARG] = {"relname", TEXTOID},
 	[RELPAGES_ARG] = {"relpages", INT4OID},
 	[RELTUPLES_ARG] = {"reltuples", FLOAT4OID},
 	[RELALLVISIBLE_ARG] = {"relallvisible", INT4OID},
@@ -59,6 +64,9 @@ static bool
 relation_statistics_update(FunctionCallInfo fcinfo)
 {
 	bool		result = true;
+	char	   *nspname;
+	Oid			nspoid;
+	char	   *relname;
 	Oid			reloid;
 	Relation	crel;
 	BlockNumber relpages = 0;
@@ -76,6 +84,32 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 	bool		nulls[4] = {0};
 	int			nreplaces = 0;
 
+	stats_check_required_arg(fcinfo, relarginfo, RELSCHEMA_ARG);
+	stats_check_required_arg(fcinfo, relarginfo, RELNAME_ARG);
+
+	nspname = TextDatumGetCString(PG_GETARG_DATUM(RELSCHEMA_ARG));
+	nspoid = stats_schema_check_privileges(nspname);
+	if (!OidIsValid(nspoid))
+		return false;
+
+	relname = TextDatumGetCString(PG_GETARG_DATUM(RELNAME_ARG));
+	reloid = get_relname_relid(relname, nspoid);
+	if (!OidIsValid(reloid))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("Relation \"%s\".\"%s\" not found.", nspname, relname)));
+		return false;
+	}
+
+	if (RecoveryInProgress())
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("recovery is in progress"),
+				 errhint("Statistics cannot be modified during recovery.")));
+
+	stats_lock_check_privileges(reloid);
+
 	if (!PG_ARGISNULL(RELPAGES_ARG))
 	{
 		relpages = PG_GETARG_UINT32(RELPAGES_ARG);
@@ -108,17 +142,6 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 		update_relallfrozen = true;
 	}
 
-	stats_check_required_arg(fcinfo, relarginfo, RELATION_ARG);
-	reloid = PG_GETARG_OID(RELATION_ARG);
-
-	if (RecoveryInProgress())
-		ereport(ERROR,
-				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
-				 errmsg("recovery is in progress"),
-				 errhint("Statistics cannot be modified during recovery.")));
-
-	stats_lock_check_privileges(reloid);
-
 	/*
 	 * Take RowExclusiveLock on pg_class, consistent with
 	 * vac_update_relstats().
@@ -187,20 +210,22 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 Datum
 pg_clear_relation_stats(PG_FUNCTION_ARGS)
 {
-	LOCAL_FCINFO(newfcinfo, 5);
+	LOCAL_FCINFO(newfcinfo, 6);
 
-	InitFunctionCallInfoData(*newfcinfo, NULL, 5, InvalidOid, NULL, NULL);
+	InitFunctionCallInfoData(*newfcinfo, NULL, 6, InvalidOid, NULL, NULL);
 
-	newfcinfo->args[0].value = PG_GETARG_OID(0);
+	newfcinfo->args[0].value = PG_GETARG_DATUM(0);
 	newfcinfo->args[0].isnull = PG_ARGISNULL(0);
-	newfcinfo->args[1].value = UInt32GetDatum(0);
-	newfcinfo->args[1].isnull = false;
-	newfcinfo->args[2].value = Float4GetDatum(-1.0);
+	newfcinfo->args[1].value = PG_GETARG_DATUM(1);
+	newfcinfo->args[1].isnull = PG_ARGISNULL(1);
+	newfcinfo->args[2].value = UInt32GetDatum(0);
 	newfcinfo->args[2].isnull = false;
-	newfcinfo->args[3].value = UInt32GetDatum(0);
+	newfcinfo->args[3].value = Float4GetDatum(-1.0);
 	newfcinfo->args[3].isnull = false;
 	newfcinfo->args[4].value = UInt32GetDatum(0);
 	newfcinfo->args[4].isnull = false;
+	newfcinfo->args[5].value = UInt32GetDatum(0);
+	newfcinfo->args[5].isnull = false;
 
 	relation_statistics_update(newfcinfo);
 	PG_RETURN_VOID();
diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c
index 9647f5108b3..e037d4994e8 100644
--- a/src/backend/statistics/stat_utils.c
+++ b/src/backend/statistics/stat_utils.c
@@ -18,7 +18,9 @@
 
 #include "access/relation.h"
 #include "catalog/index.h"
+#include "catalog/namespace.h"
 #include "catalog/pg_database.h"
+#include "catalog/pg_namespace.h"
 #include "funcapi.h"
 #include "miscadmin.h"
 #include "statistics/stat_utils.h"
@@ -213,6 +215,41 @@ stats_lock_check_privileges(Oid reloid)
 	relation_close(table, NoLock);
 }
 
+
+/*
+ * Resolve a schema name into an Oid, ensure that the user has usage privs on
+ * that schema.
+ */
+Oid
+stats_schema_check_privileges(const char *nspname)
+{
+	Oid			nspoid;
+	AclResult	aclresult;
+
+	nspoid = get_namespace_oid(nspname, true);
+
+	if (nspoid == InvalidOid)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_SCHEMA_NAME),
+				 errmsg("schema %s does not exist", nspname)));
+		return InvalidOid;
+	}
+
+	aclresult = object_aclcheck(NamespaceRelationId, nspoid, GetUserId(), ACL_USAGE);
+
+	if (aclresult != ACLCHECK_OK)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for schema %s", nspname)));
+		return InvalidOid;
+	}
+
+	return nspoid;
+}
+
+
 /*
  * Find the argument number for the given argument name, returning -1 if not
  * found.
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 4f4ad2ee150..6cf2c7d1fe4 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -10492,7 +10492,6 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 	PQExpBuffer out;
 	DumpId	   *deps = NULL;
 	int			ndeps = 0;
-	char	   *qualified_name;
 	char		reltuples_str[FLOAT_SHORTEST_DECIMAL_LEN];
 	int			i_attname;
 	int			i_inherited;
@@ -10558,15 +10557,16 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 
 	out = createPQExpBuffer();
 
-	qualified_name = pg_strdup(fmtQualifiedDumpable(rsinfo));
-
 	/* restore relation stats */
 	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
 	appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
 					  fout->remoteVersion);
-	appendPQExpBufferStr(out, "\t'relation', ");
-	appendStringLiteralAH(out, qualified_name, fout);
-	appendPQExpBufferStr(out, "::regclass,\n");
+	appendPQExpBufferStr(out, "\t'schemaname', ");
+	appendStringLiteralAH(out, rsinfo->dobj.namespace->dobj.name, fout);
+	appendPQExpBufferStr(out, ",\n");
+	appendPQExpBufferStr(out, "\t'relname', ");
+	appendStringLiteralAH(out, rsinfo->dobj.name, fout);
+	appendPQExpBufferStr(out, ",\n");
 	appendPQExpBuffer(out, "\t'relpages', '%d'::integer,\n", rsinfo->relpages);
 	float_to_shortest_decimal_buf(rsinfo->reltuples, reltuples_str);
 	appendPQExpBuffer(out, "\t'reltuples', '%s'::real,\n", reltuples_str);
@@ -10606,9 +10606,10 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
 		appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
 						  fout->remoteVersion);
-		appendPQExpBufferStr(out, "\t'relation', ");
-		appendStringLiteralAH(out, qualified_name, fout);
-		appendPQExpBufferStr(out, "::regclass");
+		appendPQExpBufferStr(out, "\t'schemaname', ");
+		appendStringLiteralAH(out, rsinfo->dobj.namespace->dobj.name, fout);
+		appendPQExpBufferStr(out, ",\n\t'relname', ");
+		appendStringLiteralAH(out, rsinfo->dobj.name, fout);
 
 		if (PQgetisnull(res, rownum, i_attname))
 			pg_fatal("attname cannot be NULL");
@@ -10620,7 +10621,10 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 		 * their attnames are not necessarily stable across dump/reload.
 		 */
 		if (rsinfo->nindAttNames == 0)
-			appendNamedArgument(out, fout, "attname", "name", attname);
+		{
+			appendPQExpBuffer(out, ",\n\t'attname', ");
+			appendStringLiteralAH(out, attname, fout);
+		}
 		else
 		{
 			bool		found = false;
@@ -10700,7 +10704,6 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 							  .deps = deps,
 							  .nDeps = ndeps));
 
-	free(qualified_name);
 	destroyPQExpBuffer(out);
 	destroyPQExpBuffer(query);
 }
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index c7bffc1b045..b037f239136 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -4725,14 +4725,16 @@ my %tests = (
 		regexp => qr/^
 			\QSELECT * FROM pg_catalog.pg_restore_relation_stats(\E\s+
 			'version',\s'\d+'::integer,\s+
-			'relation',\s'dump_test.dup_test_post_data_ix'::regclass,\s+
+			'schemaname',\s'dump_test',\s+
+			'relname',\s'dup_test_post_data_ix',\s+
 			'relpages',\s'\d+'::integer,\s+
 			'reltuples',\s'\d+'::real,\s+
 			'relallvisible',\s'\d+'::integer\s+
 			\);\s+
 			\QSELECT * FROM pg_catalog.pg_restore_attribute_stats(\E\s+
 			'version',\s'\d+'::integer,\s+
-			'relation',\s'dump_test.dup_test_post_data_ix'::regclass,\s+
+			'schemaname',\s'dump_test',\s+
+			'relname',\s'dup_test_post_data_ix',\s+
 			'attnum',\s'2'::smallint,\s+
 			'inherited',\s'f'::boolean,\s+
 			'null_frac',\s'0'::real,\s+
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index 1f46d5e7854..2f1295f2149 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -14,7 +14,8 @@ CREATE TABLE stats_import.test(
 ) WITH (autovacuum_enabled = false);
 SELECT
     pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', 18::integer,
         'reltuples', 21::real,
         'relallvisible', 24::integer,
@@ -36,7 +37,7 @@ ORDER BY relname;
  test    |       18 |        21 |            24 |           27
 (1 row)
 
-SELECT pg_clear_relation_stats('stats_import.test'::regclass);
+SELECT pg_clear_relation_stats('stats_import', 'test');
  pg_clear_relation_stats 
 -------------------------
  
@@ -45,33 +46,54 @@ SELECT pg_clear_relation_stats('stats_import.test'::regclass);
 --
 -- relstats tests
 --
---- error: relation is wrong type
+-- error: schemaname missing
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::oid,
+        'relname', 'test',
         'relpages', 17::integer);
-WARNING:  argument "relation" has type "oid", expected type "regclass"
-ERROR:  "relation" cannot be NULL
+ERROR:  "schemaname" cannot be NULL
+-- error: relname missing
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relpages', 17::integer);
+ERROR:  "relname" cannot be NULL
+--- error: schemaname is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 3.6::float,
+        'relname', 'test',
+        'relpages', 17::integer);
+WARNING:  argument "schemaname" has type "double precision", expected type "text"
+ERROR:  "schemaname" cannot be NULL
+--- error: relname is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relname', 0::oid,
+        'relpages', 17::integer);
+WARNING:  argument "relname" has type "oid", expected type "text"
+ERROR:  "relname" cannot be NULL
 -- error: relation not found
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::oid::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'nope',
         'relpages', 17::integer);
-ERROR:  could not open relation with OID 0
+WARNING:  Relation "stats_import"."nope" not found.
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
 -- error: odd number of variadic arguments cannot be pairs
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relallvisible');
 ERROR:  variadic arguments must be name/value pairs
 HINT:  Provide an even number of variadic arguments that can be divided into pairs.
 -- error: argument name is NULL
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         NULL, '17'::integer);
-ERROR:  name at variadic position 3 is NULL
--- error: argument name is not a text type
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        17, '17'::integer);
-ERROR:  name at variadic position 3 has type "integer", expected type "text"
+ERROR:  name at variadic position 5 is NULL
 -- starting stats
 SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
@@ -84,7 +106,8 @@ WHERE oid = 'stats_import.test_i'::regclass;
 -- regular indexes have special case locking rules
 BEGIN;
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.test_i'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test_i',
         'relpages', 18::integer);
  pg_restore_relation_stats 
 ---------------------------
@@ -132,7 +155,8 @@ WHERE oid = 'stats_import.part_parent'::regclass;
 --
 BEGIN;
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent_i'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'part_parent_i',
         'relpages', 2::integer);
  pg_restore_relation_stats 
 ---------------------------
@@ -166,7 +190,8 @@ WHERE oid = 'stats_import.part_parent_i'::regclass;
 
 -- ok: set all relstats, with version, no bounds checking
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'version', 150000::integer,
         'relpages', '-17'::integer,
         'reltuples', 400::real,
@@ -187,7 +212,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just relpages, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', '16'::integer);
  pg_restore_relation_stats 
 ---------------------------
@@ -204,7 +230,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just reltuples, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'reltuples', '500'::real);
  pg_restore_relation_stats 
 ---------------------------
@@ -221,7 +248,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just relallvisible, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relallvisible', 5::integer);
  pg_restore_relation_stats 
 ---------------------------
@@ -238,7 +266,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: just relallfrozen
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'version', 150000::integer,
         'relallfrozen', 3::integer);
  pg_restore_relation_stats 
@@ -256,7 +285,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- warn: bad relpages type, rest updated
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', 'nope'::text,
         'reltuples', 400.0::real,
         'relallvisible', 4::integer,
@@ -277,7 +307,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- unrecognized argument name, rest ok
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', '171'::integer,
         'nope', 10::integer);
 WARNING:  unrecognized argument name: "nope"
@@ -295,8 +326,7 @@ WHERE oid = 'stats_import.test'::regclass;
 (1 row)
 
 -- ok: clear stats
-SELECT pg_catalog.pg_clear_relation_stats(
-    relation => 'stats_import.test'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'test');
  pg_clear_relation_stats 
 -------------------------
  
@@ -313,87 +343,123 @@ WHERE oid = 'stats_import.test'::regclass;
 -- invalid relkinds for statistics
 CREATE SEQUENCE stats_import.testseq;
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.testseq'::regclass);
+        'schemaname', 'stats_import',
+        'relname', 'testseq');
 ERROR:  cannot modify statistics for relation "testseq"
 DETAIL:  This operation is not supported for sequences.
-SELECT pg_catalog.pg_clear_relation_stats(
-        'stats_import.testseq'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testseq');
 ERROR:  cannot modify statistics for relation "testseq"
 DETAIL:  This operation is not supported for sequences.
 CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
-SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.testview'::regclass);
-ERROR:  cannot modify statistics for relation "testview"
-DETAIL:  This operation is not supported for views.
-SELECT pg_catalog.pg_clear_relation_stats(
-        'stats_import.testview'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testview');
 ERROR:  cannot modify statistics for relation "testview"
 DETAIL:  This operation is not supported for views.
 --
 -- attribute stats
 --
--- error: object does not exist
+-- error: schemaname missing
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', '0'::oid::regclass,
-    'attname', 'id'::name,
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  could not open relation with OID 0
--- error: relation null
+ERROR:  "schemaname" cannot be NULL
+-- error: schema does not exist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', NULL::oid::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'nope',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  "relation" cannot be NULL
+WARNING:  schema nope does not exist
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- error: relname missing
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+ERROR:  "relname" cannot be NULL
+-- error: relname does not exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'relname', 'nope',
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+WARNING:  Relation "stats_import"."nope" not found.
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- error: relname null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'relname', NULL,
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+ERROR:  "relname" cannot be NULL
 -- error: NULL attname
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', NULL::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', NULL,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 ERROR:  must specify either attname or attnum
 -- error: attname doesn't exist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'nope'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'nope',
     'inherited', false::boolean,
     'null_frac', 0.1::real,
     'avg_width', 2::integer,
     'n_distinct', 0.3::real);
-ERROR:  column "nope" of relation "test" does not exist
+ERROR:  column "nope" of relation "stats_import"."test" does not exist
 -- error: both attname and attnum
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'attnum', 1::smallint,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 ERROR:  cannot specify both attname and attnum
 -- error: neither attname nor attnum
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
+    'schemaname', 'stats_import',
+    'relname', 'test',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 ERROR:  must specify either attname or attnum
 -- error: attribute is system column
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'xmin'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'xmin',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 ERROR:  cannot modify statistics on system column "xmin"
 -- error: inherited null
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', NULL::boolean,
     'null_frac', 0.1::real);
 ERROR:  "inherited" cannot be NULL
 -- ok: just the fixed values, with version, no stakinds
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'version', 150000::integer,
     'null_frac', 0.2::real,
@@ -421,7 +487,8 @@ AND attname = 'id';
 -- for any stat-having relation.
 --
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
+    'schemaname', 'stats_import',
+    'relname', 'test',
     'attnum', 1::smallint,
     'inherited', false::boolean,
     'null_frac', 0.4::real);
@@ -443,8 +510,9 @@ AND attname = 'id';
 
 -- warn: unrecognized argument name, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.2::real,
     'nope', 0.5::real);
@@ -467,8 +535,9 @@ AND attname = 'id';
 
 -- warn: mcv / mcf null mismatch part 1, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.21::real,
     'most_common_freqs', '{0.1,0.2,0.3}'::real[]
@@ -492,8 +561,9 @@ AND attname = 'id';
 
 -- warn: mcv / mcf null mismatch part 2, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.21::real,
     'most_common_vals', '{1,2,3}'::text
@@ -517,8 +587,9 @@ AND attname = 'id';
 
 -- warn: mcf type mismatch, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.22::real,
     'most_common_vals', '{2,1,3}'::text,
@@ -544,8 +615,9 @@ AND attname = 'id';
 
 -- warn: mcv cast failure, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.23::real,
     'most_common_vals', '{2,four,3}'::text,
@@ -570,8 +642,9 @@ AND attname = 'id';
 
 -- ok: mcv+mcf
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'most_common_vals', '{2,1,3}'::text,
     'most_common_freqs', '{0.3,0.25,0.05}'::real[]
@@ -594,8 +667,9 @@ AND attname = 'id';
 
 -- warn: NULL in histogram array, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.24::real,
     'histogram_bounds', '{1,NULL,3,4}'::text
@@ -619,8 +693,9 @@ AND attname = 'id';
 
 -- ok: histogram_bounds
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'histogram_bounds', '{1,2,3,4}'::text
     );
@@ -642,8 +717,9 @@ AND attname = 'id';
 
 -- warn: elem_count_histogram null element, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.25::real,
     'elem_count_histogram', '{1,1,NULL,1,1,1,1,1}'::real[]
@@ -667,8 +743,9 @@ AND attname = 'tags';
 
 -- ok: elem_count_histogram
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.26::real,
     'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
@@ -691,8 +768,9 @@ AND attname = 'tags';
 
 -- warn: range stats on a scalar type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.27::real,
     'range_empty_frac', 0.5::real,
@@ -718,8 +796,9 @@ AND attname = 'id';
 
 -- warn: range_empty_frac range_length_hist null mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.28::real,
     'range_length_histogram', '{399,499,Infinity}'::text
@@ -743,8 +822,9 @@ AND attname = 'arange';
 
 -- warn: range_empty_frac range_length_hist null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.29::real,
     'range_empty_frac', 0.5::real
@@ -768,8 +848,9 @@ AND attname = 'arange';
 
 -- ok: range_empty_frac + range_length_hist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'range_empty_frac', 0.5::real,
     'range_length_histogram', '{399,499,Infinity}'::text
@@ -792,8 +873,9 @@ AND attname = 'arange';
 
 -- warn: range bounds histogram on scalar, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.31::real,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
@@ -818,8 +900,9 @@ AND attname = 'id';
 
 -- ok: range_bounds_histogram
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
     );
@@ -841,8 +924,9 @@ AND attname = 'arange';
 
 -- warn: cannot set most_common_elems for range type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.32::real,
     'most_common_elems', '{3,1}'::text,
@@ -868,8 +952,9 @@ AND attname = 'arange';
 
 -- warn: scalars can't have mcelem, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.33::real,
     'most_common_elems', '{1,3}'::text,
@@ -895,8 +980,9 @@ AND attname = 'id';
 
 -- warn: mcelem / mcelem mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.34::real,
     'most_common_elems', '{one,two}'::text
@@ -920,8 +1006,9 @@ AND attname = 'tags';
 
 -- warn: mcelem / mcelem null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.35::real,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
@@ -945,8 +1032,9 @@ AND attname = 'tags';
 
 -- ok: mcelem
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'most_common_elems', '{one,three}'::text,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
@@ -969,8 +1057,9 @@ AND attname = 'tags';
 
 -- warn: scalars can't have elem_count_histogram, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.36::real,
     'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1}'::real[]
@@ -1022,8 +1111,9 @@ SELECT s.schemaname, s.tablename, s.attname, s.inherited, r.*
 FROM pg_catalog.pg_stats AS s
 CROSS JOIN LATERAL
     pg_catalog.pg_restore_attribute_stats(
-        'relation', ('stats_import.' || s.tablename || '_clone')::regclass,
-        'attname', s.attname,
+        'schemaname', 'stats_import',
+        'relname', s.tablename::text || '_clone',
+        'attname', s.attname::text,
         'inherited', s.inherited,
         'version', 150000,
         'null_frac', s.null_frac,
@@ -1200,9 +1290,10 @@ AND attname = 'arange';
 (1 row)
 
 SELECT pg_catalog.pg_clear_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'arange'::name,
-    inherited => false::boolean);
+    schemaname => 'stats_import',
+    relname => 'test',
+    attname => 'arange',
+    inherited => false);
  pg_clear_attribute_stats 
 --------------------------
  
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index 0ec590688c2..ccdc44e9236 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -17,7 +17,8 @@ CREATE TABLE stats_import.test(
 
 SELECT
     pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', 18::integer,
         'reltuples', 21::real,
         'relallvisible', 24::integer,
@@ -32,37 +33,52 @@ FROM pg_class
 WHERE oid = 'stats_import.test'::regclass
 ORDER BY relname;
 
-SELECT pg_clear_relation_stats('stats_import.test'::regclass);
+SELECT pg_clear_relation_stats('stats_import', 'test');
 
 --
 -- relstats tests
 --
 
---- error: relation is wrong type
+-- error: schemaname missing
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::oid,
+        'relname', 'test',
+        'relpages', 17::integer);
+
+-- error: relname missing
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relpages', 17::integer);
+
+--- error: schemaname is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 3.6::float,
+        'relname', 'test',
+        'relpages', 17::integer);
+
+--- error: relname is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relname', 0::oid,
         'relpages', 17::integer);
 
 -- error: relation not found
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::oid::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'nope',
         'relpages', 17::integer);
 
 -- error: odd number of variadic arguments cannot be pairs
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relallvisible');
 
 -- error: argument name is NULL
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         NULL, '17'::integer);
 
--- error: argument name is not a text type
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        17, '17'::integer);
-
 -- starting stats
 SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
@@ -71,7 +87,8 @@ WHERE oid = 'stats_import.test_i'::regclass;
 -- regular indexes have special case locking rules
 BEGIN;
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.test_i'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test_i',
         'relpages', 18::integer);
 
 SELECT mode FROM pg_locks
@@ -108,7 +125,8 @@ WHERE oid = 'stats_import.part_parent'::regclass;
 BEGIN;
 
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent_i'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'part_parent_i',
         'relpages', 2::integer);
 
 SELECT mode FROM pg_locks
@@ -127,7 +145,8 @@ WHERE oid = 'stats_import.part_parent_i'::regclass;
 
 -- ok: set all relstats, with version, no bounds checking
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'version', 150000::integer,
         'relpages', '-17'::integer,
         'reltuples', 400::real,
@@ -140,7 +159,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just relpages, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', '16'::integer);
 
 SELECT relpages, reltuples, relallvisible, relallfrozen
@@ -149,7 +169,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just reltuples, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'reltuples', '500'::real);
 
 SELECT relpages, reltuples, relallvisible, relallfrozen
@@ -158,7 +179,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just relallvisible, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relallvisible', 5::integer);
 
 SELECT relpages, reltuples, relallvisible, relallfrozen
@@ -167,7 +189,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: just relallfrozen
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'version', 150000::integer,
         'relallfrozen', 3::integer);
 
@@ -177,7 +200,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- warn: bad relpages type, rest updated
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', 'nope'::text,
         'reltuples', 400.0::real,
         'relallvisible', 4::integer,
@@ -189,7 +213,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- unrecognized argument name, rest ok
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', '171'::integer,
         'nope', 10::integer);
 
@@ -198,8 +223,7 @@ FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: clear stats
-SELECT pg_catalog.pg_clear_relation_stats(
-    relation => 'stats_import.test'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'test');
 
 SELECT relpages, reltuples, relallvisible
 FROM pg_class
@@ -209,48 +233,70 @@ WHERE oid = 'stats_import.test'::regclass;
 CREATE SEQUENCE stats_import.testseq;
 
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.testseq'::regclass);
+        'schemaname', 'stats_import',
+        'relname', 'testseq');
 
-SELECT pg_catalog.pg_clear_relation_stats(
-        'stats_import.testseq'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testseq');
 
 CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
 
-SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.testview'::regclass);
-
-SELECT pg_catalog.pg_clear_relation_stats(
-        'stats_import.testview'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testview');
 
 --
 -- attribute stats
 --
 
--- error: object does not exist
+-- error: schemaname missing
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', '0'::oid::regclass,
-    'attname', 'id'::name,
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: relation null
+-- error: schema does not exist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', NULL::oid::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'nope',
+    'relname', 'test',
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: relname missing
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: relname does not exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'relname', 'nope',
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: relname null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'relname', NULL,
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
 -- error: NULL attname
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', NULL::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', NULL,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
 -- error: attname doesn't exist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'nope'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'nope',
     'inherited', false::boolean,
     'null_frac', 0.1::real,
     'avg_width', 2::integer,
@@ -258,36 +304,41 @@ SELECT pg_catalog.pg_restore_attribute_stats(
 
 -- error: both attname and attnum
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'attnum', 1::smallint,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
 -- error: neither attname nor attnum
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
+    'schemaname', 'stats_import',
+    'relname', 'test',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
 -- error: attribute is system column
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'xmin'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'xmin',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
 -- error: inherited null
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', NULL::boolean,
     'null_frac', 0.1::real);
 
 -- ok: just the fixed values, with version, no stakinds
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'version', 150000::integer,
     'null_frac', 0.2::real,
@@ -307,7 +358,8 @@ AND attname = 'id';
 -- for any stat-having relation.
 --
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
+    'schemaname', 'stats_import',
+    'relname', 'test',
     'attnum', 1::smallint,
     'inherited', false::boolean,
     'null_frac', 0.4::real);
@@ -321,8 +373,9 @@ AND attname = 'id';
 
 -- warn: unrecognized argument name, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.2::real,
     'nope', 0.5::real);
@@ -336,8 +389,9 @@ AND attname = 'id';
 
 -- warn: mcv / mcf null mismatch part 1, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.21::real,
     'most_common_freqs', '{0.1,0.2,0.3}'::real[]
@@ -352,8 +406,9 @@ AND attname = 'id';
 
 -- warn: mcv / mcf null mismatch part 2, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.21::real,
     'most_common_vals', '{1,2,3}'::text
@@ -368,8 +423,9 @@ AND attname = 'id';
 
 -- warn: mcf type mismatch, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.22::real,
     'most_common_vals', '{2,1,3}'::text,
@@ -385,8 +441,9 @@ AND attname = 'id';
 
 -- warn: mcv cast failure, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.23::real,
     'most_common_vals', '{2,four,3}'::text,
@@ -402,8 +459,9 @@ AND attname = 'id';
 
 -- ok: mcv+mcf
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'most_common_vals', '{2,1,3}'::text,
     'most_common_freqs', '{0.3,0.25,0.05}'::real[]
@@ -418,8 +476,9 @@ AND attname = 'id';
 
 -- warn: NULL in histogram array, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.24::real,
     'histogram_bounds', '{1,NULL,3,4}'::text
@@ -434,8 +493,9 @@ AND attname = 'id';
 
 -- ok: histogram_bounds
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'histogram_bounds', '{1,2,3,4}'::text
     );
@@ -449,8 +509,9 @@ AND attname = 'id';
 
 -- warn: elem_count_histogram null element, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.25::real,
     'elem_count_histogram', '{1,1,NULL,1,1,1,1,1}'::real[]
@@ -465,8 +526,9 @@ AND attname = 'tags';
 
 -- ok: elem_count_histogram
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.26::real,
     'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
@@ -481,8 +543,9 @@ AND attname = 'tags';
 
 -- warn: range stats on a scalar type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.27::real,
     'range_empty_frac', 0.5::real,
@@ -498,8 +561,9 @@ AND attname = 'id';
 
 -- warn: range_empty_frac range_length_hist null mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.28::real,
     'range_length_histogram', '{399,499,Infinity}'::text
@@ -514,8 +578,9 @@ AND attname = 'arange';
 
 -- warn: range_empty_frac range_length_hist null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.29::real,
     'range_empty_frac', 0.5::real
@@ -530,8 +595,9 @@ AND attname = 'arange';
 
 -- ok: range_empty_frac + range_length_hist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'range_empty_frac', 0.5::real,
     'range_length_histogram', '{399,499,Infinity}'::text
@@ -546,8 +612,9 @@ AND attname = 'arange';
 
 -- warn: range bounds histogram on scalar, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.31::real,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
@@ -562,8 +629,9 @@ AND attname = 'id';
 
 -- ok: range_bounds_histogram
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
     );
@@ -577,8 +645,9 @@ AND attname = 'arange';
 
 -- warn: cannot set most_common_elems for range type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.32::real,
     'most_common_elems', '{3,1}'::text,
@@ -594,8 +663,9 @@ AND attname = 'arange';
 
 -- warn: scalars can't have mcelem, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.33::real,
     'most_common_elems', '{1,3}'::text,
@@ -611,8 +681,9 @@ AND attname = 'id';
 
 -- warn: mcelem / mcelem mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.34::real,
     'most_common_elems', '{one,two}'::text
@@ -627,8 +698,9 @@ AND attname = 'tags';
 
 -- warn: mcelem / mcelem null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.35::real,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
@@ -643,8 +715,9 @@ AND attname = 'tags';
 
 -- ok: mcelem
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'most_common_elems', '{one,three}'::text,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
@@ -659,8 +732,9 @@ AND attname = 'tags';
 
 -- warn: scalars can't have elem_count_histogram, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.36::real,
     'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1}'::real[]
@@ -707,8 +781,9 @@ SELECT s.schemaname, s.tablename, s.attname, s.inherited, r.*
 FROM pg_catalog.pg_stats AS s
 CROSS JOIN LATERAL
     pg_catalog.pg_restore_attribute_stats(
-        'relation', ('stats_import.' || s.tablename || '_clone')::regclass,
-        'attname', s.attname,
+        'schemaname', 'stats_import',
+        'relname', s.tablename::text || '_clone',
+        'attname', s.attname::text,
         'inherited', s.inherited,
         'version', 150000,
         'null_frac', s.null_frac,
@@ -853,9 +928,10 @@ AND inherited = false
 AND attname = 'arange';
 
 SELECT pg_catalog.pg_clear_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'arange'::name,
-    inherited => false::boolean);
+    schemaname => 'stats_import',
+    relname => 'test',
+    attname => 'arange',
+    inherited => false);
 
 SELECT COUNT(*)
 FROM pg_stats
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 51dd8ad6571..63a260a8ff8 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30348,22 +30348,24 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          <structname>mytable</structname>:
 <programlisting>
  SELECT pg_restore_relation_stats(
-    'relation',  'mytable'::regclass,
-    'relpages',  173::integer,
-    'reltuples', 10000::real);
+    'schemaname', 'myschema',
+    'relname',    'mytable',
+    'relpages',   173::integer,
+    'reltuples',  10000::real);
 </programlisting>
         </para>
         <para>
-         The argument <literal>relation</literal> with a value of type
-         <type>regclass</type> is required, and specifies the table. Other
-         arguments are the names and values of statistics corresponding to
-         certain columns in <link
+         The arguments <literal>schemaname</literal> with a value of type
+         <type>regclass</type> and <literal>relname</literal> are required,
+         and specifies the table. Other arguments are the names and values
+         of statistics corresponding to certain columns in <link
          linkend="catalog-pg-class"><structname>pg_class</structname></link>.
          The currently-supported relation statistics are
          <literal>relpages</literal> with a value of type
          <type>integer</type>, <literal>reltuples</literal> with a value of
-         type <type>real</type>, and <literal>relallvisible</literal> with a
-         value of type <type>integer</type>.
+         type <type>real</type>, <literal>relallvisible</literal> with a
+         value of type <type>integer</type>, and <literal>relallfrozen</literal>
+         with a value of type <type>integer</type>.
         </para>
         <para>
          Additionally, this function accepts argument name
@@ -30391,7 +30393,7 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          <indexterm>
           <primary>pg_clear_relation_stats</primary>
          </indexterm>
-         <function>pg_clear_relation_stats</function> ( <parameter>relation</parameter> <type>regclass</type> )
+         <function>pg_clear_relation_stats</function> ( <parameter>schemaname</parameter> <type>text</type>, <parameter>relname</parameter> <type>text</type> )
          <returnvalue>void</returnvalue>
         </para>
         <para>
@@ -30440,16 +30442,18 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          <structname>mytable</structname>:
 <programlisting>
  SELECT pg_restore_attribute_stats(
-    'relation',    'mytable'::regclass,
-    'attname',     'col1'::name,
-    'inherited',   false,
-    'avg_width',   125::integer,
-    'null_frac',   0.5::real);
+    'schemaname', 'myschema',
+    'relname',    'mytable',
+    'attname',    'col1',
+    'inherited',  false,
+    'avg_width',  125::integer,
+    'null_frac',  0.5::real);
 </programlisting>
         </para>
         <para>
-         The required arguments are <literal>relation</literal> with a value
-         of type <type>regclass</type>, which specifies the table; either
+         The required arguments are <literal>schemaname</literal> with a value
+         of type <type>regclass</type> and <literal>relname</literal> with a value
+         of type <type>text</type> which specify the table; either
          <literal>attname</literal> with a value of type <type>name</type> or
          <literal>attnum</literal> with a value of type <type>smallint</type>,
          which specifies the column; and <literal>inherited</literal>, which
@@ -30485,7 +30489,8 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
           <primary>pg_clear_attribute_stats</primary>
          </indexterm>
          <function>pg_clear_attribute_stats</function> (
-         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>schemaname</parameter> <type>text</type>,
+         <parameter>relname</parameter> <type>text</type>,
          <parameter>attname</parameter> <type>name</type>,
          <parameter>inherited</parameter> <type>boolean</type> )
          <returnvalue>void</returnvalue>

base-commit: 21f653cc0024100f8ecc279162631f2b1ba8c46c
-- 
2.48.1

v7-0002-Downgrade-as-man-pg_restore_-_stats-errors-to-war.patchtext/x-patch; charset=US-ASCII; name=v7-0002-Downgrade-as-man-pg_restore_-_stats-errors-to-war.patchDownload
From 4d8d76b78b87f53d0adbd6781a2a66beac5bc264 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 8 Mar 2025 00:52:41 -0500
Subject: [PATCH v7 2/2] Downgrade as man pg_restore_*_stats errors to
 warnings.

We want to avoid errors that can potentially stop an otherwise
successful pg_upgrade or pg_restore operation. With that in mind, change
as many ERROR reports to WARNING + early termination with no data
updated.
---
 src/include/statistics/stat_utils.h        |   4 +-
 src/backend/statistics/attribute_stats.c   | 124 +++++++++++-----
 src/backend/statistics/relation_stats.c    |  10 +-
 src/backend/statistics/stat_utils.c        |  51 +++++--
 src/test/regress/expected/stats_import.out | 163 ++++++++++++++++-----
 src/test/regress/sql/stats_import.sql      |  36 ++---
 6 files changed, 277 insertions(+), 111 deletions(-)

diff --git a/src/include/statistics/stat_utils.h b/src/include/statistics/stat_utils.h
index cad042c8e4a..298cbae3436 100644
--- a/src/include/statistics/stat_utils.h
+++ b/src/include/statistics/stat_utils.h
@@ -21,7 +21,7 @@ struct StatsArgInfo
 	Oid			argtype;
 };
 
-extern void stats_check_required_arg(FunctionCallInfo fcinfo,
+extern bool stats_check_required_arg(FunctionCallInfo fcinfo,
 									 struct StatsArgInfo *arginfo,
 									 int argnum);
 extern bool stats_check_arg_array(FunctionCallInfo fcinfo,
@@ -30,7 +30,7 @@ extern bool stats_check_arg_pair(FunctionCallInfo fcinfo,
 								 struct StatsArgInfo *arginfo,
 								 int argnum1, int argnum2);
 
-extern void stats_lock_check_privileges(Oid reloid);
+extern bool stats_lock_check_privileges(Oid reloid);
 
 extern Oid stats_schema_check_privileges(const char *nspname);
 
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index f87db2d6102..4f9bc18f8c6 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -100,7 +100,7 @@ static struct StatsArgInfo cleararginfo[] =
 
 static bool attribute_statistics_update(FunctionCallInfo fcinfo);
 static Node *get_attr_expr(Relation rel, int attnum);
-static void get_attr_stat_type(Oid reloid, AttrNumber attnum,
+static bool get_attr_stat_type(Oid reloid, AttrNumber attnum,
 							   Oid *atttypid, int32 *atttypmod,
 							   char *atttyptype, Oid *atttypcoll,
 							   Oid *eq_opr, Oid *lt_opr);
@@ -129,10 +129,12 @@ static void init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
  * stored as an anyarray, and the representation of the array needs to store
  * the correct element type, which must be derived from the attribute.
  *
- * Major errors, such as the table not existing, the attribute not existing,
- * or a permissions failure are always reported at ERROR. Other errors, such
- * as a conversion failure on one statistic kind, are reported as a WARNING
- * and other statistic kinds may still be updated.
+ * This function is called during database upgrades and restorations, therefore
+ * it is imperative to avoid ERRORs that could potentially end the upgrade or
+ * restore unless. Major errors, such as the table not existing, the attribute
+ * not existing, or permissions failure are reported as WARNINGs with an end to
+ * the function, thus allowing the upgrade/restore to continue, but without the
+ * stats that can be regenereated once the database is online again.
  */
 static bool
 attribute_statistics_update(FunctionCallInfo fcinfo)
@@ -149,8 +151,8 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	HeapTuple	statup;
 
 	Oid			atttypid = InvalidOid;
-	int32		atttypmod;
-	char		atttyptype;
+	int32		atttypmod = -1;
+	char		atttyptype = TYPTYPE_PSEUDO; /* Not a great default, but there is no TYPTYPE_INVALID */
 	Oid			atttypcoll = InvalidOid;
 	Oid			eq_opr = InvalidOid;
 	Oid			lt_opr = InvalidOid;
@@ -177,17 +179,19 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 	bool		result = true;
 
-	stats_check_required_arg(fcinfo, attarginfo, ATTRELSCHEMA_ARG);
-	stats_check_required_arg(fcinfo, attarginfo, ATTRELNAME_ARG);
+	if (!stats_check_required_arg(fcinfo, attarginfo, ATTRELSCHEMA_ARG))
+		return false;
+	if (!stats_check_required_arg(fcinfo, attarginfo, ATTRELNAME_ARG))
+		return false;
 
 	nspname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELSCHEMA_ARG));
 	nspoid = stats_schema_check_privileges(nspname);
-	if (nspoid == InvalidOid)
+	if (!OidIsValid(nspoid))
 		return false;
 
 	relname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELNAME_ARG));
 	reloid = get_relname_relid(relname, nspoid);
-	if (reloid == InvalidOid)
+	if (!OidIsValid(reloid))
 	{
 		ereport(WARNING,
 				(errcode(ERRCODE_UNDEFINED_OBJECT),
@@ -196,29 +200,39 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	}
 
 	if (RecoveryInProgress())
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("recovery is in progress"),
 				 errhint("Statistics cannot be modified during recovery.")));
+		return false;
+	}
 
 	/* lock before looking up attribute */
-	stats_lock_check_privileges(reloid);
+	if (!stats_lock_check_privileges(reloid))
+		return false;
 
 	/* user can specify either attname or attnum, but not both */
 	if (!PG_ARGISNULL(ATTNAME_ARG))
 	{
 		if (!PG_ARGISNULL(ATTNUM_ARG))
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("cannot specify both attname and attnum")));
+			return false;
+		}
 		attname = TextDatumGetCString(PG_GETARG_DATUM(ATTNAME_ARG));
 		attnum = get_attnum(reloid, attname);
 		/* note that this test covers attisdropped cases too: */
 		if (attnum == InvalidAttrNumber)
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 					(errcode(ERRCODE_UNDEFINED_COLUMN),
 					 errmsg("column \"%s\" of relation \"%s\".\"%s\" does not exist",
 							attname, nspname, relname)));
+			return false;
+		}
 	}
 	else if (!PG_ARGISNULL(ATTNUM_ARG))
 	{
@@ -227,27 +241,33 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 		/* annoyingly, get_attname doesn't check attisdropped */
 		if (attname == NULL ||
 			!SearchSysCacheExistsAttName(reloid, attname))
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 					(errcode(ERRCODE_UNDEFINED_COLUMN),
 					 errmsg("column %d of relation \"%s\".\"%s\" does not exist",
 							attnum, nspname, relname)));
+			return false;
+		}
 	}
 	else
 	{
-		ereport(ERROR,
+		ereport(WARNING,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("must specify either attname or attnum")));
-		attname = NULL;			/* keep compiler quiet */
-		attnum = 0;
+		return false;
 	}
 
 	if (attnum < 0)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot modify statistics on system column \"%s\"",
 						attname)));
+		return false;
+	}
 
-	stats_check_required_arg(fcinfo, attarginfo, INHERITED_ARG);
+	if (!stats_check_required_arg(fcinfo, attarginfo, INHERITED_ARG))
+		return false;
 	inherited = PG_GETARG_BOOL(INHERITED_ARG);
 
 	/*
@@ -296,10 +316,11 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	}
 
 	/* derive information from attribute */
-	get_attr_stat_type(reloid, attnum,
-					   &atttypid, &atttypmod,
-					   &atttyptype, &atttypcoll,
-					   &eq_opr, &lt_opr);
+	if (!get_attr_stat_type(reloid, attnum,
+							&atttypid, &atttypmod,
+							&atttyptype, &atttypcoll,
+							&eq_opr, &lt_opr))
+		result = false;
 
 	/* if needed, derive element type */
 	if (do_mcelem || do_dechist)
@@ -579,7 +600,7 @@ get_attr_expr(Relation rel, int attnum)
 /*
  * Derive type information from the attribute.
  */
-static void
+static bool
 get_attr_stat_type(Oid reloid, AttrNumber attnum,
 				   Oid *atttypid, int32 *atttypmod,
 				   char *atttyptype, Oid *atttypcoll,
@@ -596,18 +617,26 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum,
 
 	/* Attribute not found */
 	if (!HeapTupleIsValid(atup))
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 errmsg("attribute %d of relation \"%s\" does not exist",
 						attnum, RelationGetRelationName(rel))));
+		relation_close(rel, NoLock);
+		return false;
+	}
 
 	attr = (Form_pg_attribute) GETSTRUCT(atup);
 
 	if (attr->attisdropped)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 errmsg("attribute %d of relation \"%s\" does not exist",
 						attnum, RelationGetRelationName(rel))));
+		relation_close(rel, NoLock);
+		return false;
+	}
 
 	expr = get_attr_expr(rel, attr->attnum);
 
@@ -656,6 +685,7 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum,
 		*atttypcoll = DEFAULT_COLLATION_OID;
 
 	relation_close(rel, NoLock);
+	return true;
 }
 
 /*
@@ -781,6 +811,10 @@ set_stats_slot(Datum *values, bool *nulls, bool *replaces,
 	if (slotidx >= STATISTIC_NUM_SLOTS && first_empty >= 0)
 		slotidx = first_empty;
 
+	/*
+	 * Currently there is no datatype that can have more than STATISTIC_NUM_SLOTS
+	 * statistic kinds, so this can safely remain an ERROR for now.
+	 */
 	if (slotidx >= STATISTIC_NUM_SLOTS)
 		ereport(ERROR,
 				(errmsg("maximum number of statistics slots exceeded: %d",
@@ -927,15 +961,19 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 	AttrNumber	attnum;
 	bool		inherited;
 
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELSCHEMA_ARG);
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELNAME_ARG);
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTNAME_ARG);
-	stats_check_required_arg(fcinfo, cleararginfo, C_INHERITED_ARG);
+	if (!stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELSCHEMA_ARG))
+		PG_RETURN_VOID();
+	if (!stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELNAME_ARG))
+		PG_RETURN_VOID();
+	if (!stats_check_required_arg(fcinfo, cleararginfo, C_ATTNAME_ARG))
+		PG_RETURN_VOID();
+	if (!stats_check_required_arg(fcinfo, cleararginfo, C_INHERITED_ARG))
+		PG_RETURN_VOID();
 
 	nspname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELSCHEMA_ARG));
 	nspoid = stats_schema_check_privileges(nspname);
 	if (!OidIsValid(nspoid))
-		return false;
+		PG_RETURN_VOID();
 
 	relname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELNAME_ARG));
 	reloid = get_relname_relid(relname, nspoid);
@@ -944,31 +982,41 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 		ereport(WARNING,
 				(errcode(ERRCODE_UNDEFINED_OBJECT),
 				 errmsg("Relation \"%s\".\"%s\" not found.", nspname, relname)));
-		return false;
+		PG_RETURN_VOID();
 	}
 
 	if (RecoveryInProgress())
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("recovery is in progress"),
 				 errhint("Statistics cannot be modified during recovery.")));
+		PG_RETURN_VOID();
+	}
 
-	stats_lock_check_privileges(reloid);
+	if (!stats_lock_check_privileges(reloid))
+		PG_RETURN_VOID();
 
 	attname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTNAME_ARG));
 	attnum = get_attnum(reloid, attname);
 
 	if (attnum < 0)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot clear statistics on system column \"%s\"",
 						attname)));
+		PG_RETURN_VOID();
+	}
 
 	if (attnum == InvalidAttrNumber)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 errmsg("column \"%s\" of relation \"%s\" does not exist",
 						attname, get_rel_name(reloid))));
+		PG_RETURN_VOID();
+	}
 
 	inherited = PG_GETARG_BOOL(C_INHERITED_ARG);
 
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index fdc69bc93e2..49109cf721d 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -84,8 +84,11 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 	bool		nulls[4] = {0};
 	int			nreplaces = 0;
 
-	stats_check_required_arg(fcinfo, relarginfo, RELSCHEMA_ARG);
-	stats_check_required_arg(fcinfo, relarginfo, RELNAME_ARG);
+	if (!stats_check_required_arg(fcinfo, relarginfo, RELSCHEMA_ARG))
+		return false;
+
+	if (!stats_check_required_arg(fcinfo, relarginfo, RELNAME_ARG))
+		return false;
 
 	nspname = TextDatumGetCString(PG_GETARG_DATUM(RELSCHEMA_ARG));
 	nspoid = stats_schema_check_privileges(nspname);
@@ -108,7 +111,8 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 				 errmsg("recovery is in progress"),
 				 errhint("Statistics cannot be modified during recovery.")));
 
-	stats_lock_check_privileges(reloid);
+	if (!stats_lock_check_privileges(reloid))
+		return false;
 
 	if (!PG_ARGISNULL(RELPAGES_ARG))
 	{
diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c
index e037d4994e8..dd9d88ac1c5 100644
--- a/src/backend/statistics/stat_utils.c
+++ b/src/backend/statistics/stat_utils.c
@@ -34,16 +34,20 @@
 /*
  * Ensure that a given argument is not null.
  */
-void
+bool
 stats_check_required_arg(FunctionCallInfo fcinfo,
 						 struct StatsArgInfo *arginfo,
 						 int argnum)
 {
 	if (PG_ARGISNULL(argnum))
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("\"%s\" cannot be NULL",
 						arginfo[argnum].argname)));
+		return false;
+	}
+	return true;
 }
 
 /*
@@ -128,13 +132,14 @@ stats_check_arg_pair(FunctionCallInfo fcinfo,
  *   - the role owns the current database and the relation is not shared
  *   - the role has the MAINTAIN privilege on the relation
  */
-void
+bool
 stats_lock_check_privileges(Oid reloid)
 {
 	Relation	table;
 	Oid			table_oid = reloid;
 	Oid			index_oid = InvalidOid;
 	LOCKMODE	index_lockmode = NoLock;
+	bool		ok = true;
 
 	/*
 	 * For indexes, we follow the locking behavior in do_analyze_rel() and
@@ -174,14 +179,15 @@ stats_lock_check_privileges(Oid reloid)
 		case RELKIND_PARTITIONED_TABLE:
 			break;
 		default:
-			ereport(ERROR,
+			ereport(WARNING,
 					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 					 errmsg("cannot modify statistics for relation \"%s\"",
 							RelationGetRelationName(table)),
 					 errdetail_relkind_not_supported(table->rd_rel->relkind)));
+		ok = false;
 	}
 
-	if (OidIsValid(index_oid))
+	if (ok && (OidIsValid(index_oid)))
 	{
 		Relation	index;
 
@@ -194,25 +200,33 @@ stats_lock_check_privileges(Oid reloid)
 		relation_close(index, NoLock);
 	}
 
-	if (table->rd_rel->relisshared)
-		ereport(ERROR,
+	if (ok && (table->rd_rel->relisshared))
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot modify statistics for shared relation")));
+		ok = false;
+	}
 
-	if (!object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId()))
+	if (ok && (!object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId())))
 	{
 		AclResult	aclresult = pg_class_aclcheck(RelationGetRelid(table),
 												  GetUserId(),
 												  ACL_MAINTAIN);
 
 		if (aclresult != ACLCHECK_OK)
-			aclcheck_error(aclresult,
-						   get_relkind_objtype(table->rd_rel->relkind),
-						   NameStr(table->rd_rel->relname));
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+						errmsg("permission denied for relation %s",
+							   NameStr(table->rd_rel->relname))));
+			ok = false;
+		}
 	}
 
 	/* retain lock on table */
 	relation_close(table, NoLock);
+	return ok;
 }
 
 
@@ -318,9 +332,12 @@ stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
 								  &args, &types, &argnulls);
 
 	if (nargs % 2 != 0)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				errmsg("variadic arguments must be name/value pairs"),
 				errhint("Provide an even number of variadic arguments that can be divided into pairs."));
+		return false;
+	}
 
 	/*
 	 * For each argument name/value pair, find corresponding positional
@@ -333,14 +350,20 @@ stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
 		char	   *argname;
 
 		if (argnulls[i])
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 					(errmsg("name at variadic position %d is NULL", i + 1)));
+			return false;
+		}
 
 		if (types[i] != TEXTOID)
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 					(errmsg("name at variadic position %d has type \"%s\", expected type \"%s\"",
 							i + 1, format_type_be(types[i]),
 							format_type_be(TEXTOID))));
+			return false;
+		}
 
 		if (argnulls[i + 1])
 			continue;
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index 2f1295f2149..6551d6bf099 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -46,31 +46,51 @@ SELECT pg_clear_relation_stats('stats_import', 'test');
 --
 -- relstats tests
 --
--- error: schemaname missing
+-- warning: schemaname missing, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'relname', 'test',
         'relpages', 17::integer);
-ERROR:  "schemaname" cannot be NULL
--- error: relname missing
+WARNING:  "schemaname" cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+-- warning: relname missing, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relpages', 17::integer);
-ERROR:  "relname" cannot be NULL
---- error: schemaname is wrong type
+WARNING:  "relname" cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+--- warning: schemaname is wrong type, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 3.6::float,
         'relname', 'test',
         'relpages', 17::integer);
 WARNING:  argument "schemaname" has type "double precision", expected type "text"
-ERROR:  "schemaname" cannot be NULL
---- error: relname is wrong type
+WARNING:  "schemaname" cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+--- warning: relname is wrong type, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 0::oid,
         'relpages', 17::integer);
 WARNING:  argument "relname" has type "oid", expected type "text"
-ERROR:  "relname" cannot be NULL
--- error: relation not found
+WARNING:  "relname" cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+-- warning: relation not found, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'nope',
@@ -81,19 +101,30 @@ WARNING:  Relation "stats_import"."nope" not found.
  f
 (1 row)
 
--- error: odd number of variadic arguments cannot be pairs
+-- warning: odd number of variadic arguments cannot be pairs, nothing updated
 SELECT pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'test',
         'relallvisible');
-ERROR:  variadic arguments must be name/value pairs
+WARNING:  variadic arguments must be name/value pairs
 HINT:  Provide an even number of variadic arguments that can be divided into pairs.
--- error: argument name is NULL
+WARNING:  "schemaname" cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+-- warning: argument name is NULL, nothing updated
 SELECT pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'test',
         NULL, '17'::integer);
-ERROR:  name at variadic position 5 is NULL
+WARNING:  name at variadic position 5 is NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
 -- starting stats
 SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
@@ -345,26 +376,46 @@ CREATE SEQUENCE stats_import.testseq;
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'testseq');
-ERROR:  cannot modify statistics for relation "testseq"
+WARNING:  cannot modify statistics for relation "testseq"
 DETAIL:  This operation is not supported for sequences.
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
 SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testseq');
-ERROR:  cannot modify statistics for relation "testseq"
+WARNING:  cannot modify statistics for relation "testseq"
 DETAIL:  This operation is not supported for sequences.
+ pg_clear_relation_stats 
+-------------------------
+ 
+(1 row)
+
 CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
 SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testview');
-ERROR:  cannot modify statistics for relation "testview"
+WARNING:  cannot modify statistics for relation "testview"
 DETAIL:  This operation is not supported for views.
+ pg_clear_relation_stats 
+-------------------------
+ 
+(1 row)
+
 --
 -- attribute stats
 --
--- error: schemaname missing
+-- warning: schemaname missing, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relname', 'test',
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  "schemaname" cannot be NULL
--- error: schema does not exist
+WARNING:  "schemaname" cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: schema does not exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'nope',
     'relname', 'test',
@@ -377,14 +428,19 @@ WARNING:  schema nope does not exist
  f
 (1 row)
 
--- error: relname missing
+-- warning: relname missing, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  "relname" cannot be NULL
--- error: relname does not exist
+WARNING:  "relname" cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: relname does not exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'nope',
@@ -397,23 +453,33 @@ WARNING:  Relation "stats_import"."nope" not found.
  f
 (1 row)
 
--- error: relname null
+-- warning: relname null, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', NULL,
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  "relname" cannot be NULL
--- error: NULL attname
+WARNING:  "relname" cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: NULL attname, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
     'attname', NULL,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  must specify either attname or attnum
--- error: attname doesn't exist
+WARNING:  must specify either attname or attnum
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: attname doesn't exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -422,8 +488,13 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'null_frac', 0.1::real,
     'avg_width', 2::integer,
     'n_distinct', 0.3::real);
-ERROR:  column "nope" of relation "stats_import"."test" does not exist
--- error: both attname and attnum
+WARNING:  column "nope" of relation "stats_import"."test" does not exist
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: both attname and attnum, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -431,30 +502,50 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'attnum', 1::smallint,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  cannot specify both attname and attnum
--- error: neither attname nor attnum
+WARNING:  cannot specify both attname and attnum
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: neither attname nor attnum, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  must specify either attname or attnum
--- error: attribute is system column
+WARNING:  must specify either attname or attnum
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: attribute is system column, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
     'attname', 'xmin',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  cannot modify statistics on system column "xmin"
--- error: inherited null
+WARNING:  cannot modify statistics on system column "xmin"
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: inherited null, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
     'attname', 'id',
     'inherited', NULL::boolean,
     'null_frac', 0.1::real);
-ERROR:  "inherited" cannot be NULL
+WARNING:  "inherited" cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
 -- ok: just the fixed values, with version, no stakinds
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index ccdc44e9236..dbbebce1673 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -39,41 +39,41 @@ SELECT pg_clear_relation_stats('stats_import', 'test');
 -- relstats tests
 --
 
--- error: schemaname missing
+-- warning: schemaname missing, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'relname', 'test',
         'relpages', 17::integer);
 
--- error: relname missing
+-- warning: relname missing, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relpages', 17::integer);
 
---- error: schemaname is wrong type
+--- warning: schemaname is wrong type, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 3.6::float,
         'relname', 'test',
         'relpages', 17::integer);
 
---- error: relname is wrong type
+--- warning: relname is wrong type, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 0::oid,
         'relpages', 17::integer);
 
--- error: relation not found
+-- warning: relation not found, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'nope',
         'relpages', 17::integer);
 
--- error: odd number of variadic arguments cannot be pairs
+-- warning: odd number of variadic arguments cannot be pairs, nothing updated
 SELECT pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'test',
         'relallvisible');
 
--- error: argument name is NULL
+-- warning: argument name is NULL, nothing updated
 SELECT pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'test',
@@ -246,14 +246,14 @@ SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname
 -- attribute stats
 --
 
--- error: schemaname missing
+-- warning: schemaname missing, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relname', 'test',
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: schema does not exist
+-- warning: schema does not exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'nope',
     'relname', 'test',
@@ -261,14 +261,14 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: relname missing
+-- warning: relname missing, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: relname does not exist
+-- warning: relname does not exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'nope',
@@ -276,7 +276,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: relname null
+-- warning: relname null, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', NULL,
@@ -284,7 +284,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: NULL attname
+-- warning: NULL attname, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -292,7 +292,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: attname doesn't exist
+-- warning: attname doesn't exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -302,7 +302,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'avg_width', 2::integer,
     'n_distinct', 0.3::real);
 
--- error: both attname and attnum
+-- warning: both attname and attnum, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -311,14 +311,14 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: neither attname nor attnum
+-- warning: neither attname nor attnum, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: attribute is system column
+-- warning: attribute is system column, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -326,7 +326,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: inherited null
+-- warning: inherited null, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
-- 
2.48.1

#478Robert Treat
rob@xzilla.net
In reply to: Corey Huinker (#473)
Re: Statistics Import and Export

On Fri, Mar 7, 2025 at 10:40 PM Corey Huinker <corey.huinker@gmail.com>
wrote:

if you want everything --include=schema,data,statistics (presumably
redundant with the default behavior)
if you want schema only --include=schema
if you want "everything except schema" --include=data,statistics

Until we add a fourth option, and then it becomes completely ambiguous as
to whether you wanted data+statstics, or you not-wanted schema.

except it is perfectly clear that you *asked for* data and statistics, so
you get what you asked for. however the user conjures in their heads what
they are looking for, the logic is simple, you get what you asked for.

And if someday, for example, there is ever agreement on including role

information with normal pg_dump, you add "roles" as an option to be
parsed via --include without having to create any new flags.

This is pushing a burden onto our customers for a parsing convenience.

In the UX world, the general pattern is people start to get overwhelmed
once you get over a 1/2 dozen options (I think that's based on Miller's
law, but might be mis-remembering); we are already at 9 for this use case.
So really it is quite the opposite, we'd be reducing the burden on
customers by simplifying the interface rather than just throwing out every
possible combination and saying "you figure it out".

Robert Treat
https://xzilla.net

#479Corey Huinker
corey.huinker@gmail.com
In reply to: Robert Treat (#478)
Re: Statistics Import and Export

Until we add a fourth option, and then it becomes completely ambiguous as

to whether you wanted data+statstics, or you not-wanted schema.

except it is perfectly clear that you *asked for* data and statistics, so
you get what you asked for. however the user conjures in their heads what
they are looking for, the logic is simple, you get what you asked for.

They *asked for* that because they didn't have the mechanism to say "hold
the mayo" or "everything except pickles". That's reducing their choice, and
then blaming them for their choice.

In the UX world, the general pattern is people start to get overwhelmed

once you get over a 1/2 dozen options (I think that's based on Miller's
law, but might be mis-remembering); we are already at 9 for this use case.
So really it is quite the opposite, we'd be reducing the burden on
customers by simplifying the interface rather than just throwing out every
possible combination and saying "you figure it out".

Except that those options are easily grouped into families. I see that
there's a --no-comments flag, so why wouldn't there be a --no-statistics
flag? Lots of $thing have a --no-$thing. That's the established UX pattern
_working_. The user learned that pattern and we shouldn't punish them by
changing it for our own parsing convenience.

#480Jeff Davis
pgsql@j-davis.com
In reply to: Robert Treat (#478)
Re: Statistics Import and Export

On Sat, 2025-03-08 at 10:56 -0500, Robert Treat wrote:

In the UX world, the general pattern is people start to get
overwhelmed once you get over a 1/2 dozen options (I think that's
based on Miller's law, but might be mis-remembering); we are already
at 9 for this use case. So really it is quite the opposite, we'd be
reducing the burden on customers by simplifying the interface rather
than just throwing out every possible combination and saying "you
figure it out". 

To be clear about your proposal:

* --include conflicts with --schema-only and --data-only
* --include overrides any default

is that right?

Thoughts on how we should document when/how to use --section vs --
include? Granted, that might be a point of confusion regardless of the
options we offer.

Regards,
Jeff Davis

#481Tom Lane
tgl@sss.pgh.pa.us
In reply to: Tom Lane (#470)
Re: Statistics Import and Export: difference in statistics dumped

I wrote:

I think what is happening is that the patch shut off CREATE
INDEX's update of not only the table's stats but also the
index's stats. This seems unhelpful: the index's empty
stats can never be what's wanted.

I looked at this more closely and realized that it's a simple matter
of having made the tests in the wrong order. The whole stanza
should only apply when dealing with the table, not the index.

I verified that this change fixes the cross-version-upgrade
failure in local testing, and pushed it.

regards, tom lane

#482Jeff Davis
pgsql@j-davis.com
In reply to: Tom Lane (#481)
Re: Statistics Import and Export: difference in statistics dumped

On Mon, 2025-03-10 at 17:53 -0400, Tom Lane wrote:

I wrote:

I think what is happening is that the patch shut off CREATE
INDEX's update of not only the table's stats but also the
index's stats.  This seems unhelpful: the index's empty
stats can never be what's wanted.

I looked at this more closely and realized that it's a simple matter
of having made the tests in the wrong order.  The whole stanza
should only apply when dealing with the table, not the index.

I verified that this change fixes the cross-version-upgrade
failure in local testing, and pushed it.

Ah, thank you.

Regards,
Jeff Davis

#483Ashutosh Bapat
ashutosh.bapat.oss@gmail.com
In reply to: Jeff Davis (#482)
Re: Statistics Import and Export: difference in statistics dumped

On Tue, Mar 11, 2025 at 5:23 AM Jeff Davis <pgsql@j-davis.com> wrote:

On Mon, 2025-03-10 at 17:53 -0400, Tom Lane wrote:

I wrote:

I think what is happening is that the patch shut off CREATE
INDEX's update of not only the table's stats but also the
index's stats. This seems unhelpful: the index's empty
stats can never be what's wanted.

I looked at this more closely and realized that it's a simple matter
of having made the tests in the wrong order. The whole stanza
should only apply when dealing with the table, not the index.

I verified that this change fixes the cross-version-upgrade
failure in local testing, and pushed it.

Ah, thank you.

Regards,
Jeff Davis

Thanks. I verified that it has been fixed now. But there's something
wrong with materialized view statistics. I am starting a new thread
for the same.

--
Best Wishes,
Ashutosh Bapat

#484Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#480)
4 attachment(s)
Re: Statistics Import and Export

New patches and a rebase.

0001 - no changes, but the longer I go the more I'm certain this is
something we want to do.
0002- same as 0001

0003 -

Storing the restore function calls in the archive entry hogged a lot of
memory and made people nervous. This introduces a new function pointer that
generates those restore SQL calls right before they're written to disk,
thus reducing the memory load from "stats for every object to be dumped" to
just one object. Thanks to Nathan for diagnosing some weird quirks with
various formats.

0004 -

This replaces the query in the prepared statement with one that batches
them 100 relations at a time, and then maintains that result set until it
is consumed. It seems to have obvious speedups.

database pg14, 100k tables x 2 columns each:

0004: 34.5s with statistics, 25.04s without
0003: 42.23s with statistics, 24.29s without
0002: 42.25s with statistics, 23.17s without

Gory details:

PGSERVICE=benchmark14 time /usr/local/pgsql/bin/pg_dump --file=tip.run1.dump
5.45user 2.38system 0:34.50elapsed 22%CPU (0avgtext+0avgdata
912680maxresident)k
0inputs+2105736outputs (0major+245090minor)pagefaults 0swaps

PGSERVICE=benchmark14 time /usr/local/pgsql/bin/pg_dump --no-statistics
--file=tip.nostats.run1.dump
4.36user 2.05system 0:25.04elapsed 25%CPU (0avgtext+0avgdata
702488maxresident)k
0inputs+1643048outputs (0major+192512minor)pagefaults 0swaps

PGSERVICE=benchmark14 time /usr/local/pgsql/bin/pg_dump
--file=nobatch.run1.dump
5.60user 3.95system 0:42.23elapsed 22%CPU (0avgtext+0avgdata
902424maxresident)k
0inputs+2105672outputs (0major+242536minor)pagefaults 0swaps

PGSERVICE=benchmark14 time /usr/local/pgsql/bin/pg_dump --no-statistics
--file=nobatch-nostats.run1.dump
4.38user 2.13system 0:24.29elapsed 26%CPU (0avgtext+0avgdata
702292maxresident)k
48inputs+1642952outputs (0major+192515minor)pagefaults 0swaps

PGSERVICE=benchmark14 time /usr/local/pgsql/bin/pg_dump
--file=nostmtfn.run1.dump
6.01user 4.47system 0:42.25elapsed 24%CPU (0avgtext+0avgdata
1089784maxresident)k
0inputs+2106840outputs (0major+289407minor)pagefaults 0swaps

PGSERVICE=benchmark14 time /usr/local/pgsql/bin/pg_dump --no-statistics
--file=nostmtfn-nostats.run1.dump
4.35user 2.13system 0:23.17elapsed 27%CPU (0avgtext+0avgdata
690000maxresident)k
0inputs+1642952outputs (0major+189383minor)pagefaults 0swaps

Attachments:

v8-0001-Split-relation-into-schemaname-and-relname.patchtext/x-patch; charset=US-ASCII; name=v8-0001-Split-relation-into-schemaname-and-relname.patchDownload
From a2c68b8390cf137323f449a4bc826ad66bada0bb Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Tue, 4 Mar 2025 22:16:52 -0500
Subject: [PATCH v8 1/4] Split relation into schemaname and relname.

In order to further reduce potential error-failures in restores and
upgrades, replace the numerous casts of fully qualified relation names
into their schema+relname text components.

Further remove the ::name casts on attname and change the expected
datatype to text.

Add an ACL_USAGE check on the namespace oid after it is looked up.
---
 src/include/catalog/pg_proc.dat            |   8 +-
 src/include/statistics/stat_utils.h        |   2 +
 src/backend/statistics/attribute_stats.c   |  87 ++++--
 src/backend/statistics/relation_stats.c    |  65 +++--
 src/backend/statistics/stat_utils.c        |  37 +++
 src/bin/pg_dump/pg_dump.c                  |  25 +-
 src/bin/pg_dump/t/002_pg_dump.pl           |   6 +-
 src/test/regress/expected/stats_import.out | 307 +++++++++++++--------
 src/test/regress/sql/stats_import.sql      | 276 +++++++++++-------
 doc/src/sgml/func.sgml                     |  41 +--
 10 files changed, 566 insertions(+), 288 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 890822eaf79..8dee321d248 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12453,8 +12453,8 @@
   descr => 'clear statistics on relation',
   proname => 'pg_clear_relation_stats', provolatile => 'v', proisstrict => 'f',
   proparallel => 'u', prorettype => 'void',
-  proargtypes => 'regclass',
-  proargnames => '{relation}',
+  proargtypes => 'text text',
+  proargnames => '{schemaname,relname}',
   prosrc => 'pg_clear_relation_stats' },
 { oid => '8461',
   descr => 'restore statistics on attribute',
@@ -12469,8 +12469,8 @@
   descr => 'clear statistics on attribute',
   proname => 'pg_clear_attribute_stats', provolatile => 'v', proisstrict => 'f',
   proparallel => 'u', prorettype => 'void',
-  proargtypes => 'regclass name bool',
-  proargnames => '{relation,attname,inherited}',
+  proargtypes => 'text text text bool',
+  proargnames => '{schemaname,relname,attname,inherited}',
   prosrc => 'pg_clear_attribute_stats' },
 
 # GiST stratnum implementations
diff --git a/src/include/statistics/stat_utils.h b/src/include/statistics/stat_utils.h
index 0eb4decfcac..cad042c8e4a 100644
--- a/src/include/statistics/stat_utils.h
+++ b/src/include/statistics/stat_utils.h
@@ -32,6 +32,8 @@ extern bool stats_check_arg_pair(FunctionCallInfo fcinfo,
 
 extern void stats_lock_check_privileges(Oid reloid);
 
+extern Oid stats_schema_check_privileges(const char *nspname);
+
 extern bool stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
 											 FunctionCallInfo positional_fcinfo,
 											 struct StatsArgInfo *arginfo);
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index 6bcbee0edba..f87db2d6102 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -36,7 +36,8 @@
 
 enum attribute_stats_argnum
 {
-	ATTRELATION_ARG = 0,
+	ATTRELSCHEMA_ARG = 0,
+	ATTRELNAME_ARG,
 	ATTNAME_ARG,
 	ATTNUM_ARG,
 	INHERITED_ARG,
@@ -58,8 +59,9 @@ enum attribute_stats_argnum
 
 static struct StatsArgInfo attarginfo[] =
 {
-	[ATTRELATION_ARG] = {"relation", REGCLASSOID},
-	[ATTNAME_ARG] = {"attname", NAMEOID},
+	[ATTRELSCHEMA_ARG] = {"schemaname", TEXTOID},
+	[ATTRELNAME_ARG] = {"relname", TEXTOID},
+	[ATTNAME_ARG] = {"attname", TEXTOID},
 	[ATTNUM_ARG] = {"attnum", INT2OID},
 	[INHERITED_ARG] = {"inherited", BOOLOID},
 	[NULL_FRAC_ARG] = {"null_frac", FLOAT4OID},
@@ -80,7 +82,8 @@ static struct StatsArgInfo attarginfo[] =
 
 enum clear_attribute_stats_argnum
 {
-	C_ATTRELATION_ARG = 0,
+	C_ATTRELSCHEMA_ARG = 0,
+	C_ATTRELNAME_ARG,
 	C_ATTNAME_ARG,
 	C_INHERITED_ARG,
 	C_NUM_ATTRIBUTE_STATS_ARGS
@@ -88,8 +91,9 @@ enum clear_attribute_stats_argnum
 
 static struct StatsArgInfo cleararginfo[] =
 {
-	[C_ATTRELATION_ARG] = {"relation", REGCLASSOID},
-	[C_ATTNAME_ARG] = {"attname", NAMEOID},
+	[C_ATTRELSCHEMA_ARG] = {"relation", TEXTOID},
+	[C_ATTRELNAME_ARG] = {"relation", TEXTOID},
+	[C_ATTNAME_ARG] = {"attname", TEXTOID},
 	[C_INHERITED_ARG] = {"inherited", BOOLOID},
 	[C_NUM_ATTRIBUTE_STATS_ARGS] = {0}
 };
@@ -133,6 +137,9 @@ static void init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
 static bool
 attribute_statistics_update(FunctionCallInfo fcinfo)
 {
+	char	   *nspname;
+	Oid			nspoid;
+	char	   *relname;
 	Oid			reloid;
 	char	   *attname;
 	AttrNumber	attnum;
@@ -170,8 +177,23 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 	bool		result = true;
 
-	stats_check_required_arg(fcinfo, attarginfo, ATTRELATION_ARG);
-	reloid = PG_GETARG_OID(ATTRELATION_ARG);
+	stats_check_required_arg(fcinfo, attarginfo, ATTRELSCHEMA_ARG);
+	stats_check_required_arg(fcinfo, attarginfo, ATTRELNAME_ARG);
+
+	nspname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELSCHEMA_ARG));
+	nspoid = stats_schema_check_privileges(nspname);
+	if (nspoid == InvalidOid)
+		return false;
+
+	relname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELNAME_ARG));
+	reloid = get_relname_relid(relname, nspoid);
+	if (reloid == InvalidOid)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("Relation \"%s\".\"%s\" not found.", nspname, relname)));
+		return false;
+	}
 
 	if (RecoveryInProgress())
 		ereport(ERROR,
@@ -185,21 +207,18 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	/* user can specify either attname or attnum, but not both */
 	if (!PG_ARGISNULL(ATTNAME_ARG))
 	{
-		Name		attnamename;
-
 		if (!PG_ARGISNULL(ATTNUM_ARG))
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("cannot specify both attname and attnum")));
-		attnamename = PG_GETARG_NAME(ATTNAME_ARG);
-		attname = NameStr(*attnamename);
+		attname = TextDatumGetCString(PG_GETARG_DATUM(ATTNAME_ARG));
 		attnum = get_attnum(reloid, attname);
 		/* note that this test covers attisdropped cases too: */
 		if (attnum == InvalidAttrNumber)
 			ereport(ERROR,
 					(errcode(ERRCODE_UNDEFINED_COLUMN),
-					 errmsg("column \"%s\" of relation \"%s\" does not exist",
-							attname, get_rel_name(reloid))));
+					 errmsg("column \"%s\" of relation \"%s\".\"%s\" does not exist",
+							attname, nspname, relname)));
 	}
 	else if (!PG_ARGISNULL(ATTNUM_ARG))
 	{
@@ -210,8 +229,8 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 			!SearchSysCacheExistsAttName(reloid, attname))
 			ereport(ERROR,
 					(errcode(ERRCODE_UNDEFINED_COLUMN),
-					 errmsg("column %d of relation \"%s\" does not exist",
-							attnum, get_rel_name(reloid))));
+					 errmsg("column %d of relation \"%s\".\"%s\" does not exist",
+							attnum, nspname, relname)));
 	}
 	else
 	{
@@ -900,13 +919,33 @@ init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
 Datum
 pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 {
+	char	   *nspname;
+	Oid			nspoid;
+	char	   *relname;
 	Oid			reloid;
-	Name		attname;
+	char	   *attname;
 	AttrNumber	attnum;
 	bool		inherited;
 
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELATION_ARG);
-	reloid = PG_GETARG_OID(C_ATTRELATION_ARG);
+	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELSCHEMA_ARG);
+	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELNAME_ARG);
+	stats_check_required_arg(fcinfo, cleararginfo, C_ATTNAME_ARG);
+	stats_check_required_arg(fcinfo, cleararginfo, C_INHERITED_ARG);
+
+	nspname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELSCHEMA_ARG));
+	nspoid = stats_schema_check_privileges(nspname);
+	if (!OidIsValid(nspoid))
+		return false;
+
+	relname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELNAME_ARG));
+	reloid = get_relname_relid(relname, nspoid);
+	if (!OidIsValid(reloid))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("Relation \"%s\".\"%s\" not found.", nspname, relname)));
+		return false;
+	}
 
 	if (RecoveryInProgress())
 		ereport(ERROR,
@@ -916,23 +955,21 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 
 	stats_lock_check_privileges(reloid);
 
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTNAME_ARG);
-	attname = PG_GETARG_NAME(C_ATTNAME_ARG);
-	attnum = get_attnum(reloid, NameStr(*attname));
+	attname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTNAME_ARG));
+	attnum = get_attnum(reloid, attname);
 
 	if (attnum < 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot clear statistics on system column \"%s\"",
-						NameStr(*attname))));
+						attname)));
 
 	if (attnum == InvalidAttrNumber)
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 errmsg("column \"%s\" of relation \"%s\" does not exist",
-						NameStr(*attname), get_rel_name(reloid))));
+						attname, get_rel_name(reloid))));
 
-	stats_check_required_arg(fcinfo, cleararginfo, C_INHERITED_ARG);
 	inherited = PG_GETARG_BOOL(C_INHERITED_ARG);
 
 	delete_pg_statistic(reloid, attnum, inherited);
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index 52dfa477187..fdc69bc93e2 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -19,9 +19,12 @@
 
 #include "access/heapam.h"
 #include "catalog/indexing.h"
+#include "catalog/namespace.h"
 #include "statistics/stat_utils.h"
+#include "utils/builtins.h"
 #include "utils/fmgroids.h"
 #include "utils/fmgrprotos.h"
+#include "utils/lsyscache.h"
 #include "utils/syscache.h"
 
 
@@ -32,7 +35,8 @@
 
 enum relation_stats_argnum
 {
-	RELATION_ARG = 0,
+	RELSCHEMA_ARG = 0,
+	RELNAME_ARG,
 	RELPAGES_ARG,
 	RELTUPLES_ARG,
 	RELALLVISIBLE_ARG,
@@ -42,7 +46,8 @@ enum relation_stats_argnum
 
 static struct StatsArgInfo relarginfo[] =
 {
-	[RELATION_ARG] = {"relation", REGCLASSOID},
+	[RELSCHEMA_ARG] = {"schemaname", TEXTOID},
+	[RELNAME_ARG] = {"relname", TEXTOID},
 	[RELPAGES_ARG] = {"relpages", INT4OID},
 	[RELTUPLES_ARG] = {"reltuples", FLOAT4OID},
 	[RELALLVISIBLE_ARG] = {"relallvisible", INT4OID},
@@ -59,6 +64,9 @@ static bool
 relation_statistics_update(FunctionCallInfo fcinfo)
 {
 	bool		result = true;
+	char	   *nspname;
+	Oid			nspoid;
+	char	   *relname;
 	Oid			reloid;
 	Relation	crel;
 	BlockNumber relpages = 0;
@@ -76,6 +84,32 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 	bool		nulls[4] = {0};
 	int			nreplaces = 0;
 
+	stats_check_required_arg(fcinfo, relarginfo, RELSCHEMA_ARG);
+	stats_check_required_arg(fcinfo, relarginfo, RELNAME_ARG);
+
+	nspname = TextDatumGetCString(PG_GETARG_DATUM(RELSCHEMA_ARG));
+	nspoid = stats_schema_check_privileges(nspname);
+	if (!OidIsValid(nspoid))
+		return false;
+
+	relname = TextDatumGetCString(PG_GETARG_DATUM(RELNAME_ARG));
+	reloid = get_relname_relid(relname, nspoid);
+	if (!OidIsValid(reloid))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("Relation \"%s\".\"%s\" not found.", nspname, relname)));
+		return false;
+	}
+
+	if (RecoveryInProgress())
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("recovery is in progress"),
+				 errhint("Statistics cannot be modified during recovery.")));
+
+	stats_lock_check_privileges(reloid);
+
 	if (!PG_ARGISNULL(RELPAGES_ARG))
 	{
 		relpages = PG_GETARG_UINT32(RELPAGES_ARG);
@@ -108,17 +142,6 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 		update_relallfrozen = true;
 	}
 
-	stats_check_required_arg(fcinfo, relarginfo, RELATION_ARG);
-	reloid = PG_GETARG_OID(RELATION_ARG);
-
-	if (RecoveryInProgress())
-		ereport(ERROR,
-				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
-				 errmsg("recovery is in progress"),
-				 errhint("Statistics cannot be modified during recovery.")));
-
-	stats_lock_check_privileges(reloid);
-
 	/*
 	 * Take RowExclusiveLock on pg_class, consistent with
 	 * vac_update_relstats().
@@ -187,20 +210,22 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 Datum
 pg_clear_relation_stats(PG_FUNCTION_ARGS)
 {
-	LOCAL_FCINFO(newfcinfo, 5);
+	LOCAL_FCINFO(newfcinfo, 6);
 
-	InitFunctionCallInfoData(*newfcinfo, NULL, 5, InvalidOid, NULL, NULL);
+	InitFunctionCallInfoData(*newfcinfo, NULL, 6, InvalidOid, NULL, NULL);
 
-	newfcinfo->args[0].value = PG_GETARG_OID(0);
+	newfcinfo->args[0].value = PG_GETARG_DATUM(0);
 	newfcinfo->args[0].isnull = PG_ARGISNULL(0);
-	newfcinfo->args[1].value = UInt32GetDatum(0);
-	newfcinfo->args[1].isnull = false;
-	newfcinfo->args[2].value = Float4GetDatum(-1.0);
+	newfcinfo->args[1].value = PG_GETARG_DATUM(1);
+	newfcinfo->args[1].isnull = PG_ARGISNULL(1);
+	newfcinfo->args[2].value = UInt32GetDatum(0);
 	newfcinfo->args[2].isnull = false;
-	newfcinfo->args[3].value = UInt32GetDatum(0);
+	newfcinfo->args[3].value = Float4GetDatum(-1.0);
 	newfcinfo->args[3].isnull = false;
 	newfcinfo->args[4].value = UInt32GetDatum(0);
 	newfcinfo->args[4].isnull = false;
+	newfcinfo->args[5].value = UInt32GetDatum(0);
+	newfcinfo->args[5].isnull = false;
 
 	relation_statistics_update(newfcinfo);
 	PG_RETURN_VOID();
diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c
index 9647f5108b3..e037d4994e8 100644
--- a/src/backend/statistics/stat_utils.c
+++ b/src/backend/statistics/stat_utils.c
@@ -18,7 +18,9 @@
 
 #include "access/relation.h"
 #include "catalog/index.h"
+#include "catalog/namespace.h"
 #include "catalog/pg_database.h"
+#include "catalog/pg_namespace.h"
 #include "funcapi.h"
 #include "miscadmin.h"
 #include "statistics/stat_utils.h"
@@ -213,6 +215,41 @@ stats_lock_check_privileges(Oid reloid)
 	relation_close(table, NoLock);
 }
 
+
+/*
+ * Resolve a schema name into an Oid, ensure that the user has usage privs on
+ * that schema.
+ */
+Oid
+stats_schema_check_privileges(const char *nspname)
+{
+	Oid			nspoid;
+	AclResult	aclresult;
+
+	nspoid = get_namespace_oid(nspname, true);
+
+	if (nspoid == InvalidOid)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_SCHEMA_NAME),
+				 errmsg("schema %s does not exist", nspname)));
+		return InvalidOid;
+	}
+
+	aclresult = object_aclcheck(NamespaceRelationId, nspoid, GetUserId(), ACL_USAGE);
+
+	if (aclresult != ACLCHECK_OK)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for schema %s", nspname)));
+		return InvalidOid;
+	}
+
+	return nspoid;
+}
+
+
 /*
  * Find the argument number for the given argument name, returning -1 if not
  * found.
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index c371570501a..bd857bb076c 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -10490,7 +10490,6 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 	PQExpBuffer out;
 	DumpId	   *deps = NULL;
 	int			ndeps = 0;
-	char	   *qualified_name;
 	int			i_attname;
 	int			i_inherited;
 	int			i_null_frac;
@@ -10555,15 +10554,16 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 
 	out = createPQExpBuffer();
 
-	qualified_name = pg_strdup(fmtQualifiedDumpable(rsinfo));
-
 	/* restore relation stats */
 	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
 	appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
 					  fout->remoteVersion);
-	appendPQExpBufferStr(out, "\t'relation', ");
-	appendStringLiteralAH(out, qualified_name, fout);
-	appendPQExpBufferStr(out, "::regclass,\n");
+	appendPQExpBufferStr(out, "\t'schemaname', ");
+	appendStringLiteralAH(out, rsinfo->dobj.namespace->dobj.name, fout);
+	appendPQExpBufferStr(out, ",\n");
+	appendPQExpBufferStr(out, "\t'relname', ");
+	appendStringLiteralAH(out, rsinfo->dobj.name, fout);
+	appendPQExpBufferStr(out, ",\n");
 	appendPQExpBuffer(out, "\t'relpages', '%d'::integer,\n", rsinfo->relpages);
 	appendPQExpBuffer(out, "\t'reltuples', '%s'::real,\n", rsinfo->reltuples);
 	appendPQExpBuffer(out, "\t'relallvisible', '%d'::integer\n);\n",
@@ -10602,9 +10602,10 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
 		appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
 						  fout->remoteVersion);
-		appendPQExpBufferStr(out, "\t'relation', ");
-		appendStringLiteralAH(out, qualified_name, fout);
-		appendPQExpBufferStr(out, "::regclass");
+		appendPQExpBufferStr(out, "\t'schemaname', ");
+		appendStringLiteralAH(out, rsinfo->dobj.namespace->dobj.name, fout);
+		appendPQExpBufferStr(out, ",\n\t'relname', ");
+		appendStringLiteralAH(out, rsinfo->dobj.name, fout);
 
 		if (PQgetisnull(res, rownum, i_attname))
 			pg_fatal("attname cannot be NULL");
@@ -10616,7 +10617,10 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 		 * their attnames are not necessarily stable across dump/reload.
 		 */
 		if (rsinfo->nindAttNames == 0)
-			appendNamedArgument(out, fout, "attname", "name", attname);
+		{
+			appendPQExpBuffer(out, ",\n\t'attname', ");
+			appendStringLiteralAH(out, attname, fout);
+		}
 		else
 		{
 			bool		found = false;
@@ -10696,7 +10700,6 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 							  .deps = deps,
 							  .nDeps = ndeps));
 
-	free(qualified_name);
 	destroyPQExpBuffer(out);
 	destroyPQExpBuffer(query);
 }
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index c7bffc1b045..b037f239136 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -4725,14 +4725,16 @@ my %tests = (
 		regexp => qr/^
 			\QSELECT * FROM pg_catalog.pg_restore_relation_stats(\E\s+
 			'version',\s'\d+'::integer,\s+
-			'relation',\s'dump_test.dup_test_post_data_ix'::regclass,\s+
+			'schemaname',\s'dump_test',\s+
+			'relname',\s'dup_test_post_data_ix',\s+
 			'relpages',\s'\d+'::integer,\s+
 			'reltuples',\s'\d+'::real,\s+
 			'relallvisible',\s'\d+'::integer\s+
 			\);\s+
 			\QSELECT * FROM pg_catalog.pg_restore_attribute_stats(\E\s+
 			'version',\s'\d+'::integer,\s+
-			'relation',\s'dump_test.dup_test_post_data_ix'::regclass,\s+
+			'schemaname',\s'dump_test',\s+
+			'relname',\s'dup_test_post_data_ix',\s+
 			'attnum',\s'2'::smallint,\s+
 			'inherited',\s'f'::boolean,\s+
 			'null_frac',\s'0'::real,\s+
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index 1f46d5e7854..2f1295f2149 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -14,7 +14,8 @@ CREATE TABLE stats_import.test(
 ) WITH (autovacuum_enabled = false);
 SELECT
     pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', 18::integer,
         'reltuples', 21::real,
         'relallvisible', 24::integer,
@@ -36,7 +37,7 @@ ORDER BY relname;
  test    |       18 |        21 |            24 |           27
 (1 row)
 
-SELECT pg_clear_relation_stats('stats_import.test'::regclass);
+SELECT pg_clear_relation_stats('stats_import', 'test');
  pg_clear_relation_stats 
 -------------------------
  
@@ -45,33 +46,54 @@ SELECT pg_clear_relation_stats('stats_import.test'::regclass);
 --
 -- relstats tests
 --
---- error: relation is wrong type
+-- error: schemaname missing
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::oid,
+        'relname', 'test',
         'relpages', 17::integer);
-WARNING:  argument "relation" has type "oid", expected type "regclass"
-ERROR:  "relation" cannot be NULL
+ERROR:  "schemaname" cannot be NULL
+-- error: relname missing
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relpages', 17::integer);
+ERROR:  "relname" cannot be NULL
+--- error: schemaname is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 3.6::float,
+        'relname', 'test',
+        'relpages', 17::integer);
+WARNING:  argument "schemaname" has type "double precision", expected type "text"
+ERROR:  "schemaname" cannot be NULL
+--- error: relname is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relname', 0::oid,
+        'relpages', 17::integer);
+WARNING:  argument "relname" has type "oid", expected type "text"
+ERROR:  "relname" cannot be NULL
 -- error: relation not found
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::oid::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'nope',
         'relpages', 17::integer);
-ERROR:  could not open relation with OID 0
+WARNING:  Relation "stats_import"."nope" not found.
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
 -- error: odd number of variadic arguments cannot be pairs
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relallvisible');
 ERROR:  variadic arguments must be name/value pairs
 HINT:  Provide an even number of variadic arguments that can be divided into pairs.
 -- error: argument name is NULL
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         NULL, '17'::integer);
-ERROR:  name at variadic position 3 is NULL
--- error: argument name is not a text type
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        17, '17'::integer);
-ERROR:  name at variadic position 3 has type "integer", expected type "text"
+ERROR:  name at variadic position 5 is NULL
 -- starting stats
 SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
@@ -84,7 +106,8 @@ WHERE oid = 'stats_import.test_i'::regclass;
 -- regular indexes have special case locking rules
 BEGIN;
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.test_i'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test_i',
         'relpages', 18::integer);
  pg_restore_relation_stats 
 ---------------------------
@@ -132,7 +155,8 @@ WHERE oid = 'stats_import.part_parent'::regclass;
 --
 BEGIN;
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent_i'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'part_parent_i',
         'relpages', 2::integer);
  pg_restore_relation_stats 
 ---------------------------
@@ -166,7 +190,8 @@ WHERE oid = 'stats_import.part_parent_i'::regclass;
 
 -- ok: set all relstats, with version, no bounds checking
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'version', 150000::integer,
         'relpages', '-17'::integer,
         'reltuples', 400::real,
@@ -187,7 +212,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just relpages, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', '16'::integer);
  pg_restore_relation_stats 
 ---------------------------
@@ -204,7 +230,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just reltuples, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'reltuples', '500'::real);
  pg_restore_relation_stats 
 ---------------------------
@@ -221,7 +248,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just relallvisible, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relallvisible', 5::integer);
  pg_restore_relation_stats 
 ---------------------------
@@ -238,7 +266,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: just relallfrozen
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'version', 150000::integer,
         'relallfrozen', 3::integer);
  pg_restore_relation_stats 
@@ -256,7 +285,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- warn: bad relpages type, rest updated
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', 'nope'::text,
         'reltuples', 400.0::real,
         'relallvisible', 4::integer,
@@ -277,7 +307,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- unrecognized argument name, rest ok
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', '171'::integer,
         'nope', 10::integer);
 WARNING:  unrecognized argument name: "nope"
@@ -295,8 +326,7 @@ WHERE oid = 'stats_import.test'::regclass;
 (1 row)
 
 -- ok: clear stats
-SELECT pg_catalog.pg_clear_relation_stats(
-    relation => 'stats_import.test'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'test');
  pg_clear_relation_stats 
 -------------------------
  
@@ -313,87 +343,123 @@ WHERE oid = 'stats_import.test'::regclass;
 -- invalid relkinds for statistics
 CREATE SEQUENCE stats_import.testseq;
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.testseq'::regclass);
+        'schemaname', 'stats_import',
+        'relname', 'testseq');
 ERROR:  cannot modify statistics for relation "testseq"
 DETAIL:  This operation is not supported for sequences.
-SELECT pg_catalog.pg_clear_relation_stats(
-        'stats_import.testseq'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testseq');
 ERROR:  cannot modify statistics for relation "testseq"
 DETAIL:  This operation is not supported for sequences.
 CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
-SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.testview'::regclass);
-ERROR:  cannot modify statistics for relation "testview"
-DETAIL:  This operation is not supported for views.
-SELECT pg_catalog.pg_clear_relation_stats(
-        'stats_import.testview'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testview');
 ERROR:  cannot modify statistics for relation "testview"
 DETAIL:  This operation is not supported for views.
 --
 -- attribute stats
 --
--- error: object does not exist
+-- error: schemaname missing
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', '0'::oid::regclass,
-    'attname', 'id'::name,
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  could not open relation with OID 0
--- error: relation null
+ERROR:  "schemaname" cannot be NULL
+-- error: schema does not exist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', NULL::oid::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'nope',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  "relation" cannot be NULL
+WARNING:  schema nope does not exist
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- error: relname missing
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+ERROR:  "relname" cannot be NULL
+-- error: relname does not exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'relname', 'nope',
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+WARNING:  Relation "stats_import"."nope" not found.
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- error: relname null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'relname', NULL,
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+ERROR:  "relname" cannot be NULL
 -- error: NULL attname
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', NULL::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', NULL,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 ERROR:  must specify either attname or attnum
 -- error: attname doesn't exist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'nope'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'nope',
     'inherited', false::boolean,
     'null_frac', 0.1::real,
     'avg_width', 2::integer,
     'n_distinct', 0.3::real);
-ERROR:  column "nope" of relation "test" does not exist
+ERROR:  column "nope" of relation "stats_import"."test" does not exist
 -- error: both attname and attnum
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'attnum', 1::smallint,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 ERROR:  cannot specify both attname and attnum
 -- error: neither attname nor attnum
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
+    'schemaname', 'stats_import',
+    'relname', 'test',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 ERROR:  must specify either attname or attnum
 -- error: attribute is system column
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'xmin'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'xmin',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 ERROR:  cannot modify statistics on system column "xmin"
 -- error: inherited null
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', NULL::boolean,
     'null_frac', 0.1::real);
 ERROR:  "inherited" cannot be NULL
 -- ok: just the fixed values, with version, no stakinds
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'version', 150000::integer,
     'null_frac', 0.2::real,
@@ -421,7 +487,8 @@ AND attname = 'id';
 -- for any stat-having relation.
 --
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
+    'schemaname', 'stats_import',
+    'relname', 'test',
     'attnum', 1::smallint,
     'inherited', false::boolean,
     'null_frac', 0.4::real);
@@ -443,8 +510,9 @@ AND attname = 'id';
 
 -- warn: unrecognized argument name, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.2::real,
     'nope', 0.5::real);
@@ -467,8 +535,9 @@ AND attname = 'id';
 
 -- warn: mcv / mcf null mismatch part 1, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.21::real,
     'most_common_freqs', '{0.1,0.2,0.3}'::real[]
@@ -492,8 +561,9 @@ AND attname = 'id';
 
 -- warn: mcv / mcf null mismatch part 2, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.21::real,
     'most_common_vals', '{1,2,3}'::text
@@ -517,8 +587,9 @@ AND attname = 'id';
 
 -- warn: mcf type mismatch, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.22::real,
     'most_common_vals', '{2,1,3}'::text,
@@ -544,8 +615,9 @@ AND attname = 'id';
 
 -- warn: mcv cast failure, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.23::real,
     'most_common_vals', '{2,four,3}'::text,
@@ -570,8 +642,9 @@ AND attname = 'id';
 
 -- ok: mcv+mcf
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'most_common_vals', '{2,1,3}'::text,
     'most_common_freqs', '{0.3,0.25,0.05}'::real[]
@@ -594,8 +667,9 @@ AND attname = 'id';
 
 -- warn: NULL in histogram array, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.24::real,
     'histogram_bounds', '{1,NULL,3,4}'::text
@@ -619,8 +693,9 @@ AND attname = 'id';
 
 -- ok: histogram_bounds
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'histogram_bounds', '{1,2,3,4}'::text
     );
@@ -642,8 +717,9 @@ AND attname = 'id';
 
 -- warn: elem_count_histogram null element, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.25::real,
     'elem_count_histogram', '{1,1,NULL,1,1,1,1,1}'::real[]
@@ -667,8 +743,9 @@ AND attname = 'tags';
 
 -- ok: elem_count_histogram
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.26::real,
     'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
@@ -691,8 +768,9 @@ AND attname = 'tags';
 
 -- warn: range stats on a scalar type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.27::real,
     'range_empty_frac', 0.5::real,
@@ -718,8 +796,9 @@ AND attname = 'id';
 
 -- warn: range_empty_frac range_length_hist null mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.28::real,
     'range_length_histogram', '{399,499,Infinity}'::text
@@ -743,8 +822,9 @@ AND attname = 'arange';
 
 -- warn: range_empty_frac range_length_hist null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.29::real,
     'range_empty_frac', 0.5::real
@@ -768,8 +848,9 @@ AND attname = 'arange';
 
 -- ok: range_empty_frac + range_length_hist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'range_empty_frac', 0.5::real,
     'range_length_histogram', '{399,499,Infinity}'::text
@@ -792,8 +873,9 @@ AND attname = 'arange';
 
 -- warn: range bounds histogram on scalar, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.31::real,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
@@ -818,8 +900,9 @@ AND attname = 'id';
 
 -- ok: range_bounds_histogram
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
     );
@@ -841,8 +924,9 @@ AND attname = 'arange';
 
 -- warn: cannot set most_common_elems for range type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.32::real,
     'most_common_elems', '{3,1}'::text,
@@ -868,8 +952,9 @@ AND attname = 'arange';
 
 -- warn: scalars can't have mcelem, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.33::real,
     'most_common_elems', '{1,3}'::text,
@@ -895,8 +980,9 @@ AND attname = 'id';
 
 -- warn: mcelem / mcelem mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.34::real,
     'most_common_elems', '{one,two}'::text
@@ -920,8 +1006,9 @@ AND attname = 'tags';
 
 -- warn: mcelem / mcelem null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.35::real,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
@@ -945,8 +1032,9 @@ AND attname = 'tags';
 
 -- ok: mcelem
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'most_common_elems', '{one,three}'::text,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
@@ -969,8 +1057,9 @@ AND attname = 'tags';
 
 -- warn: scalars can't have elem_count_histogram, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.36::real,
     'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1}'::real[]
@@ -1022,8 +1111,9 @@ SELECT s.schemaname, s.tablename, s.attname, s.inherited, r.*
 FROM pg_catalog.pg_stats AS s
 CROSS JOIN LATERAL
     pg_catalog.pg_restore_attribute_stats(
-        'relation', ('stats_import.' || s.tablename || '_clone')::regclass,
-        'attname', s.attname,
+        'schemaname', 'stats_import',
+        'relname', s.tablename::text || '_clone',
+        'attname', s.attname::text,
         'inherited', s.inherited,
         'version', 150000,
         'null_frac', s.null_frac,
@@ -1200,9 +1290,10 @@ AND attname = 'arange';
 (1 row)
 
 SELECT pg_catalog.pg_clear_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'arange'::name,
-    inherited => false::boolean);
+    schemaname => 'stats_import',
+    relname => 'test',
+    attname => 'arange',
+    inherited => false);
  pg_clear_attribute_stats 
 --------------------------
  
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index 0ec590688c2..ccdc44e9236 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -17,7 +17,8 @@ CREATE TABLE stats_import.test(
 
 SELECT
     pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', 18::integer,
         'reltuples', 21::real,
         'relallvisible', 24::integer,
@@ -32,37 +33,52 @@ FROM pg_class
 WHERE oid = 'stats_import.test'::regclass
 ORDER BY relname;
 
-SELECT pg_clear_relation_stats('stats_import.test'::regclass);
+SELECT pg_clear_relation_stats('stats_import', 'test');
 
 --
 -- relstats tests
 --
 
---- error: relation is wrong type
+-- error: schemaname missing
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::oid,
+        'relname', 'test',
+        'relpages', 17::integer);
+
+-- error: relname missing
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relpages', 17::integer);
+
+--- error: schemaname is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 3.6::float,
+        'relname', 'test',
+        'relpages', 17::integer);
+
+--- error: relname is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relname', 0::oid,
         'relpages', 17::integer);
 
 -- error: relation not found
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::oid::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'nope',
         'relpages', 17::integer);
 
 -- error: odd number of variadic arguments cannot be pairs
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relallvisible');
 
 -- error: argument name is NULL
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         NULL, '17'::integer);
 
--- error: argument name is not a text type
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        17, '17'::integer);
-
 -- starting stats
 SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
@@ -71,7 +87,8 @@ WHERE oid = 'stats_import.test_i'::regclass;
 -- regular indexes have special case locking rules
 BEGIN;
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.test_i'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test_i',
         'relpages', 18::integer);
 
 SELECT mode FROM pg_locks
@@ -108,7 +125,8 @@ WHERE oid = 'stats_import.part_parent'::regclass;
 BEGIN;
 
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent_i'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'part_parent_i',
         'relpages', 2::integer);
 
 SELECT mode FROM pg_locks
@@ -127,7 +145,8 @@ WHERE oid = 'stats_import.part_parent_i'::regclass;
 
 -- ok: set all relstats, with version, no bounds checking
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'version', 150000::integer,
         'relpages', '-17'::integer,
         'reltuples', 400::real,
@@ -140,7 +159,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just relpages, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', '16'::integer);
 
 SELECT relpages, reltuples, relallvisible, relallfrozen
@@ -149,7 +169,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just reltuples, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'reltuples', '500'::real);
 
 SELECT relpages, reltuples, relallvisible, relallfrozen
@@ -158,7 +179,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just relallvisible, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relallvisible', 5::integer);
 
 SELECT relpages, reltuples, relallvisible, relallfrozen
@@ -167,7 +189,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: just relallfrozen
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'version', 150000::integer,
         'relallfrozen', 3::integer);
 
@@ -177,7 +200,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- warn: bad relpages type, rest updated
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', 'nope'::text,
         'reltuples', 400.0::real,
         'relallvisible', 4::integer,
@@ -189,7 +213,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- unrecognized argument name, rest ok
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', '171'::integer,
         'nope', 10::integer);
 
@@ -198,8 +223,7 @@ FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: clear stats
-SELECT pg_catalog.pg_clear_relation_stats(
-    relation => 'stats_import.test'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'test');
 
 SELECT relpages, reltuples, relallvisible
 FROM pg_class
@@ -209,48 +233,70 @@ WHERE oid = 'stats_import.test'::regclass;
 CREATE SEQUENCE stats_import.testseq;
 
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.testseq'::regclass);
+        'schemaname', 'stats_import',
+        'relname', 'testseq');
 
-SELECT pg_catalog.pg_clear_relation_stats(
-        'stats_import.testseq'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testseq');
 
 CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
 
-SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.testview'::regclass);
-
-SELECT pg_catalog.pg_clear_relation_stats(
-        'stats_import.testview'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testview');
 
 --
 -- attribute stats
 --
 
--- error: object does not exist
+-- error: schemaname missing
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', '0'::oid::regclass,
-    'attname', 'id'::name,
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: relation null
+-- error: schema does not exist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', NULL::oid::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'nope',
+    'relname', 'test',
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: relname missing
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: relname does not exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'relname', 'nope',
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: relname null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'relname', NULL,
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
 -- error: NULL attname
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', NULL::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', NULL,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
 -- error: attname doesn't exist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'nope'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'nope',
     'inherited', false::boolean,
     'null_frac', 0.1::real,
     'avg_width', 2::integer,
@@ -258,36 +304,41 @@ SELECT pg_catalog.pg_restore_attribute_stats(
 
 -- error: both attname and attnum
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'attnum', 1::smallint,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
 -- error: neither attname nor attnum
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
+    'schemaname', 'stats_import',
+    'relname', 'test',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
 -- error: attribute is system column
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'xmin'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'xmin',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
 -- error: inherited null
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', NULL::boolean,
     'null_frac', 0.1::real);
 
 -- ok: just the fixed values, with version, no stakinds
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'version', 150000::integer,
     'null_frac', 0.2::real,
@@ -307,7 +358,8 @@ AND attname = 'id';
 -- for any stat-having relation.
 --
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
+    'schemaname', 'stats_import',
+    'relname', 'test',
     'attnum', 1::smallint,
     'inherited', false::boolean,
     'null_frac', 0.4::real);
@@ -321,8 +373,9 @@ AND attname = 'id';
 
 -- warn: unrecognized argument name, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.2::real,
     'nope', 0.5::real);
@@ -336,8 +389,9 @@ AND attname = 'id';
 
 -- warn: mcv / mcf null mismatch part 1, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.21::real,
     'most_common_freqs', '{0.1,0.2,0.3}'::real[]
@@ -352,8 +406,9 @@ AND attname = 'id';
 
 -- warn: mcv / mcf null mismatch part 2, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.21::real,
     'most_common_vals', '{1,2,3}'::text
@@ -368,8 +423,9 @@ AND attname = 'id';
 
 -- warn: mcf type mismatch, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.22::real,
     'most_common_vals', '{2,1,3}'::text,
@@ -385,8 +441,9 @@ AND attname = 'id';
 
 -- warn: mcv cast failure, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.23::real,
     'most_common_vals', '{2,four,3}'::text,
@@ -402,8 +459,9 @@ AND attname = 'id';
 
 -- ok: mcv+mcf
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'most_common_vals', '{2,1,3}'::text,
     'most_common_freqs', '{0.3,0.25,0.05}'::real[]
@@ -418,8 +476,9 @@ AND attname = 'id';
 
 -- warn: NULL in histogram array, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.24::real,
     'histogram_bounds', '{1,NULL,3,4}'::text
@@ -434,8 +493,9 @@ AND attname = 'id';
 
 -- ok: histogram_bounds
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'histogram_bounds', '{1,2,3,4}'::text
     );
@@ -449,8 +509,9 @@ AND attname = 'id';
 
 -- warn: elem_count_histogram null element, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.25::real,
     'elem_count_histogram', '{1,1,NULL,1,1,1,1,1}'::real[]
@@ -465,8 +526,9 @@ AND attname = 'tags';
 
 -- ok: elem_count_histogram
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.26::real,
     'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
@@ -481,8 +543,9 @@ AND attname = 'tags';
 
 -- warn: range stats on a scalar type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.27::real,
     'range_empty_frac', 0.5::real,
@@ -498,8 +561,9 @@ AND attname = 'id';
 
 -- warn: range_empty_frac range_length_hist null mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.28::real,
     'range_length_histogram', '{399,499,Infinity}'::text
@@ -514,8 +578,9 @@ AND attname = 'arange';
 
 -- warn: range_empty_frac range_length_hist null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.29::real,
     'range_empty_frac', 0.5::real
@@ -530,8 +595,9 @@ AND attname = 'arange';
 
 -- ok: range_empty_frac + range_length_hist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'range_empty_frac', 0.5::real,
     'range_length_histogram', '{399,499,Infinity}'::text
@@ -546,8 +612,9 @@ AND attname = 'arange';
 
 -- warn: range bounds histogram on scalar, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.31::real,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
@@ -562,8 +629,9 @@ AND attname = 'id';
 
 -- ok: range_bounds_histogram
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
     );
@@ -577,8 +645,9 @@ AND attname = 'arange';
 
 -- warn: cannot set most_common_elems for range type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.32::real,
     'most_common_elems', '{3,1}'::text,
@@ -594,8 +663,9 @@ AND attname = 'arange';
 
 -- warn: scalars can't have mcelem, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.33::real,
     'most_common_elems', '{1,3}'::text,
@@ -611,8 +681,9 @@ AND attname = 'id';
 
 -- warn: mcelem / mcelem mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.34::real,
     'most_common_elems', '{one,two}'::text
@@ -627,8 +698,9 @@ AND attname = 'tags';
 
 -- warn: mcelem / mcelem null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.35::real,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
@@ -643,8 +715,9 @@ AND attname = 'tags';
 
 -- ok: mcelem
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'most_common_elems', '{one,three}'::text,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
@@ -659,8 +732,9 @@ AND attname = 'tags';
 
 -- warn: scalars can't have elem_count_histogram, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.36::real,
     'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1}'::real[]
@@ -707,8 +781,9 @@ SELECT s.schemaname, s.tablename, s.attname, s.inherited, r.*
 FROM pg_catalog.pg_stats AS s
 CROSS JOIN LATERAL
     pg_catalog.pg_restore_attribute_stats(
-        'relation', ('stats_import.' || s.tablename || '_clone')::regclass,
-        'attname', s.attname,
+        'schemaname', 'stats_import',
+        'relname', s.tablename::text || '_clone',
+        'attname', s.attname::text,
         'inherited', s.inherited,
         'version', 150000,
         'null_frac', s.null_frac,
@@ -853,9 +928,10 @@ AND inherited = false
 AND attname = 'arange';
 
 SELECT pg_catalog.pg_clear_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'arange'::name,
-    inherited => false::boolean);
+    schemaname => 'stats_import',
+    relname => 'test',
+    attname => 'arange',
+    inherited => false);
 
 SELECT COUNT(*)
 FROM pg_stats
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 1c3810e1a04..a75e95bc5fd 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30365,22 +30365,24 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          <structname>mytable</structname>:
 <programlisting>
  SELECT pg_restore_relation_stats(
-    'relation',  'mytable'::regclass,
-    'relpages',  173::integer,
-    'reltuples', 10000::real);
+    'schemaname', 'myschema',
+    'relname',    'mytable',
+    'relpages',   173::integer,
+    'reltuples',  10000::real);
 </programlisting>
         </para>
         <para>
-         The argument <literal>relation</literal> with a value of type
-         <type>regclass</type> is required, and specifies the table. Other
-         arguments are the names and values of statistics corresponding to
-         certain columns in <link
+         The arguments <literal>schemaname</literal> with a value of type
+         <type>regclass</type> and <literal>relname</literal> are required,
+         and specifies the table. Other arguments are the names and values
+         of statistics corresponding to certain columns in <link
          linkend="catalog-pg-class"><structname>pg_class</structname></link>.
          The currently-supported relation statistics are
          <literal>relpages</literal> with a value of type
          <type>integer</type>, <literal>reltuples</literal> with a value of
-         type <type>real</type>, and <literal>relallvisible</literal> with a
-         value of type <type>integer</type>.
+         type <type>real</type>, <literal>relallvisible</literal> with a
+         value of type <type>integer</type>, and <literal>relallfrozen</literal>
+         with a value of type <type>integer</type>.
         </para>
         <para>
          Additionally, this function accepts argument name
@@ -30408,7 +30410,7 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          <indexterm>
           <primary>pg_clear_relation_stats</primary>
          </indexterm>
-         <function>pg_clear_relation_stats</function> ( <parameter>relation</parameter> <type>regclass</type> )
+         <function>pg_clear_relation_stats</function> ( <parameter>schemaname</parameter> <type>text</type>, <parameter>relname</parameter> <type>text</type> )
          <returnvalue>void</returnvalue>
         </para>
         <para>
@@ -30457,16 +30459,18 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          <structname>mytable</structname>:
 <programlisting>
  SELECT pg_restore_attribute_stats(
-    'relation',    'mytable'::regclass,
-    'attname',     'col1'::name,
-    'inherited',   false,
-    'avg_width',   125::integer,
-    'null_frac',   0.5::real);
+    'schemaname', 'myschema',
+    'relname',    'mytable',
+    'attname',    'col1',
+    'inherited',  false,
+    'avg_width',  125::integer,
+    'null_frac',  0.5::real);
 </programlisting>
         </para>
         <para>
-         The required arguments are <literal>relation</literal> with a value
-         of type <type>regclass</type>, which specifies the table; either
+         The required arguments are <literal>schemaname</literal> with a value
+         of type <type>regclass</type> and <literal>relname</literal> with a value
+         of type <type>text</type> which specify the table; either
          <literal>attname</literal> with a value of type <type>name</type> or
          <literal>attnum</literal> with a value of type <type>smallint</type>,
          which specifies the column; and <literal>inherited</literal>, which
@@ -30502,7 +30506,8 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
           <primary>pg_clear_attribute_stats</primary>
          </indexterm>
          <function>pg_clear_attribute_stats</function> (
-         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>schemaname</parameter> <type>text</type>,
+         <parameter>relname</parameter> <type>text</type>,
          <parameter>attname</parameter> <type>name</type>,
          <parameter>inherited</parameter> <type>boolean</type> )
          <returnvalue>void</returnvalue>

base-commit: 6d376c3b0d1e79c318d2a1c04097025784e28377
-- 
2.48.1

v8-0002-Downgrade-as-man-pg_restore_-_stats-errors-to-war.patchtext/x-patch; charset=US-ASCII; name=v8-0002-Downgrade-as-man-pg_restore_-_stats-errors-to-war.patchDownload
From 98f1eea90be0804ecdd43a89306aa83a6b9784c5 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 8 Mar 2025 00:52:41 -0500
Subject: [PATCH v8 2/4] Downgrade as man pg_restore_*_stats errors to
 warnings.

We want to avoid errors that can potentially stop an otherwise
successful pg_upgrade or pg_restore operation. With that in mind, change
as many ERROR reports to WARNING + early termination with no data
updated.
---
 src/include/statistics/stat_utils.h        |   4 +-
 src/backend/statistics/attribute_stats.c   | 124 +++++++++++-----
 src/backend/statistics/relation_stats.c    |  10 +-
 src/backend/statistics/stat_utils.c        |  51 +++++--
 src/test/regress/expected/stats_import.out | 163 ++++++++++++++++-----
 src/test/regress/sql/stats_import.sql      |  36 ++---
 6 files changed, 277 insertions(+), 111 deletions(-)

diff --git a/src/include/statistics/stat_utils.h b/src/include/statistics/stat_utils.h
index cad042c8e4a..298cbae3436 100644
--- a/src/include/statistics/stat_utils.h
+++ b/src/include/statistics/stat_utils.h
@@ -21,7 +21,7 @@ struct StatsArgInfo
 	Oid			argtype;
 };
 
-extern void stats_check_required_arg(FunctionCallInfo fcinfo,
+extern bool stats_check_required_arg(FunctionCallInfo fcinfo,
 									 struct StatsArgInfo *arginfo,
 									 int argnum);
 extern bool stats_check_arg_array(FunctionCallInfo fcinfo,
@@ -30,7 +30,7 @@ extern bool stats_check_arg_pair(FunctionCallInfo fcinfo,
 								 struct StatsArgInfo *arginfo,
 								 int argnum1, int argnum2);
 
-extern void stats_lock_check_privileges(Oid reloid);
+extern bool stats_lock_check_privileges(Oid reloid);
 
 extern Oid stats_schema_check_privileges(const char *nspname);
 
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index f87db2d6102..4f9bc18f8c6 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -100,7 +100,7 @@ static struct StatsArgInfo cleararginfo[] =
 
 static bool attribute_statistics_update(FunctionCallInfo fcinfo);
 static Node *get_attr_expr(Relation rel, int attnum);
-static void get_attr_stat_type(Oid reloid, AttrNumber attnum,
+static bool get_attr_stat_type(Oid reloid, AttrNumber attnum,
 							   Oid *atttypid, int32 *atttypmod,
 							   char *atttyptype, Oid *atttypcoll,
 							   Oid *eq_opr, Oid *lt_opr);
@@ -129,10 +129,12 @@ static void init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
  * stored as an anyarray, and the representation of the array needs to store
  * the correct element type, which must be derived from the attribute.
  *
- * Major errors, such as the table not existing, the attribute not existing,
- * or a permissions failure are always reported at ERROR. Other errors, such
- * as a conversion failure on one statistic kind, are reported as a WARNING
- * and other statistic kinds may still be updated.
+ * This function is called during database upgrades and restorations, therefore
+ * it is imperative to avoid ERRORs that could potentially end the upgrade or
+ * restore unless. Major errors, such as the table not existing, the attribute
+ * not existing, or permissions failure are reported as WARNINGs with an end to
+ * the function, thus allowing the upgrade/restore to continue, but without the
+ * stats that can be regenereated once the database is online again.
  */
 static bool
 attribute_statistics_update(FunctionCallInfo fcinfo)
@@ -149,8 +151,8 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	HeapTuple	statup;
 
 	Oid			atttypid = InvalidOid;
-	int32		atttypmod;
-	char		atttyptype;
+	int32		atttypmod = -1;
+	char		atttyptype = TYPTYPE_PSEUDO; /* Not a great default, but there is no TYPTYPE_INVALID */
 	Oid			atttypcoll = InvalidOid;
 	Oid			eq_opr = InvalidOid;
 	Oid			lt_opr = InvalidOid;
@@ -177,17 +179,19 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 	bool		result = true;
 
-	stats_check_required_arg(fcinfo, attarginfo, ATTRELSCHEMA_ARG);
-	stats_check_required_arg(fcinfo, attarginfo, ATTRELNAME_ARG);
+	if (!stats_check_required_arg(fcinfo, attarginfo, ATTRELSCHEMA_ARG))
+		return false;
+	if (!stats_check_required_arg(fcinfo, attarginfo, ATTRELNAME_ARG))
+		return false;
 
 	nspname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELSCHEMA_ARG));
 	nspoid = stats_schema_check_privileges(nspname);
-	if (nspoid == InvalidOid)
+	if (!OidIsValid(nspoid))
 		return false;
 
 	relname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELNAME_ARG));
 	reloid = get_relname_relid(relname, nspoid);
-	if (reloid == InvalidOid)
+	if (!OidIsValid(reloid))
 	{
 		ereport(WARNING,
 				(errcode(ERRCODE_UNDEFINED_OBJECT),
@@ -196,29 +200,39 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	}
 
 	if (RecoveryInProgress())
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("recovery is in progress"),
 				 errhint("Statistics cannot be modified during recovery.")));
+		return false;
+	}
 
 	/* lock before looking up attribute */
-	stats_lock_check_privileges(reloid);
+	if (!stats_lock_check_privileges(reloid))
+		return false;
 
 	/* user can specify either attname or attnum, but not both */
 	if (!PG_ARGISNULL(ATTNAME_ARG))
 	{
 		if (!PG_ARGISNULL(ATTNUM_ARG))
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("cannot specify both attname and attnum")));
+			return false;
+		}
 		attname = TextDatumGetCString(PG_GETARG_DATUM(ATTNAME_ARG));
 		attnum = get_attnum(reloid, attname);
 		/* note that this test covers attisdropped cases too: */
 		if (attnum == InvalidAttrNumber)
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 					(errcode(ERRCODE_UNDEFINED_COLUMN),
 					 errmsg("column \"%s\" of relation \"%s\".\"%s\" does not exist",
 							attname, nspname, relname)));
+			return false;
+		}
 	}
 	else if (!PG_ARGISNULL(ATTNUM_ARG))
 	{
@@ -227,27 +241,33 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 		/* annoyingly, get_attname doesn't check attisdropped */
 		if (attname == NULL ||
 			!SearchSysCacheExistsAttName(reloid, attname))
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 					(errcode(ERRCODE_UNDEFINED_COLUMN),
 					 errmsg("column %d of relation \"%s\".\"%s\" does not exist",
 							attnum, nspname, relname)));
+			return false;
+		}
 	}
 	else
 	{
-		ereport(ERROR,
+		ereport(WARNING,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("must specify either attname or attnum")));
-		attname = NULL;			/* keep compiler quiet */
-		attnum = 0;
+		return false;
 	}
 
 	if (attnum < 0)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot modify statistics on system column \"%s\"",
 						attname)));
+		return false;
+	}
 
-	stats_check_required_arg(fcinfo, attarginfo, INHERITED_ARG);
+	if (!stats_check_required_arg(fcinfo, attarginfo, INHERITED_ARG))
+		return false;
 	inherited = PG_GETARG_BOOL(INHERITED_ARG);
 
 	/*
@@ -296,10 +316,11 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	}
 
 	/* derive information from attribute */
-	get_attr_stat_type(reloid, attnum,
-					   &atttypid, &atttypmod,
-					   &atttyptype, &atttypcoll,
-					   &eq_opr, &lt_opr);
+	if (!get_attr_stat_type(reloid, attnum,
+							&atttypid, &atttypmod,
+							&atttyptype, &atttypcoll,
+							&eq_opr, &lt_opr))
+		result = false;
 
 	/* if needed, derive element type */
 	if (do_mcelem || do_dechist)
@@ -579,7 +600,7 @@ get_attr_expr(Relation rel, int attnum)
 /*
  * Derive type information from the attribute.
  */
-static void
+static bool
 get_attr_stat_type(Oid reloid, AttrNumber attnum,
 				   Oid *atttypid, int32 *atttypmod,
 				   char *atttyptype, Oid *atttypcoll,
@@ -596,18 +617,26 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum,
 
 	/* Attribute not found */
 	if (!HeapTupleIsValid(atup))
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 errmsg("attribute %d of relation \"%s\" does not exist",
 						attnum, RelationGetRelationName(rel))));
+		relation_close(rel, NoLock);
+		return false;
+	}
 
 	attr = (Form_pg_attribute) GETSTRUCT(atup);
 
 	if (attr->attisdropped)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 errmsg("attribute %d of relation \"%s\" does not exist",
 						attnum, RelationGetRelationName(rel))));
+		relation_close(rel, NoLock);
+		return false;
+	}
 
 	expr = get_attr_expr(rel, attr->attnum);
 
@@ -656,6 +685,7 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum,
 		*atttypcoll = DEFAULT_COLLATION_OID;
 
 	relation_close(rel, NoLock);
+	return true;
 }
 
 /*
@@ -781,6 +811,10 @@ set_stats_slot(Datum *values, bool *nulls, bool *replaces,
 	if (slotidx >= STATISTIC_NUM_SLOTS && first_empty >= 0)
 		slotidx = first_empty;
 
+	/*
+	 * Currently there is no datatype that can have more than STATISTIC_NUM_SLOTS
+	 * statistic kinds, so this can safely remain an ERROR for now.
+	 */
 	if (slotidx >= STATISTIC_NUM_SLOTS)
 		ereport(ERROR,
 				(errmsg("maximum number of statistics slots exceeded: %d",
@@ -927,15 +961,19 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 	AttrNumber	attnum;
 	bool		inherited;
 
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELSCHEMA_ARG);
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELNAME_ARG);
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTNAME_ARG);
-	stats_check_required_arg(fcinfo, cleararginfo, C_INHERITED_ARG);
+	if (!stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELSCHEMA_ARG))
+		PG_RETURN_VOID();
+	if (!stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELNAME_ARG))
+		PG_RETURN_VOID();
+	if (!stats_check_required_arg(fcinfo, cleararginfo, C_ATTNAME_ARG))
+		PG_RETURN_VOID();
+	if (!stats_check_required_arg(fcinfo, cleararginfo, C_INHERITED_ARG))
+		PG_RETURN_VOID();
 
 	nspname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELSCHEMA_ARG));
 	nspoid = stats_schema_check_privileges(nspname);
 	if (!OidIsValid(nspoid))
-		return false;
+		PG_RETURN_VOID();
 
 	relname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELNAME_ARG));
 	reloid = get_relname_relid(relname, nspoid);
@@ -944,31 +982,41 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 		ereport(WARNING,
 				(errcode(ERRCODE_UNDEFINED_OBJECT),
 				 errmsg("Relation \"%s\".\"%s\" not found.", nspname, relname)));
-		return false;
+		PG_RETURN_VOID();
 	}
 
 	if (RecoveryInProgress())
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("recovery is in progress"),
 				 errhint("Statistics cannot be modified during recovery.")));
+		PG_RETURN_VOID();
+	}
 
-	stats_lock_check_privileges(reloid);
+	if (!stats_lock_check_privileges(reloid))
+		PG_RETURN_VOID();
 
 	attname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTNAME_ARG));
 	attnum = get_attnum(reloid, attname);
 
 	if (attnum < 0)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot clear statistics on system column \"%s\"",
 						attname)));
+		PG_RETURN_VOID();
+	}
 
 	if (attnum == InvalidAttrNumber)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 errmsg("column \"%s\" of relation \"%s\" does not exist",
 						attname, get_rel_name(reloid))));
+		PG_RETURN_VOID();
+	}
 
 	inherited = PG_GETARG_BOOL(C_INHERITED_ARG);
 
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index fdc69bc93e2..49109cf721d 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -84,8 +84,11 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 	bool		nulls[4] = {0};
 	int			nreplaces = 0;
 
-	stats_check_required_arg(fcinfo, relarginfo, RELSCHEMA_ARG);
-	stats_check_required_arg(fcinfo, relarginfo, RELNAME_ARG);
+	if (!stats_check_required_arg(fcinfo, relarginfo, RELSCHEMA_ARG))
+		return false;
+
+	if (!stats_check_required_arg(fcinfo, relarginfo, RELNAME_ARG))
+		return false;
 
 	nspname = TextDatumGetCString(PG_GETARG_DATUM(RELSCHEMA_ARG));
 	nspoid = stats_schema_check_privileges(nspname);
@@ -108,7 +111,8 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 				 errmsg("recovery is in progress"),
 				 errhint("Statistics cannot be modified during recovery.")));
 
-	stats_lock_check_privileges(reloid);
+	if (!stats_lock_check_privileges(reloid))
+		return false;
 
 	if (!PG_ARGISNULL(RELPAGES_ARG))
 	{
diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c
index e037d4994e8..dd9d88ac1c5 100644
--- a/src/backend/statistics/stat_utils.c
+++ b/src/backend/statistics/stat_utils.c
@@ -34,16 +34,20 @@
 /*
  * Ensure that a given argument is not null.
  */
-void
+bool
 stats_check_required_arg(FunctionCallInfo fcinfo,
 						 struct StatsArgInfo *arginfo,
 						 int argnum)
 {
 	if (PG_ARGISNULL(argnum))
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("\"%s\" cannot be NULL",
 						arginfo[argnum].argname)));
+		return false;
+	}
+	return true;
 }
 
 /*
@@ -128,13 +132,14 @@ stats_check_arg_pair(FunctionCallInfo fcinfo,
  *   - the role owns the current database and the relation is not shared
  *   - the role has the MAINTAIN privilege on the relation
  */
-void
+bool
 stats_lock_check_privileges(Oid reloid)
 {
 	Relation	table;
 	Oid			table_oid = reloid;
 	Oid			index_oid = InvalidOid;
 	LOCKMODE	index_lockmode = NoLock;
+	bool		ok = true;
 
 	/*
 	 * For indexes, we follow the locking behavior in do_analyze_rel() and
@@ -174,14 +179,15 @@ stats_lock_check_privileges(Oid reloid)
 		case RELKIND_PARTITIONED_TABLE:
 			break;
 		default:
-			ereport(ERROR,
+			ereport(WARNING,
 					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 					 errmsg("cannot modify statistics for relation \"%s\"",
 							RelationGetRelationName(table)),
 					 errdetail_relkind_not_supported(table->rd_rel->relkind)));
+		ok = false;
 	}
 
-	if (OidIsValid(index_oid))
+	if (ok && (OidIsValid(index_oid)))
 	{
 		Relation	index;
 
@@ -194,25 +200,33 @@ stats_lock_check_privileges(Oid reloid)
 		relation_close(index, NoLock);
 	}
 
-	if (table->rd_rel->relisshared)
-		ereport(ERROR,
+	if (ok && (table->rd_rel->relisshared))
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot modify statistics for shared relation")));
+		ok = false;
+	}
 
-	if (!object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId()))
+	if (ok && (!object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId())))
 	{
 		AclResult	aclresult = pg_class_aclcheck(RelationGetRelid(table),
 												  GetUserId(),
 												  ACL_MAINTAIN);
 
 		if (aclresult != ACLCHECK_OK)
-			aclcheck_error(aclresult,
-						   get_relkind_objtype(table->rd_rel->relkind),
-						   NameStr(table->rd_rel->relname));
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+						errmsg("permission denied for relation %s",
+							   NameStr(table->rd_rel->relname))));
+			ok = false;
+		}
 	}
 
 	/* retain lock on table */
 	relation_close(table, NoLock);
+	return ok;
 }
 
 
@@ -318,9 +332,12 @@ stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
 								  &args, &types, &argnulls);
 
 	if (nargs % 2 != 0)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				errmsg("variadic arguments must be name/value pairs"),
 				errhint("Provide an even number of variadic arguments that can be divided into pairs."));
+		return false;
+	}
 
 	/*
 	 * For each argument name/value pair, find corresponding positional
@@ -333,14 +350,20 @@ stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
 		char	   *argname;
 
 		if (argnulls[i])
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 					(errmsg("name at variadic position %d is NULL", i + 1)));
+			return false;
+		}
 
 		if (types[i] != TEXTOID)
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 					(errmsg("name at variadic position %d has type \"%s\", expected type \"%s\"",
 							i + 1, format_type_be(types[i]),
 							format_type_be(TEXTOID))));
+			return false;
+		}
 
 		if (argnulls[i + 1])
 			continue;
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index 2f1295f2149..6551d6bf099 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -46,31 +46,51 @@ SELECT pg_clear_relation_stats('stats_import', 'test');
 --
 -- relstats tests
 --
--- error: schemaname missing
+-- warning: schemaname missing, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'relname', 'test',
         'relpages', 17::integer);
-ERROR:  "schemaname" cannot be NULL
--- error: relname missing
+WARNING:  "schemaname" cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+-- warning: relname missing, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relpages', 17::integer);
-ERROR:  "relname" cannot be NULL
---- error: schemaname is wrong type
+WARNING:  "relname" cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+--- warning: schemaname is wrong type, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 3.6::float,
         'relname', 'test',
         'relpages', 17::integer);
 WARNING:  argument "schemaname" has type "double precision", expected type "text"
-ERROR:  "schemaname" cannot be NULL
---- error: relname is wrong type
+WARNING:  "schemaname" cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+--- warning: relname is wrong type, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 0::oid,
         'relpages', 17::integer);
 WARNING:  argument "relname" has type "oid", expected type "text"
-ERROR:  "relname" cannot be NULL
--- error: relation not found
+WARNING:  "relname" cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+-- warning: relation not found, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'nope',
@@ -81,19 +101,30 @@ WARNING:  Relation "stats_import"."nope" not found.
  f
 (1 row)
 
--- error: odd number of variadic arguments cannot be pairs
+-- warning: odd number of variadic arguments cannot be pairs, nothing updated
 SELECT pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'test',
         'relallvisible');
-ERROR:  variadic arguments must be name/value pairs
+WARNING:  variadic arguments must be name/value pairs
 HINT:  Provide an even number of variadic arguments that can be divided into pairs.
--- error: argument name is NULL
+WARNING:  "schemaname" cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+-- warning: argument name is NULL, nothing updated
 SELECT pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'test',
         NULL, '17'::integer);
-ERROR:  name at variadic position 5 is NULL
+WARNING:  name at variadic position 5 is NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
 -- starting stats
 SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
@@ -345,26 +376,46 @@ CREATE SEQUENCE stats_import.testseq;
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'testseq');
-ERROR:  cannot modify statistics for relation "testseq"
+WARNING:  cannot modify statistics for relation "testseq"
 DETAIL:  This operation is not supported for sequences.
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
 SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testseq');
-ERROR:  cannot modify statistics for relation "testseq"
+WARNING:  cannot modify statistics for relation "testseq"
 DETAIL:  This operation is not supported for sequences.
+ pg_clear_relation_stats 
+-------------------------
+ 
+(1 row)
+
 CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
 SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testview');
-ERROR:  cannot modify statistics for relation "testview"
+WARNING:  cannot modify statistics for relation "testview"
 DETAIL:  This operation is not supported for views.
+ pg_clear_relation_stats 
+-------------------------
+ 
+(1 row)
+
 --
 -- attribute stats
 --
--- error: schemaname missing
+-- warning: schemaname missing, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relname', 'test',
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  "schemaname" cannot be NULL
--- error: schema does not exist
+WARNING:  "schemaname" cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: schema does not exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'nope',
     'relname', 'test',
@@ -377,14 +428,19 @@ WARNING:  schema nope does not exist
  f
 (1 row)
 
--- error: relname missing
+-- warning: relname missing, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  "relname" cannot be NULL
--- error: relname does not exist
+WARNING:  "relname" cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: relname does not exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'nope',
@@ -397,23 +453,33 @@ WARNING:  Relation "stats_import"."nope" not found.
  f
 (1 row)
 
--- error: relname null
+-- warning: relname null, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', NULL,
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  "relname" cannot be NULL
--- error: NULL attname
+WARNING:  "relname" cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: NULL attname, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
     'attname', NULL,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  must specify either attname or attnum
--- error: attname doesn't exist
+WARNING:  must specify either attname or attnum
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: attname doesn't exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -422,8 +488,13 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'null_frac', 0.1::real,
     'avg_width', 2::integer,
     'n_distinct', 0.3::real);
-ERROR:  column "nope" of relation "stats_import"."test" does not exist
--- error: both attname and attnum
+WARNING:  column "nope" of relation "stats_import"."test" does not exist
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: both attname and attnum, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -431,30 +502,50 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'attnum', 1::smallint,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  cannot specify both attname and attnum
--- error: neither attname nor attnum
+WARNING:  cannot specify both attname and attnum
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: neither attname nor attnum, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  must specify either attname or attnum
--- error: attribute is system column
+WARNING:  must specify either attname or attnum
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: attribute is system column, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
     'attname', 'xmin',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  cannot modify statistics on system column "xmin"
--- error: inherited null
+WARNING:  cannot modify statistics on system column "xmin"
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: inherited null, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
     'attname', 'id',
     'inherited', NULL::boolean,
     'null_frac', 0.1::real);
-ERROR:  "inherited" cannot be NULL
+WARNING:  "inherited" cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
 -- ok: just the fixed values, with version, no stakinds
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index ccdc44e9236..dbbebce1673 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -39,41 +39,41 @@ SELECT pg_clear_relation_stats('stats_import', 'test');
 -- relstats tests
 --
 
--- error: schemaname missing
+-- warning: schemaname missing, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'relname', 'test',
         'relpages', 17::integer);
 
--- error: relname missing
+-- warning: relname missing, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relpages', 17::integer);
 
---- error: schemaname is wrong type
+--- warning: schemaname is wrong type, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 3.6::float,
         'relname', 'test',
         'relpages', 17::integer);
 
---- error: relname is wrong type
+--- warning: relname is wrong type, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 0::oid,
         'relpages', 17::integer);
 
--- error: relation not found
+-- warning: relation not found, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'nope',
         'relpages', 17::integer);
 
--- error: odd number of variadic arguments cannot be pairs
+-- warning: odd number of variadic arguments cannot be pairs, nothing updated
 SELECT pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'test',
         'relallvisible');
 
--- error: argument name is NULL
+-- warning: argument name is NULL, nothing updated
 SELECT pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'test',
@@ -246,14 +246,14 @@ SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname
 -- attribute stats
 --
 
--- error: schemaname missing
+-- warning: schemaname missing, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relname', 'test',
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: schema does not exist
+-- warning: schema does not exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'nope',
     'relname', 'test',
@@ -261,14 +261,14 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: relname missing
+-- warning: relname missing, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: relname does not exist
+-- warning: relname does not exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'nope',
@@ -276,7 +276,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: relname null
+-- warning: relname null, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', NULL,
@@ -284,7 +284,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: NULL attname
+-- warning: NULL attname, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -292,7 +292,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: attname doesn't exist
+-- warning: attname doesn't exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -302,7 +302,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'avg_width', 2::integer,
     'n_distinct', 0.3::real);
 
--- error: both attname and attnum
+-- warning: both attname and attnum, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -311,14 +311,14 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: neither attname nor attnum
+-- warning: neither attname nor attnum, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: attribute is system column
+-- warning: attribute is system column, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -326,7 +326,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: inherited null
+-- warning: inherited null, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
-- 
2.48.1

v8-0003-Introduce-CreateStmtPtr.patchtext/x-patch; charset=US-ASCII; name=v8-0003-Introduce-CreateStmtPtr.patchDownload
From e2b00e46afdfa45f263b4666831160bf4d21dd09 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Fri, 14 Mar 2025 01:06:19 -0400
Subject: [PATCH v8 3/4] Introduce CreateStmtPtr.

CreateStmtPtr is a function pointer that can replace the createStmt/defn
parameter. This is useful in situations where the amount of text
generated for a definition is so large that it is undesirable to hold
many such objects in memory at the same time.

Using functions of this type, the text created is then immediately
written out to the appropriate file for the given dump format.
---
 src/bin/pg_dump/pg_backup.h          |   2 +
 src/bin/pg_dump/pg_backup_archiver.c |  22 ++-
 src/bin/pg_dump/pg_backup_archiver.h |   7 +
 src/bin/pg_dump/pg_dump.c            | 230 +++++++++++++++------------
 4 files changed, 156 insertions(+), 105 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index e783cc68d89..bb175874a5a 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -287,6 +287,8 @@ typedef int (*DataDumperPtr) (Archive *AH, const void *userArg);
 
 typedef void (*SetupWorkerPtrType) (Archive *AH);
 
+typedef char *(*CreateStmtPtr) (Archive *AH, const void *userArg);
+
 /*
  * Main archiver interface.
  */
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 7480e122b61..3fcfecf6719 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -1263,6 +1263,9 @@ ArchiveEntry(Archive *AHX, CatalogId catalogId, DumpId dumpId,
 	newToc->dataDumper = opts->dumpFn;
 	newToc->dataDumperArg = opts->dumpArg;
 	newToc->hadDumper = opts->dumpFn ? true : false;
+	newToc->createDumper = opts->createFn;
+	newToc->createDumperArg = opts->createArg;
+	newToc->hadCreateDumper = opts->createFn ? true : false;
 
 	newToc->formatData = NULL;
 	newToc->dataLength = 0;
@@ -2619,7 +2622,17 @@ WriteToc(ArchiveHandle *AH)
 		WriteStr(AH, te->tag);
 		WriteStr(AH, te->desc);
 		WriteInt(AH, te->section);
-		WriteStr(AH, te->defn);
+
+		if (te->hadCreateDumper)
+		{
+			char	   *defn = te->createDumper((Archive *) AH, te->createDumperArg);
+
+			WriteStr(AH, defn);
+			pg_free(defn);
+		}
+		else
+			WriteStr(AH, te->defn);
+
 		WriteStr(AH, te->dropStmt);
 		WriteStr(AH, te->copyStmt);
 		WriteStr(AH, te->namespace);
@@ -3849,6 +3862,13 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 	{
 		IssueACLPerBlob(AH, te);
 	}
+	else if (te->hadCreateDumper)
+	{
+		char	   *ptr = te->createDumper((Archive *) AH, te->createDumperArg);
+
+		ahwrite(ptr, 1, strlen(ptr), AH);
+		pg_free(ptr);
+	}
 	else if (te->defn && strlen(te->defn) > 0)
 	{
 		ahprintf(AH, "%s\n\n", te->defn);
diff --git a/src/bin/pg_dump/pg_backup_archiver.h b/src/bin/pg_dump/pg_backup_archiver.h
index a2064f471ed..e68db633995 100644
--- a/src/bin/pg_dump/pg_backup_archiver.h
+++ b/src/bin/pg_dump/pg_backup_archiver.h
@@ -368,6 +368,11 @@ struct _tocEntry
 	const void *dataDumperArg;	/* Arg for above routine */
 	void	   *formatData;		/* TOC Entry data specific to file format */
 
+	CreateStmtPtr createDumper; /* Routine for create statement creation */
+	const void *createDumperArg;	/* arg for the above routine */
+	bool		hadCreateDumper;	/* Archiver was passed a create statement
+									 * routine */
+
 	/* working state while dumping/restoring */
 	pgoff_t		dataLength;		/* item's data size; 0 if none or unknown */
 	int			reqs;			/* do we need schema and/or data of object
@@ -407,6 +412,8 @@ typedef struct _archiveOpts
 	int			nDeps;
 	DataDumperPtr dumpFn;
 	const void *dumpArg;
+	CreateStmtPtr createFn;
+	const void *createArg;
 } ArchiveOpts;
 #define ARCHIVE_OPTS(...) &(ArchiveOpts){__VA_ARGS__}
 /* Called to add a TOC entry */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index bd857bb076c..38ba6a90106 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -10477,51 +10477,44 @@ statisticsDumpSection(const RelStatsInfo *rsinfo)
 }
 
 /*
- * dumpRelationStats --
+ * printDumpRelationStats --
  *
- * Dump command to import stats into the relation on the new database.
+ * Generate the SQL statements needed to restore a relation's statistics.
  */
-static void
-dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+static char *
+printRelationStats(Archive *fout, const void *userArg)
 {
+	const RelStatsInfo *rsinfo = (RelStatsInfo *) userArg;
 	const DumpableObject *dobj = &rsinfo->dobj;
+
+	PQExpBufferData query;
+	PQExpBufferData out;
+
 	PGresult   *res;
-	PQExpBuffer query;
-	PQExpBuffer out;
-	DumpId	   *deps = NULL;
-	int			ndeps = 0;
-	int			i_attname;
-	int			i_inherited;
-	int			i_null_frac;
-	int			i_avg_width;
-	int			i_n_distinct;
-	int			i_most_common_vals;
-	int			i_most_common_freqs;
-	int			i_histogram_bounds;
-	int			i_correlation;
-	int			i_most_common_elems;
-	int			i_most_common_elem_freqs;
-	int			i_elem_count_histogram;
-	int			i_range_length_histogram;
-	int			i_range_empty_frac;
-	int			i_range_bounds_histogram;
 
-	/* nothing to do if we are not dumping statistics */
-	if (!fout->dopt->dumpStatistics)
-		return;
+	static bool first_query = true;
+	static int	i_attname;
+	static int	i_inherited;
+	static int	i_null_frac;
+	static int	i_avg_width;
+	static int	i_n_distinct;
+	static int	i_most_common_vals;
+	static int	i_most_common_freqs;
+	static int	i_histogram_bounds;
+	static int	i_correlation;
+	static int	i_most_common_elems;
+	static int	i_most_common_elem_freqs;
+	static int	i_elem_count_histogram;
+	static int	i_range_length_histogram;
+	static int	i_range_empty_frac;
+	static int	i_range_bounds_histogram;
 
-	/* dependent on the relation definition, if doing schema */
-	if (fout->dopt->dumpSchema)
+	initPQExpBuffer(&query);
+
+	if (first_query)
 	{
-		deps = dobj->dependencies;
-		ndeps = dobj->nDeps;
-	}
-
-	query = createPQExpBuffer();
-	if (!fout->is_prepared[PREPQUERY_GETATTRIBUTESTATS])
-	{
-		appendPQExpBufferStr(query,
-							 "PREPARE getAttributeStats(pg_catalog.name, pg_catalog.name) AS\n"
+		appendPQExpBufferStr(&query,
+							 "PREPARE getAttributeStats(pg_catalog.text, pg_catalog.text) AS\n"
 							 "SELECT s.attname, s.inherited, "
 							 "s.null_frac, s.avg_width, s.n_distinct, "
 							 "s.most_common_vals, s.most_common_freqs, "
@@ -10530,82 +10523,85 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 							 "s.elem_count_histogram, ");
 
 		if (fout->remoteVersion >= 170000)
-			appendPQExpBufferStr(query,
+			appendPQExpBufferStr(&query,
 								 "s.range_length_histogram, "
 								 "s.range_empty_frac, "
 								 "s.range_bounds_histogram ");
 		else
-			appendPQExpBufferStr(query,
+			appendPQExpBufferStr(&query,
 								 "NULL AS range_length_histogram,"
 								 "NULL AS range_empty_frac,"
 								 "NULL AS range_bounds_histogram ");
 
-		appendPQExpBufferStr(query,
+		appendPQExpBufferStr(&query,
 							 "FROM pg_catalog.pg_stats s "
 							 "WHERE s.schemaname = $1 "
 							 "AND s.tablename = $2 "
 							 "ORDER BY s.attname, s.inherited");
 
-		ExecuteSqlStatement(fout, query->data);
+		ExecuteSqlStatement(fout, query.data);
 
-		fout->is_prepared[PREPQUERY_GETATTRIBUTESTATS] = true;
-		resetPQExpBuffer(query);
+		resetPQExpBuffer(&query);
 	}
 
-	out = createPQExpBuffer();
+	initPQExpBuffer(&out);
 
 	/* restore relation stats */
-	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
-	appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
+	appendPQExpBufferStr(&out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
+	appendPQExpBuffer(&out, "\t'version', '%u'::integer,\n",
 					  fout->remoteVersion);
-	appendPQExpBufferStr(out, "\t'schemaname', ");
-	appendStringLiteralAH(out, rsinfo->dobj.namespace->dobj.name, fout);
-	appendPQExpBufferStr(out, ",\n");
-	appendPQExpBufferStr(out, "\t'relname', ");
-	appendStringLiteralAH(out, rsinfo->dobj.name, fout);
-	appendPQExpBufferStr(out, ",\n");
-	appendPQExpBuffer(out, "\t'relpages', '%d'::integer,\n", rsinfo->relpages);
-	appendPQExpBuffer(out, "\t'reltuples', '%s'::real,\n", rsinfo->reltuples);
-	appendPQExpBuffer(out, "\t'relallvisible', '%d'::integer\n);\n",
+	appendPQExpBufferStr(&out, "\t'schemaname', ");
+	appendStringLiteralAH(&out, rsinfo->dobj.namespace->dobj.name, fout);
+	appendPQExpBufferStr(&out, ",\n");
+	appendPQExpBufferStr(&out, "\t'relname', ");
+	appendStringLiteralAH(&out, rsinfo->dobj.name, fout);
+	appendPQExpBufferStr(&out, ",\n");
+	appendPQExpBuffer(&out, "\t'relpages', '%d'::integer,\n", rsinfo->relpages);
+	appendPQExpBuffer(&out, "\t'reltuples', '%s'::real,\n", rsinfo->reltuples);
+	appendPQExpBuffer(&out, "\t'relallvisible', '%d'::integer\n);\n",
 					  rsinfo->relallvisible);
 
 	/* fetch attribute stats */
-	appendPQExpBufferStr(query, "EXECUTE getAttributeStats(");
-	appendStringLiteralAH(query, dobj->namespace->dobj.name, fout);
-	appendPQExpBufferStr(query, ", ");
-	appendStringLiteralAH(query, dobj->name, fout);
-	appendPQExpBufferStr(query, ");");
+	appendPQExpBufferStr(&query, "EXECUTE getAttributeStats(");
+	appendStringLiteralAH(&query, dobj->namespace->dobj.name, fout);
+	appendPQExpBufferStr(&query, ", ");
+	appendStringLiteralAH(&query, dobj->name, fout);
+	appendPQExpBufferStr(&query, ")");
 
-	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	res = ExecuteSqlQuery(fout, query.data, PGRES_TUPLES_OK);
 
-	i_attname = PQfnumber(res, "attname");
-	i_inherited = PQfnumber(res, "inherited");
-	i_null_frac = PQfnumber(res, "null_frac");
-	i_avg_width = PQfnumber(res, "avg_width");
-	i_n_distinct = PQfnumber(res, "n_distinct");
-	i_most_common_vals = PQfnumber(res, "most_common_vals");
-	i_most_common_freqs = PQfnumber(res, "most_common_freqs");
-	i_histogram_bounds = PQfnumber(res, "histogram_bounds");
-	i_correlation = PQfnumber(res, "correlation");
-	i_most_common_elems = PQfnumber(res, "most_common_elems");
-	i_most_common_elem_freqs = PQfnumber(res, "most_common_elem_freqs");
-	i_elem_count_histogram = PQfnumber(res, "elem_count_histogram");
-	i_range_length_histogram = PQfnumber(res, "range_length_histogram");
-	i_range_empty_frac = PQfnumber(res, "range_empty_frac");
-	i_range_bounds_histogram = PQfnumber(res, "range_bounds_histogram");
+	if (first_query)
+	{
+		i_attname = PQfnumber(res, "attname");
+		i_inherited = PQfnumber(res, "inherited");
+		i_null_frac = PQfnumber(res, "null_frac");
+		i_avg_width = PQfnumber(res, "avg_width");
+		i_n_distinct = PQfnumber(res, "n_distinct");
+		i_most_common_vals = PQfnumber(res, "most_common_vals");
+		i_most_common_freqs = PQfnumber(res, "most_common_freqs");
+		i_histogram_bounds = PQfnumber(res, "histogram_bounds");
+		i_correlation = PQfnumber(res, "correlation");
+		i_most_common_elems = PQfnumber(res, "most_common_elems");
+		i_most_common_elem_freqs = PQfnumber(res, "most_common_elem_freqs");
+		i_elem_count_histogram = PQfnumber(res, "elem_count_histogram");
+		i_range_length_histogram = PQfnumber(res, "range_length_histogram");
+		i_range_empty_frac = PQfnumber(res, "range_empty_frac");
+		i_range_bounds_histogram = PQfnumber(res, "range_bounds_histogram");
+		first_query = false;
+	}
 
 	/* restore attribute stats */
 	for (int rownum = 0; rownum < PQntuples(res); rownum++)
 	{
 		const char *attname;
 
-		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
-		appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
+		appendPQExpBufferStr(&out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+		appendPQExpBuffer(&out, "\t'version', '%u'::integer,\n",
 						  fout->remoteVersion);
-		appendPQExpBufferStr(out, "\t'schemaname', ");
-		appendStringLiteralAH(out, rsinfo->dobj.namespace->dobj.name, fout);
-		appendPQExpBufferStr(out, ",\n\t'relname', ");
-		appendStringLiteralAH(out, rsinfo->dobj.name, fout);
+		appendPQExpBufferStr(&out, "\t'schemaname', ");
+		appendStringLiteralAH(&out, rsinfo->dobj.namespace->dobj.name, fout);
+		appendPQExpBufferStr(&out, ",\n\t'relname', ");
+		appendStringLiteralAH(&out, rsinfo->dobj.name, fout);
 
 		if (PQgetisnull(res, rownum, i_attname))
 			pg_fatal("attname cannot be NULL");
@@ -10618,8 +10614,8 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 		 */
 		if (rsinfo->nindAttNames == 0)
 		{
-			appendPQExpBuffer(out, ",\n\t'attname', ");
-			appendStringLiteralAH(out, attname, fout);
+			appendPQExpBuffer(&out, ",\n\t'attname', ");
+			appendStringLiteralAH(&out, attname, fout);
 		}
 		else
 		{
@@ -10629,7 +10625,7 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 			{
 				if (strcmp(attname, rsinfo->indAttNames[i]) == 0)
 				{
-					appendPQExpBuffer(out, ",\n\t'attnum', '%d'::smallint",
+					appendPQExpBuffer(&out, ",\n\t'attnum', '%d'::smallint",
 									  i + 1);
 					found = true;
 					break;
@@ -10641,67 +10637,93 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 		}
 
 		if (!PQgetisnull(res, rownum, i_inherited))
-			appendNamedArgument(out, fout, "inherited", "boolean",
+			appendNamedArgument(&out, fout, "inherited", "boolean",
 								PQgetvalue(res, rownum, i_inherited));
 		if (!PQgetisnull(res, rownum, i_null_frac))
-			appendNamedArgument(out, fout, "null_frac", "real",
+			appendNamedArgument(&out, fout, "null_frac", "real",
 								PQgetvalue(res, rownum, i_null_frac));
 		if (!PQgetisnull(res, rownum, i_avg_width))
-			appendNamedArgument(out, fout, "avg_width", "integer",
+			appendNamedArgument(&out, fout, "avg_width", "integer",
 								PQgetvalue(res, rownum, i_avg_width));
 		if (!PQgetisnull(res, rownum, i_n_distinct))
-			appendNamedArgument(out, fout, "n_distinct", "real",
+			appendNamedArgument(&out, fout, "n_distinct", "real",
 								PQgetvalue(res, rownum, i_n_distinct));
 		if (!PQgetisnull(res, rownum, i_most_common_vals))
-			appendNamedArgument(out, fout, "most_common_vals", "text",
+			appendNamedArgument(&out, fout, "most_common_vals", "text",
 								PQgetvalue(res, rownum, i_most_common_vals));
 		if (!PQgetisnull(res, rownum, i_most_common_freqs))
-			appendNamedArgument(out, fout, "most_common_freqs", "real[]",
+			appendNamedArgument(&out, fout, "most_common_freqs", "real[]",
 								PQgetvalue(res, rownum, i_most_common_freqs));
 		if (!PQgetisnull(res, rownum, i_histogram_bounds))
-			appendNamedArgument(out, fout, "histogram_bounds", "text",
+			appendNamedArgument(&out, fout, "histogram_bounds", "text",
 								PQgetvalue(res, rownum, i_histogram_bounds));
 		if (!PQgetisnull(res, rownum, i_correlation))
-			appendNamedArgument(out, fout, "correlation", "real",
+			appendNamedArgument(&out, fout, "correlation", "real",
 								PQgetvalue(res, rownum, i_correlation));
 		if (!PQgetisnull(res, rownum, i_most_common_elems))
-			appendNamedArgument(out, fout, "most_common_elems", "text",
+			appendNamedArgument(&out, fout, "most_common_elems", "text",
 								PQgetvalue(res, rownum, i_most_common_elems));
 		if (!PQgetisnull(res, rownum, i_most_common_elem_freqs))
-			appendNamedArgument(out, fout, "most_common_elem_freqs", "real[]",
+			appendNamedArgument(&out, fout, "most_common_elem_freqs", "real[]",
 								PQgetvalue(res, rownum, i_most_common_elem_freqs));
 		if (!PQgetisnull(res, rownum, i_elem_count_histogram))
-			appendNamedArgument(out, fout, "elem_count_histogram", "real[]",
+			appendNamedArgument(&out, fout, "elem_count_histogram", "real[]",
 								PQgetvalue(res, rownum, i_elem_count_histogram));
 		if (fout->remoteVersion >= 170000)
 		{
 			if (!PQgetisnull(res, rownum, i_range_length_histogram))
-				appendNamedArgument(out, fout, "range_length_histogram", "text",
+				appendNamedArgument(&out, fout, "range_length_histogram", "text",
 									PQgetvalue(res, rownum, i_range_length_histogram));
 			if (!PQgetisnull(res, rownum, i_range_empty_frac))
-				appendNamedArgument(out, fout, "range_empty_frac", "real",
+				appendNamedArgument(&out, fout, "range_empty_frac", "real",
 									PQgetvalue(res, rownum, i_range_empty_frac));
 			if (!PQgetisnull(res, rownum, i_range_bounds_histogram))
-				appendNamedArgument(out, fout, "range_bounds_histogram", "text",
+				appendNamedArgument(&out, fout, "range_bounds_histogram", "text",
 									PQgetvalue(res, rownum, i_range_bounds_histogram));
 		}
-		appendPQExpBufferStr(out, "\n);\n");
+		appendPQExpBufferStr(&out, "\n);\n");
 	}
 
 	PQclear(res);
 
+	termPQExpBuffer(&query);
+	return out.data;
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	const DumpableObject *dobj = &rsinfo->dobj;
+
+	DumpId	   *deps = NULL;
+	int			ndeps = 0;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
+	/* dependent on the relation definition, if doing schema */
+	if (fout->dopt->dumpSchema)
+	{
+		deps = dobj->dependencies;
+		ndeps = dobj->nDeps;
+	}
+
 	ArchiveEntry(fout, nilCatalogId, createDumpId(),
 				 ARCHIVE_OPTS(.tag = dobj->name,
 							  .namespace = dobj->namespace->dobj.name,
 							  .description = "STATISTICS DATA",
 							  .section = rsinfo->postponed_def ?
 							  SECTION_POST_DATA : statisticsDumpSection(rsinfo),
-							  .createStmt = out->data,
+							  .createFn = printRelationStats,
+							  .createArg = rsinfo,
 							  .deps = deps,
 							  .nDeps = ndeps));
-
-	destroyPQExpBuffer(out);
-	destroyPQExpBuffer(query);
 }
 
 /*
-- 
2.48.1

v8-0004-Batching-getAttributeStats.patchtext/x-patch; charset=US-ASCII; name=v8-0004-Batching-getAttributeStats.patchDownload
From 737a29c9b8f146eb037630ab183eb8601a76409a Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Fri, 14 Mar 2025 03:54:26 -0400
Subject: [PATCH v8 4/4] Batching getAttributeStats().

The prepared statement getAttributeStats() is fairly heavyweight and
could greatly increase pg_dump/pg_upgrade runtime. To alleviate this,
create a result set buffer of all of the attribute stats fetched for a
batch of 100 relations that could potentially have stats.

The query ensures that the order of results exactly matches the needs of
the code walking the TOC to print the stats calls.
---
 src/bin/pg_dump/pg_dump.c | 556 ++++++++++++++++++++++++++------------
 1 file changed, 385 insertions(+), 171 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 38ba6a90106..0c26dc7a1b4 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -143,6 +143,25 @@ typedef enum OidOptions
 	zeroAsNone = 4,
 } OidOptions;
 
+typedef enum StatsBufferState
+{
+	STATSBUF_UNINITIALIZED = 0,
+	STATSBUF_ACTIVE,
+	STATSBUF_EXHAUSTED
+}			StatsBufferState;
+
+typedef struct
+{
+	PGresult   *res;			/* results from most recent
+								 * getAttributeStats() */
+	int			idx;			/* first un-consumed row of results */
+	TocEntry   *te;				/* next TOC entry to search for statsitics
+								 * data */
+
+	StatsBufferState state;		/* current state of the buffer */
+}			AttributeStatsBuffer;
+
+
 /* global decls */
 static bool dosync = true;		/* Issue fsync() to make dump durable on disk. */
 
@@ -209,6 +228,18 @@ static int	nbinaryUpgradeClassOids = 0;
 static SequenceItem *sequences = NULL;
 static int	nsequences = 0;
 
+static AttributeStatsBuffer attrstats =
+{
+	NULL, 0, NULL, STATSBUF_UNINITIALIZED
+};
+
+/*
+ * The maximum number of relations that should be fetched in any one
+ * getAttributeStats() call.
+ */
+
+#define MAX_ATTR_STATS_RELS 100
+
 /*
  * The default number of rows per INSERT when
  * --inserts is specified without --rows-per-insert
@@ -222,6 +253,10 @@ static int	nsequences = 0;
  */
 #define MAX_BLOBS_PER_ARCHIVE_ENTRY 1000
 
+
+
+/* TODO: fmtId(const char *rawid) */
+
 /*
  * Macro for producing quoted, schema-qualified name of a dumpable object.
  */
@@ -399,6 +434,9 @@ static void setupDumpWorker(Archive *AH);
 static TableInfo *getRootTableInfo(const TableInfo *tbinfo);
 static bool forcePartitionRootLoad(const TableInfo *tbinfo);
 static void read_dump_filters(const char *filename, DumpOptions *dopt);
+static void appendNamedArgument(PQExpBuffer out, Archive *fout,
+								const char *argname, const char *argtype,
+								const char *argval);
 
 
 int
@@ -10477,7 +10515,286 @@ statisticsDumpSection(const RelStatsInfo *rsinfo)
 }
 
 /*
- * printDumpRelationStats --
+ * Fetch next batch of rows from getAttributeStats()
+ */
+static void
+fetchNextAttributeStats(Archive *fout)
+{
+	ArchiveHandle *AH = (ArchiveHandle *) fout;
+	PQExpBufferData schemas;
+	PQExpBufferData relations;
+	int			numoids = 0;
+
+	Assert(AH != NULL);
+
+	/* free last result set, if any */
+	if (attrstats.state == STATSBUF_ACTIVE)
+		PQclear(attrstats.res);
+
+	/* If we have looped around to the start of the TOC, restart */
+	if (attrstats.te == AH->toc)
+		attrstats.te = AH->toc->next;
+
+	initPQExpBuffer(&schemas);
+	initPQExpBuffer(&relations);
+
+	/*
+	 * Walk ahead looking for relstats entries that are active in this
+	 * section, adding the names to the schemas and relations lists.
+	 */
+	while ((attrstats.te != AH->toc) && (numoids < MAX_ATTR_STATS_RELS))
+	{
+		if (attrstats.te->reqs != 0 &&
+			strcmp(attrstats.te->desc, "STATISTICS DATA") == 0)
+		{
+			RelStatsInfo *rsinfo = (RelStatsInfo *) attrstats.te->createDumperArg;
+
+			Assert(rsinfo != NULL);
+
+			if (numoids > 0)
+			{
+				appendPQExpBufferStr(&schemas, ",");
+				appendPQExpBufferStr(&relations, ",");
+			}
+			appendPQExpBufferStr(&schemas, fmtId(rsinfo->dobj.namespace->dobj.name));
+			appendPQExpBufferStr(&relations, fmtId(rsinfo->dobj.name));
+			numoids++;
+		}
+
+		attrstats.te = attrstats.te->next;
+	}
+
+	if (numoids > 0)
+	{
+		PQExpBufferData query;
+
+		initPQExpBuffer(&query);
+		appendPQExpBuffer(&query,
+						  "EXECUTE getAttributeStats('{%s}'::pg_catalog.text[],'{%s}'::pg_catalog.text[])",
+						  schemas.data, relations.data);
+		attrstats.res = ExecuteSqlQuery(fout, query.data, PGRES_TUPLES_OK);
+		attrstats.idx = 0;
+	}
+	else
+	{
+		attrstats.state = STATSBUF_EXHAUSTED;
+		attrstats.res = NULL;
+		attrstats.idx = -1;
+	}
+
+	termPQExpBuffer(&schemas);
+	termPQExpBuffer(&relations);
+}
+
+/*
+ * Prepare the getAttributeStats() statement
+ *
+ * This is done automatically if the user specified dumpStatistics.
+ */
+static void
+initAttributeStats(Archive *fout)
+{
+	ArchiveHandle *AH = (ArchiveHandle *) fout;
+	PQExpBufferData query;
+
+	Assert(AH != NULL);
+	initPQExpBuffer(&query);
+
+	appendPQExpBufferStr(&query,
+						 "PREPARE getAttributeStats(pg_catalog.text[], pg_catalog.text[]) AS\n"
+						 "SELECT s.schemaname, s.tablename, s.attname, s.inherited, "
+						 "s.null_frac, s.avg_width, s.n_distinct, s.most_common_vals, "
+						 "s.most_common_freqs, s.histogram_bounds, s.correlation, "
+						 "s.most_common_elems, s.most_common_elem_freqs, "
+						 "s.elem_count_histogram, ");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(&query,
+							 "s.range_length_histogram, "
+							 "s.range_empty_frac, "
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(&query,
+							 "NULL AS range_length_histogram, "
+							 "NULL AS range_empty_frac, "
+							 " NULL AS range_bounds_histogram ");
+
+	/*
+	 * The results must be in the order of relations supplied in the
+	 * parameters to ensure that they are in sync with a walk of the TOC.
+	 *
+	 * The redundant (and incomplete) filter clause on s.tablename = ANY(...)
+	 * is a way to lead the query into using the index
+	 * pg_class_relname_nsp_index which in turn allows the planner to avoid an
+	 * expensive full scan of pg_stats.
+	 *
+	 * We may need to adjust this query for versions that are not so easily
+	 * led.
+	 */
+	appendPQExpBufferStr(&query,
+						 "FROM pg_catalog.pg_stats AS s "
+						 "JOIN unnest($1, $2) WITH ORDINALITY AS u(schemaname, tablename, ord) "
+						 "ON s.schemaname = u.schemaname "
+						 "AND s.tablename = u.tablename "
+						 "WHERE s.tablename = ANY($2) "
+						 "ORDER BY u.ord, s.attname, s.inherited");
+
+	ExecuteSqlStatement(fout, query.data);
+
+	termPQExpBuffer(&query);
+
+	attrstats.te = AH->toc->next;
+
+	fetchNextAttributeStats(fout);
+
+	attrstats.state = STATSBUF_ACTIVE;
+}
+
+
+/*
+ * append a single attribute stat to the buffer for this relation.
+ */
+static void
+appendAttributeStats(Archive *fout, PQExpBuffer out,
+					 const RelStatsInfo *rsinfo)
+{
+	PGresult   *res = attrstats.res;
+	int			tup_num = attrstats.idx;
+
+	const char *attname;
+
+	static bool indexes_set = false;
+	static int	i_attname,
+				i_inherited,
+				i_null_frac,
+				i_avg_width,
+				i_n_distinct,
+				i_most_common_vals,
+				i_most_common_freqs,
+				i_histogram_bounds,
+				i_correlation,
+				i_most_common_elems,
+				i_most_common_elem_freqs,
+				i_elem_count_histogram,
+				i_range_length_histogram,
+				i_range_empty_frac,
+				i_range_bounds_histogram;
+
+	if (!indexes_set)
+	{
+		/*
+		 * It's a prepared statement, so the indexes will be the same for all
+		 * result sets, so we only need to set them once.
+		 */
+		i_attname = PQfnumber(res, "attname");
+		i_inherited = PQfnumber(res, "inherited");
+		i_null_frac = PQfnumber(res, "null_frac");
+		i_avg_width = PQfnumber(res, "avg_width");
+		i_n_distinct = PQfnumber(res, "n_distinct");
+		i_most_common_vals = PQfnumber(res, "most_common_vals");
+		i_most_common_freqs = PQfnumber(res, "most_common_freqs");
+		i_histogram_bounds = PQfnumber(res, "histogram_bounds");
+		i_correlation = PQfnumber(res, "correlation");
+		i_most_common_elems = PQfnumber(res, "most_common_elems");
+		i_most_common_elem_freqs = PQfnumber(res, "most_common_elem_freqs");
+		i_elem_count_histogram = PQfnumber(res, "elem_count_histogram");
+		i_range_length_histogram = PQfnumber(res, "range_length_histogram");
+		i_range_empty_frac = PQfnumber(res, "range_empty_frac");
+		i_range_bounds_histogram = PQfnumber(res, "range_bounds_histogram");
+		indexes_set = true;
+	}
+
+	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+	appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
+					  fout->remoteVersion);
+	appendPQExpBufferStr(out, "\t'schemaname', ");
+	appendStringLiteralAH(out, rsinfo->dobj.namespace->dobj.name, fout);
+	appendPQExpBufferStr(out, ",\n\t'relname', ");
+	appendStringLiteralAH(out, rsinfo->dobj.name, fout);
+
+	if (PQgetisnull(res, tup_num, i_attname))
+		pg_fatal("attname cannot be NULL");
+	attname = PQgetvalue(res, tup_num, i_attname);
+
+	/*
+	 * Indexes look up attname in indAttNames to derive attnum, all others use
+	 * attname directly.  We must specify attnum for indexes, since their
+	 * attnames are not necessarily stable across dump/reload.
+	 */
+	if (rsinfo->nindAttNames == 0)
+	{
+		appendPQExpBuffer(out, ",\n\t'attname', ");
+		appendStringLiteralAH(out, attname, fout);
+	}
+	else
+	{
+		bool		found = false;
+
+		for (int i = 0; i < rsinfo->nindAttNames; i++)
+			if (strcmp(attname, rsinfo->indAttNames[i]) == 0)
+			{
+				appendPQExpBuffer(out, ",\n\t'attnum', '%d'::smallint",
+								  i + 1);
+				found = true;
+				break;
+			}
+
+		if (!found)
+			pg_fatal("could not find index attname \"%s\"", attname);
+	}
+
+	if (!PQgetisnull(res, tup_num, i_inherited))
+		appendNamedArgument(out, fout, "inherited", "boolean",
+							PQgetvalue(res, tup_num, i_inherited));
+	if (!PQgetisnull(res, tup_num, i_null_frac))
+		appendNamedArgument(out, fout, "null_frac", "real",
+							PQgetvalue(res, tup_num, i_null_frac));
+	if (!PQgetisnull(res, tup_num, i_avg_width))
+		appendNamedArgument(out, fout, "avg_width", "integer",
+							PQgetvalue(res, tup_num, i_avg_width));
+	if (!PQgetisnull(res, tup_num, i_n_distinct))
+		appendNamedArgument(out, fout, "n_distinct", "real",
+							PQgetvalue(res, tup_num, i_n_distinct));
+	if (!PQgetisnull(res, tup_num, i_most_common_vals))
+		appendNamedArgument(out, fout, "most_common_vals", "text",
+							PQgetvalue(res, tup_num, i_most_common_vals));
+	if (!PQgetisnull(res, tup_num, i_most_common_freqs))
+		appendNamedArgument(out, fout, "most_common_freqs", "real[]",
+							PQgetvalue(res, tup_num, i_most_common_freqs));
+	if (!PQgetisnull(res, tup_num, i_histogram_bounds))
+		appendNamedArgument(out, fout, "histogram_bounds", "text",
+							PQgetvalue(res, tup_num, i_histogram_bounds));
+	if (!PQgetisnull(res, tup_num, i_correlation))
+		appendNamedArgument(out, fout, "correlation", "real",
+							PQgetvalue(res, tup_num, i_correlation));
+	if (!PQgetisnull(res, tup_num, i_most_common_elems))
+		appendNamedArgument(out, fout, "most_common_elems", "text",
+							PQgetvalue(res, tup_num, i_most_common_elems));
+	if (!PQgetisnull(res, tup_num, i_most_common_elem_freqs))
+		appendNamedArgument(out, fout, "most_common_elem_freqs", "real[]",
+							PQgetvalue(res, tup_num, i_most_common_elem_freqs));
+	if (!PQgetisnull(res, tup_num, i_elem_count_histogram))
+		appendNamedArgument(out, fout, "elem_count_histogram", "real[]",
+							PQgetvalue(res, tup_num, i_elem_count_histogram));
+	if (fout->remoteVersion >= 170000)
+	{
+		if (!PQgetisnull(res, tup_num, i_range_length_histogram))
+			appendNamedArgument(out, fout, "range_length_histogram", "text",
+								PQgetvalue(res, tup_num, i_range_length_histogram));
+		if (!PQgetisnull(res, tup_num, i_range_empty_frac))
+			appendNamedArgument(out, fout, "range_empty_frac", "real",
+								PQgetvalue(res, tup_num, i_range_empty_frac));
+		if (!PQgetisnull(res, tup_num, i_range_bounds_histogram))
+			appendNamedArgument(out, fout, "range_bounds_histogram", "text",
+								PQgetvalue(res, tup_num, i_range_bounds_histogram));
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+
+
+/*
+ * printRelationStats --
  *
  * Generate the SQL statements needed to restore a relation's statistics.
  */
@@ -10485,64 +10802,21 @@ static char *
 printRelationStats(Archive *fout, const void *userArg)
 {
 	const RelStatsInfo *rsinfo = (RelStatsInfo *) userArg;
-	const DumpableObject *dobj = &rsinfo->dobj;
+	const DumpableObject *dobj;
+	const char *relschema;
+	const char *relname;
+
+	ArchiveHandle *AH = (ArchiveHandle *) fout;
 
-	PQExpBufferData query;
 	PQExpBufferData out;
 
-	PGresult   *res;
-
-	static bool first_query = true;
-	static int	i_attname;
-	static int	i_inherited;
-	static int	i_null_frac;
-	static int	i_avg_width;
-	static int	i_n_distinct;
-	static int	i_most_common_vals;
-	static int	i_most_common_freqs;
-	static int	i_histogram_bounds;
-	static int	i_correlation;
-	static int	i_most_common_elems;
-	static int	i_most_common_elem_freqs;
-	static int	i_elem_count_histogram;
-	static int	i_range_length_histogram;
-	static int	i_range_empty_frac;
-	static int	i_range_bounds_histogram;
-
-	initPQExpBuffer(&query);
-
-	if (first_query)
-	{
-		appendPQExpBufferStr(&query,
-							 "PREPARE getAttributeStats(pg_catalog.text, pg_catalog.text) AS\n"
-							 "SELECT s.attname, s.inherited, "
-							 "s.null_frac, s.avg_width, s.n_distinct, "
-							 "s.most_common_vals, s.most_common_freqs, "
-							 "s.histogram_bounds, s.correlation, "
-							 "s.most_common_elems, s.most_common_elem_freqs, "
-							 "s.elem_count_histogram, ");
-
-		if (fout->remoteVersion >= 170000)
-			appendPQExpBufferStr(&query,
-								 "s.range_length_histogram, "
-								 "s.range_empty_frac, "
-								 "s.range_bounds_histogram ");
-		else
-			appendPQExpBufferStr(&query,
-								 "NULL AS range_length_histogram,"
-								 "NULL AS range_empty_frac,"
-								 "NULL AS range_bounds_histogram ");
-
-		appendPQExpBufferStr(&query,
-							 "FROM pg_catalog.pg_stats s "
-							 "WHERE s.schemaname = $1 "
-							 "AND s.tablename = $2 "
-							 "ORDER BY s.attname, s.inherited");
-
-		ExecuteSqlStatement(fout, query.data);
-
-		resetPQExpBuffer(&query);
-	}
+	Assert(rsinfo != NULL);
+	dobj = &rsinfo->dobj;
+	Assert(dobj != NULL);
+	relschema = dobj->namespace->dobj.name;
+	Assert(relschema != NULL);
+	relname = dobj->name;
+	Assert(relname != NULL);
 
 	initPQExpBuffer(&out);
 
@@ -10561,132 +10835,72 @@ printRelationStats(Archive *fout, const void *userArg)
 	appendPQExpBuffer(&out, "\t'relallvisible', '%d'::integer\n);\n",
 					  rsinfo->relallvisible);
 
-	/* fetch attribute stats */
-	appendPQExpBufferStr(&query, "EXECUTE getAttributeStats(");
-	appendStringLiteralAH(&query, dobj->namespace->dobj.name, fout);
-	appendPQExpBufferStr(&query, ", ");
-	appendStringLiteralAH(&query, dobj->name, fout);
-	appendPQExpBufferStr(&query, ")");
+	AH->txnCount++;
 
-	res = ExecuteSqlQuery(fout, query.data, PGRES_TUPLES_OK);
+	if (attrstats.state == STATSBUF_UNINITIALIZED)
+		initAttributeStats(fout);
 
-	if (first_query)
+	/*
+	 * Because the query returns rows in the same order as the relations
+	 * requested, and because every relation gets at least one row in the
+	 * result set, the first row for this relation must correspond either to
+	 * the current row of this result set (if one exists) or the first row of
+	 * the next result set (if this one is already consumed).
+	 */
+	if (attrstats.state != STATSBUF_ACTIVE)
+		pg_fatal("Exhausted getAttributeStats() before processing %s.%s",
+				 rsinfo->dobj.namespace->dobj.name,
+				 rsinfo->dobj.name);
+
+	/*
+	 * If the current result set has been fully consumed, then the row(s) we
+	 * need (if any) would be found in the next one. This will update
+	 * attrstats.res and attrstats.idx.
+	 */
+	if (PQntuples(attrstats.res) <= attrstats.idx)
+		fetchNextAttributeStats(fout);
+
+	while (true)
 	{
-		i_attname = PQfnumber(res, "attname");
-		i_inherited = PQfnumber(res, "inherited");
-		i_null_frac = PQfnumber(res, "null_frac");
-		i_avg_width = PQfnumber(res, "avg_width");
-		i_n_distinct = PQfnumber(res, "n_distinct");
-		i_most_common_vals = PQfnumber(res, "most_common_vals");
-		i_most_common_freqs = PQfnumber(res, "most_common_freqs");
-		i_histogram_bounds = PQfnumber(res, "histogram_bounds");
-		i_correlation = PQfnumber(res, "correlation");
-		i_most_common_elems = PQfnumber(res, "most_common_elems");
-		i_most_common_elem_freqs = PQfnumber(res, "most_common_elem_freqs");
-		i_elem_count_histogram = PQfnumber(res, "elem_count_histogram");
-		i_range_length_histogram = PQfnumber(res, "range_length_histogram");
-		i_range_empty_frac = PQfnumber(res, "range_empty_frac");
-		i_range_bounds_histogram = PQfnumber(res, "range_bounds_histogram");
-		first_query = false;
-	}
-
-	/* restore attribute stats */
-	for (int rownum = 0; rownum < PQntuples(res); rownum++)
-	{
-		const char *attname;
-
-		appendPQExpBufferStr(&out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
-		appendPQExpBuffer(&out, "\t'version', '%u'::integer,\n",
-						  fout->remoteVersion);
-		appendPQExpBufferStr(&out, "\t'schemaname', ");
-		appendStringLiteralAH(&out, rsinfo->dobj.namespace->dobj.name, fout);
-		appendPQExpBufferStr(&out, ",\n\t'relname', ");
-		appendStringLiteralAH(&out, rsinfo->dobj.name, fout);
-
-		if (PQgetisnull(res, rownum, i_attname))
-			pg_fatal("attname cannot be NULL");
-		attname = PQgetvalue(res, rownum, i_attname);
+		int			i_schemaname;
+		int			i_tablename;
+		char	   *schemaname;
+		char	   *tablename;	/* misnomer, following pg_stats naming */
 
 		/*
-		 * Indexes look up attname in indAttNames to derive attnum, all others
-		 * use attname directly.  We must specify attnum for indexes, since
-		 * their attnames are not necessarily stable across dump/reload.
+		 * If we hit the end of the result set, then there are no more records
+		 * for this relation, so we should stop, but first get the next result
+		 * set for the next batch of relations.
 		 */
-		if (rsinfo->nindAttNames == 0)
+		if (PQntuples(attrstats.res) <= attrstats.idx)
 		{
-			appendPQExpBuffer(&out, ",\n\t'attname', ");
-			appendStringLiteralAH(&out, attname, fout);
-		}
-		else
-		{
-			bool		found = false;
-
-			for (int i = 0; i < rsinfo->nindAttNames; i++)
-			{
-				if (strcmp(attname, rsinfo->indAttNames[i]) == 0)
-				{
-					appendPQExpBuffer(&out, ",\n\t'attnum', '%d'::smallint",
-									  i + 1);
-					found = true;
-					break;
-				}
-			}
-
-			if (!found)
-				pg_fatal("could not find index attname \"%s\"", attname);
+			fetchNextAttributeStats(fout);
+			break;
 		}
 
-		if (!PQgetisnull(res, rownum, i_inherited))
-			appendNamedArgument(&out, fout, "inherited", "boolean",
-								PQgetvalue(res, rownum, i_inherited));
-		if (!PQgetisnull(res, rownum, i_null_frac))
-			appendNamedArgument(&out, fout, "null_frac", "real",
-								PQgetvalue(res, rownum, i_null_frac));
-		if (!PQgetisnull(res, rownum, i_avg_width))
-			appendNamedArgument(&out, fout, "avg_width", "integer",
-								PQgetvalue(res, rownum, i_avg_width));
-		if (!PQgetisnull(res, rownum, i_n_distinct))
-			appendNamedArgument(&out, fout, "n_distinct", "real",
-								PQgetvalue(res, rownum, i_n_distinct));
-		if (!PQgetisnull(res, rownum, i_most_common_vals))
-			appendNamedArgument(&out, fout, "most_common_vals", "text",
-								PQgetvalue(res, rownum, i_most_common_vals));
-		if (!PQgetisnull(res, rownum, i_most_common_freqs))
-			appendNamedArgument(&out, fout, "most_common_freqs", "real[]",
-								PQgetvalue(res, rownum, i_most_common_freqs));
-		if (!PQgetisnull(res, rownum, i_histogram_bounds))
-			appendNamedArgument(&out, fout, "histogram_bounds", "text",
-								PQgetvalue(res, rownum, i_histogram_bounds));
-		if (!PQgetisnull(res, rownum, i_correlation))
-			appendNamedArgument(&out, fout, "correlation", "real",
-								PQgetvalue(res, rownum, i_correlation));
-		if (!PQgetisnull(res, rownum, i_most_common_elems))
-			appendNamedArgument(&out, fout, "most_common_elems", "text",
-								PQgetvalue(res, rownum, i_most_common_elems));
-		if (!PQgetisnull(res, rownum, i_most_common_elem_freqs))
-			appendNamedArgument(&out, fout, "most_common_elem_freqs", "real[]",
-								PQgetvalue(res, rownum, i_most_common_elem_freqs));
-		if (!PQgetisnull(res, rownum, i_elem_count_histogram))
-			appendNamedArgument(&out, fout, "elem_count_histogram", "real[]",
-								PQgetvalue(res, rownum, i_elem_count_histogram));
-		if (fout->remoteVersion >= 170000)
-		{
-			if (!PQgetisnull(res, rownum, i_range_length_histogram))
-				appendNamedArgument(&out, fout, "range_length_histogram", "text",
-									PQgetvalue(res, rownum, i_range_length_histogram));
-			if (!PQgetisnull(res, rownum, i_range_empty_frac))
-				appendNamedArgument(&out, fout, "range_empty_frac", "real",
-									PQgetvalue(res, rownum, i_range_empty_frac));
-			if (!PQgetisnull(res, rownum, i_range_bounds_histogram))
-				appendNamedArgument(&out, fout, "range_bounds_histogram", "text",
-									PQgetvalue(res, rownum, i_range_bounds_histogram));
-		}
-		appendPQExpBufferStr(&out, "\n);\n");
+		i_schemaname = PQfnumber(attrstats.res, "schemaname");
+		Assert(i_schemaname >= 0);
+		i_tablename = PQfnumber(attrstats.res, "tablename");
+		Assert(i_tablename >= 0);
+
+		if (PQgetisnull(attrstats.res, attrstats.idx, i_schemaname))
+			pg_fatal("getAttributeStats() schemaname cannot be NULL");
+
+		if (PQgetisnull(attrstats.res, attrstats.idx, i_tablename))
+			pg_fatal("getAttributeStats() tablename cannot be NULL");
+
+		schemaname = PQgetvalue(attrstats.res, attrstats.idx, i_schemaname);
+		tablename = PQgetvalue(attrstats.res, attrstats.idx, i_tablename);
+
+		/* stop if current stat row isn't for this relation */
+		if (strcmp(relname, tablename) != 0 || strcmp(relschema, schemaname) != 0)
+			break;
+
+		appendAttributeStats(fout, &out, rsinfo);
+		AH->txnCount++;
+		attrstats.idx++;
 	}
 
-	PQclear(res);
-
-	termPQExpBuffer(&query);
 	return out.data;
 }
 
-- 
2.48.1

#485Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#484)
5 attachment(s)
Re: Statistics Import and Export

On Fri, Mar 14, 2025 at 4:03 PM Corey Huinker <corey.huinker@gmail.com>
wrote:

New patches and a rebase.

0001 - no changes, but the longer I go the more I'm certain this is
something we want to do.
0002- same as 0001

0003 -

Storing the restore function calls in the archive entry hogged a lot of
memory and made people nervous. This introduces a new function pointer that
generates those restore SQL calls right before they're written to disk,
thus reducing the memory load from "stats for every object to be dumped" to
just one object. Thanks to Nathan for diagnosing some weird quirks with
various formats.

0004 -

This replaces the query in the prepared statement with one that batches
them 100 relations at a time, and then maintains that result set until it
is consumed. It seems to have obvious speedups.

Another rebase, and a new patch 0005 to have pg_dump fetch and restore
relallfrozen for dbs of version 18 and higher. With older versions we omit
relallfrozen and let the import function assign the default.

Attachments:

v9-0005-Add-relallfrozen-to-pg_dump-statistics.patchtext/x-patch; charset=US-ASCII; name=v9-0005-Add-relallfrozen-to-pg_dump-statistics.patchDownload
From 79b459706b09458b4c27d3f80a8fab9ec9600ce7 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 15 Mar 2025 17:34:30 -0400
Subject: [PATCH v9 5/5] Add relallfrozen to pg_dump statistics.

The column relallfrozen was recently added to pg_class and it also
represent statistics, so we should add it to the dump/restore/upgrade
operations.

Dumps of databases prior to v18 will not attempt to restore any value to
relallfrozen, allowing pg_restore_relation_stats() to set the default it
deems appropriate.
---
 src/bin/pg_dump/pg_dump.c        | 52 ++++++++++++++++++++++----------
 src/bin/pg_dump/pg_dump.h        |  1 +
 src/bin/pg_dump/t/002_pg_dump.pl |  3 +-
 3 files changed, 39 insertions(+), 17 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 0c26dc7a1b4..249bcfb80a1 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -6856,7 +6856,8 @@ getFuncs(Archive *fout)
  */
 static RelStatsInfo *
 getRelationStatistics(Archive *fout, DumpableObject *rel, int32 relpages,
-					  char *reltuples, int32 relallvisible, char relkind,
+					  char *reltuples, int32 relallvisible,
+					  int32 relallfrozen, char relkind,
 					  char **indAttNames, int nindAttNames)
 {
 	if (!fout->dopt->dumpStatistics)
@@ -6885,6 +6886,7 @@ getRelationStatistics(Archive *fout, DumpableObject *rel, int32 relpages,
 		info->relpages = relpages;
 		info->reltuples = pstrdup(reltuples);
 		info->relallvisible = relallvisible;
+		info->relallfrozen = relallfrozen;
 		info->relkind = relkind;
 		info->indAttNames = indAttNames;
 		info->nindAttNames = nindAttNames;
@@ -6924,6 +6926,7 @@ getTables(Archive *fout, int *numTables)
 	int			i_relpages;
 	int			i_reltuples;
 	int			i_relallvisible;
+	int			i_relallfrozen;
 	int			i_toastpages;
 	int			i_owning_tab;
 	int			i_owning_col;
@@ -6974,8 +6977,13 @@ getTables(Archive *fout, int *numTables)
 						 "c.relowner, "
 						 "c.relchecks, "
 						 "c.relhasindex, c.relhasrules, c.relpages, "
-						 "c.reltuples, c.relallvisible, c.relhastriggers, "
-						 "c.relpersistence, "
+						 "c.reltuples, c.relallvisible, ");
+
+	if (fout->remoteVersion >= 180000)
+		appendPQExpBufferStr(query, "c.relallfrozen, ");
+
+	appendPQExpBufferStr(query,
+						 "c.relhastriggers, c.relpersistence, "
 						 "c.reloftype, "
 						 "c.relacl, "
 						 "acldefault(CASE WHEN c.relkind = " CppAsString2(RELKIND_SEQUENCE)
@@ -7140,6 +7148,7 @@ getTables(Archive *fout, int *numTables)
 	i_relpages = PQfnumber(res, "relpages");
 	i_reltuples = PQfnumber(res, "reltuples");
 	i_relallvisible = PQfnumber(res, "relallvisible");
+	i_relallfrozen = PQfnumber(res, "relallfrozen");
 	i_toastpages = PQfnumber(res, "toastpages");
 	i_owning_tab = PQfnumber(res, "owning_tab");
 	i_owning_col = PQfnumber(res, "owning_col");
@@ -7187,6 +7196,7 @@ getTables(Archive *fout, int *numTables)
 	for (i = 0; i < ntups; i++)
 	{
 		int32		relallvisible = atoi(PQgetvalue(res, i, i_relallvisible));
+		int32		relallfrozen = atoi(PQgetvalue(res, i, i_relallfrozen));
 
 		tblinfo[i].dobj.objType = DO_TABLE;
 		tblinfo[i].dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_reltableoid));
@@ -7289,7 +7299,7 @@ getTables(Archive *fout, int *numTables)
 		if (tblinfo[i].interesting)
 			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relpages,
 								  PQgetvalue(res, i, i_reltuples),
-								  relallvisible, tblinfo[i].relkind, NULL, 0);
+								  relallvisible, relallfrozen, tblinfo[i].relkind, NULL, 0);
 
 		/*
 		 * Read-lock target tables to make sure they aren't DROPPED or altered
@@ -7558,6 +7568,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				i_relpages,
 				i_reltuples,
 				i_relallvisible,
+				i_relallfrozen,
 				i_parentidx,
 				i_indexdef,
 				i_indnkeyatts,
@@ -7612,7 +7623,12 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 	appendPQExpBufferStr(query,
 						 "SELECT t.tableoid, t.oid, i.indrelid, "
 						 "t.relname AS indexname, "
-						 "t.relpages, t.reltuples, t.relallvisible, "
+						 "t.relpages, t.reltuples, t.relallvisible, ");
+
+	if (fout->remoteVersion >= 180000)
+		appendPQExpBufferStr(query, "t.relallfrozen, ");
+
+	appendPQExpBufferStr(query,
 						 "pg_catalog.pg_get_indexdef(i.indexrelid) AS indexdef, "
 						 "i.indkey, i.indisclustered, "
 						 "c.contype, c.conname, "
@@ -7728,6 +7744,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_relpages = PQfnumber(res, "relpages");
 	i_reltuples = PQfnumber(res, "reltuples");
 	i_relallvisible = PQfnumber(res, "relallvisible");
+	i_relallfrozen = PQfnumber(res, "relallfrozen");
 	i_parentidx = PQfnumber(res, "parentidx");
 	i_indexdef = PQfnumber(res, "indexdef");
 	i_indnkeyatts = PQfnumber(res, "indnkeyatts");
@@ -7799,6 +7816,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			RelStatsInfo *relstats;
 			int32		relpages = atoi(PQgetvalue(res, j, i_relpages));
 			int32		relallvisible = atoi(PQgetvalue(res, j, i_relallvisible));
+			int32		relallfrozen = atoi(PQgetvalue(res, j, i_relallfrozen));
 
 			indxinfo[j].dobj.objType = DO_INDEX;
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
@@ -7841,7 +7859,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 
 			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, relpages,
 											 PQgetvalue(res, j, i_reltuples),
-											 relallvisible, indexkind,
+											 relallvisible, relallfrozen, indexkind,
 											 indAttNames, nindAttNames);
 
 			contype = *(PQgetvalue(res, j, i_contype));
@@ -10821,19 +10839,21 @@ printRelationStats(Archive *fout, const void *userArg)
 	initPQExpBuffer(&out);
 
 	/* restore relation stats */
-	appendPQExpBufferStr(&out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
-	appendPQExpBuffer(&out, "\t'version', '%u'::integer,\n",
+	appendPQExpBufferStr(&out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(");
+	appendPQExpBuffer(&out, "\n\t'version', '%u'::integer",
 					  fout->remoteVersion);
-	appendPQExpBufferStr(&out, "\t'schemaname', ");
+	appendPQExpBufferStr(&out, ",\n\t'schemaname', ");
 	appendStringLiteralAH(&out, rsinfo->dobj.namespace->dobj.name, fout);
-	appendPQExpBufferStr(&out, ",\n");
-	appendPQExpBufferStr(&out, "\t'relname', ");
+	appendPQExpBufferStr(&out, ",\n\t'relname', ");
 	appendStringLiteralAH(&out, rsinfo->dobj.name, fout);
-	appendPQExpBufferStr(&out, ",\n");
-	appendPQExpBuffer(&out, "\t'relpages', '%d'::integer,\n", rsinfo->relpages);
-	appendPQExpBuffer(&out, "\t'reltuples', '%s'::real,\n", rsinfo->reltuples);
-	appendPQExpBuffer(&out, "\t'relallvisible', '%d'::integer\n);\n",
-					  rsinfo->relallvisible);
+	appendPQExpBuffer(&out, ",\n\t'relpages', '%d'::integer", rsinfo->relpages);
+	appendPQExpBuffer(&out, ",\n\t'reltuples', '%s'::real", rsinfo->reltuples);
+	appendPQExpBuffer(&out, ",\n\t'relallvisible', '%d'::integer", rsinfo->relallvisible);
+
+	if (fout->remoteVersion >= 180000)
+		appendPQExpBuffer(&out, ",\n\t'relallfrozen', '%d'::integer", rsinfo->relallfrozen);
+
+	appendPQExpBufferStr(&out, "\n);\n");
 
 	AH->txnCount++;
 
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index bbdb30b5f54..82f1eb3c4b7 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -441,6 +441,7 @@ typedef struct _relStatsInfo
 	int32		relpages;
 	char	   *reltuples;
 	int32		relallvisible;
+	int32		relallfrozen;
 	char		relkind;		/* 'r', 'm', 'i', etc */
 
 	/*
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index b037f239136..1d69e55a861 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -4729,7 +4729,8 @@ my %tests = (
 			'relname',\s'dup_test_post_data_ix',\s+
 			'relpages',\s'\d+'::integer,\s+
 			'reltuples',\s'\d+'::real,\s+
-			'relallvisible',\s'\d+'::integer\s+
+			'relallvisible',\s'\d+'::integer,\s+
+			'relallfrozen',\s'\d+'::integer\s+
 			\);\s+
 			\QSELECT * FROM pg_catalog.pg_restore_attribute_stats(\E\s+
 			'version',\s'\d+'::integer,\s+
-- 
2.48.1

v9-0003-Introduce-CreateStmtPtr.patchtext/x-patch; charset=US-ASCII; name=v9-0003-Introduce-CreateStmtPtr.patchDownload
From 72d0fe4b5de382a2f36151de2b26960d3a85267f Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Fri, 14 Mar 2025 01:06:19 -0400
Subject: [PATCH v9 3/5] Introduce CreateStmtPtr.

CreateStmtPtr is a function pointer that can replace the createStmt/defn
parameter. This is useful in situations where the amount of text
generated for a definition is so large that it is undesirable to hold
many such objects in memory at the same time.

Using functions of this type, the text created is then immediately
written out to the appropriate file for the given dump format.
---
 src/bin/pg_dump/pg_backup.h          |   2 +
 src/bin/pg_dump/pg_backup_archiver.c |  22 ++-
 src/bin/pg_dump/pg_backup_archiver.h |   7 +
 src/bin/pg_dump/pg_dump.c            | 230 +++++++++++++++------------
 4 files changed, 156 insertions(+), 105 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index e783cc68d89..bb175874a5a 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -287,6 +287,8 @@ typedef int (*DataDumperPtr) (Archive *AH, const void *userArg);
 
 typedef void (*SetupWorkerPtrType) (Archive *AH);
 
+typedef char *(*CreateStmtPtr) (Archive *AH, const void *userArg);
+
 /*
  * Main archiver interface.
  */
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 7480e122b61..3fcfecf6719 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -1263,6 +1263,9 @@ ArchiveEntry(Archive *AHX, CatalogId catalogId, DumpId dumpId,
 	newToc->dataDumper = opts->dumpFn;
 	newToc->dataDumperArg = opts->dumpArg;
 	newToc->hadDumper = opts->dumpFn ? true : false;
+	newToc->createDumper = opts->createFn;
+	newToc->createDumperArg = opts->createArg;
+	newToc->hadCreateDumper = opts->createFn ? true : false;
 
 	newToc->formatData = NULL;
 	newToc->dataLength = 0;
@@ -2619,7 +2622,17 @@ WriteToc(ArchiveHandle *AH)
 		WriteStr(AH, te->tag);
 		WriteStr(AH, te->desc);
 		WriteInt(AH, te->section);
-		WriteStr(AH, te->defn);
+
+		if (te->hadCreateDumper)
+		{
+			char	   *defn = te->createDumper((Archive *) AH, te->createDumperArg);
+
+			WriteStr(AH, defn);
+			pg_free(defn);
+		}
+		else
+			WriteStr(AH, te->defn);
+
 		WriteStr(AH, te->dropStmt);
 		WriteStr(AH, te->copyStmt);
 		WriteStr(AH, te->namespace);
@@ -3849,6 +3862,13 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 	{
 		IssueACLPerBlob(AH, te);
 	}
+	else if (te->hadCreateDumper)
+	{
+		char	   *ptr = te->createDumper((Archive *) AH, te->createDumperArg);
+
+		ahwrite(ptr, 1, strlen(ptr), AH);
+		pg_free(ptr);
+	}
 	else if (te->defn && strlen(te->defn) > 0)
 	{
 		ahprintf(AH, "%s\n\n", te->defn);
diff --git a/src/bin/pg_dump/pg_backup_archiver.h b/src/bin/pg_dump/pg_backup_archiver.h
index a2064f471ed..e68db633995 100644
--- a/src/bin/pg_dump/pg_backup_archiver.h
+++ b/src/bin/pg_dump/pg_backup_archiver.h
@@ -368,6 +368,11 @@ struct _tocEntry
 	const void *dataDumperArg;	/* Arg for above routine */
 	void	   *formatData;		/* TOC Entry data specific to file format */
 
+	CreateStmtPtr createDumper; /* Routine for create statement creation */
+	const void *createDumperArg;	/* arg for the above routine */
+	bool		hadCreateDumper;	/* Archiver was passed a create statement
+									 * routine */
+
 	/* working state while dumping/restoring */
 	pgoff_t		dataLength;		/* item's data size; 0 if none or unknown */
 	int			reqs;			/* do we need schema and/or data of object
@@ -407,6 +412,8 @@ typedef struct _archiveOpts
 	int			nDeps;
 	DataDumperPtr dumpFn;
 	const void *dumpArg;
+	CreateStmtPtr createFn;
+	const void *createArg;
 } ArchiveOpts;
 #define ARCHIVE_OPTS(...) &(ArchiveOpts){__VA_ARGS__}
 /* Called to add a TOC entry */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index bd857bb076c..38ba6a90106 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -10477,51 +10477,44 @@ statisticsDumpSection(const RelStatsInfo *rsinfo)
 }
 
 /*
- * dumpRelationStats --
+ * printDumpRelationStats --
  *
- * Dump command to import stats into the relation on the new database.
+ * Generate the SQL statements needed to restore a relation's statistics.
  */
-static void
-dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+static char *
+printRelationStats(Archive *fout, const void *userArg)
 {
+	const RelStatsInfo *rsinfo = (RelStatsInfo *) userArg;
 	const DumpableObject *dobj = &rsinfo->dobj;
+
+	PQExpBufferData query;
+	PQExpBufferData out;
+
 	PGresult   *res;
-	PQExpBuffer query;
-	PQExpBuffer out;
-	DumpId	   *deps = NULL;
-	int			ndeps = 0;
-	int			i_attname;
-	int			i_inherited;
-	int			i_null_frac;
-	int			i_avg_width;
-	int			i_n_distinct;
-	int			i_most_common_vals;
-	int			i_most_common_freqs;
-	int			i_histogram_bounds;
-	int			i_correlation;
-	int			i_most_common_elems;
-	int			i_most_common_elem_freqs;
-	int			i_elem_count_histogram;
-	int			i_range_length_histogram;
-	int			i_range_empty_frac;
-	int			i_range_bounds_histogram;
 
-	/* nothing to do if we are not dumping statistics */
-	if (!fout->dopt->dumpStatistics)
-		return;
+	static bool first_query = true;
+	static int	i_attname;
+	static int	i_inherited;
+	static int	i_null_frac;
+	static int	i_avg_width;
+	static int	i_n_distinct;
+	static int	i_most_common_vals;
+	static int	i_most_common_freqs;
+	static int	i_histogram_bounds;
+	static int	i_correlation;
+	static int	i_most_common_elems;
+	static int	i_most_common_elem_freqs;
+	static int	i_elem_count_histogram;
+	static int	i_range_length_histogram;
+	static int	i_range_empty_frac;
+	static int	i_range_bounds_histogram;
 
-	/* dependent on the relation definition, if doing schema */
-	if (fout->dopt->dumpSchema)
+	initPQExpBuffer(&query);
+
+	if (first_query)
 	{
-		deps = dobj->dependencies;
-		ndeps = dobj->nDeps;
-	}
-
-	query = createPQExpBuffer();
-	if (!fout->is_prepared[PREPQUERY_GETATTRIBUTESTATS])
-	{
-		appendPQExpBufferStr(query,
-							 "PREPARE getAttributeStats(pg_catalog.name, pg_catalog.name) AS\n"
+		appendPQExpBufferStr(&query,
+							 "PREPARE getAttributeStats(pg_catalog.text, pg_catalog.text) AS\n"
 							 "SELECT s.attname, s.inherited, "
 							 "s.null_frac, s.avg_width, s.n_distinct, "
 							 "s.most_common_vals, s.most_common_freqs, "
@@ -10530,82 +10523,85 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 							 "s.elem_count_histogram, ");
 
 		if (fout->remoteVersion >= 170000)
-			appendPQExpBufferStr(query,
+			appendPQExpBufferStr(&query,
 								 "s.range_length_histogram, "
 								 "s.range_empty_frac, "
 								 "s.range_bounds_histogram ");
 		else
-			appendPQExpBufferStr(query,
+			appendPQExpBufferStr(&query,
 								 "NULL AS range_length_histogram,"
 								 "NULL AS range_empty_frac,"
 								 "NULL AS range_bounds_histogram ");
 
-		appendPQExpBufferStr(query,
+		appendPQExpBufferStr(&query,
 							 "FROM pg_catalog.pg_stats s "
 							 "WHERE s.schemaname = $1 "
 							 "AND s.tablename = $2 "
 							 "ORDER BY s.attname, s.inherited");
 
-		ExecuteSqlStatement(fout, query->data);
+		ExecuteSqlStatement(fout, query.data);
 
-		fout->is_prepared[PREPQUERY_GETATTRIBUTESTATS] = true;
-		resetPQExpBuffer(query);
+		resetPQExpBuffer(&query);
 	}
 
-	out = createPQExpBuffer();
+	initPQExpBuffer(&out);
 
 	/* restore relation stats */
-	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
-	appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
+	appendPQExpBufferStr(&out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
+	appendPQExpBuffer(&out, "\t'version', '%u'::integer,\n",
 					  fout->remoteVersion);
-	appendPQExpBufferStr(out, "\t'schemaname', ");
-	appendStringLiteralAH(out, rsinfo->dobj.namespace->dobj.name, fout);
-	appendPQExpBufferStr(out, ",\n");
-	appendPQExpBufferStr(out, "\t'relname', ");
-	appendStringLiteralAH(out, rsinfo->dobj.name, fout);
-	appendPQExpBufferStr(out, ",\n");
-	appendPQExpBuffer(out, "\t'relpages', '%d'::integer,\n", rsinfo->relpages);
-	appendPQExpBuffer(out, "\t'reltuples', '%s'::real,\n", rsinfo->reltuples);
-	appendPQExpBuffer(out, "\t'relallvisible', '%d'::integer\n);\n",
+	appendPQExpBufferStr(&out, "\t'schemaname', ");
+	appendStringLiteralAH(&out, rsinfo->dobj.namespace->dobj.name, fout);
+	appendPQExpBufferStr(&out, ",\n");
+	appendPQExpBufferStr(&out, "\t'relname', ");
+	appendStringLiteralAH(&out, rsinfo->dobj.name, fout);
+	appendPQExpBufferStr(&out, ",\n");
+	appendPQExpBuffer(&out, "\t'relpages', '%d'::integer,\n", rsinfo->relpages);
+	appendPQExpBuffer(&out, "\t'reltuples', '%s'::real,\n", rsinfo->reltuples);
+	appendPQExpBuffer(&out, "\t'relallvisible', '%d'::integer\n);\n",
 					  rsinfo->relallvisible);
 
 	/* fetch attribute stats */
-	appendPQExpBufferStr(query, "EXECUTE getAttributeStats(");
-	appendStringLiteralAH(query, dobj->namespace->dobj.name, fout);
-	appendPQExpBufferStr(query, ", ");
-	appendStringLiteralAH(query, dobj->name, fout);
-	appendPQExpBufferStr(query, ");");
+	appendPQExpBufferStr(&query, "EXECUTE getAttributeStats(");
+	appendStringLiteralAH(&query, dobj->namespace->dobj.name, fout);
+	appendPQExpBufferStr(&query, ", ");
+	appendStringLiteralAH(&query, dobj->name, fout);
+	appendPQExpBufferStr(&query, ")");
 
-	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	res = ExecuteSqlQuery(fout, query.data, PGRES_TUPLES_OK);
 
-	i_attname = PQfnumber(res, "attname");
-	i_inherited = PQfnumber(res, "inherited");
-	i_null_frac = PQfnumber(res, "null_frac");
-	i_avg_width = PQfnumber(res, "avg_width");
-	i_n_distinct = PQfnumber(res, "n_distinct");
-	i_most_common_vals = PQfnumber(res, "most_common_vals");
-	i_most_common_freqs = PQfnumber(res, "most_common_freqs");
-	i_histogram_bounds = PQfnumber(res, "histogram_bounds");
-	i_correlation = PQfnumber(res, "correlation");
-	i_most_common_elems = PQfnumber(res, "most_common_elems");
-	i_most_common_elem_freqs = PQfnumber(res, "most_common_elem_freqs");
-	i_elem_count_histogram = PQfnumber(res, "elem_count_histogram");
-	i_range_length_histogram = PQfnumber(res, "range_length_histogram");
-	i_range_empty_frac = PQfnumber(res, "range_empty_frac");
-	i_range_bounds_histogram = PQfnumber(res, "range_bounds_histogram");
+	if (first_query)
+	{
+		i_attname = PQfnumber(res, "attname");
+		i_inherited = PQfnumber(res, "inherited");
+		i_null_frac = PQfnumber(res, "null_frac");
+		i_avg_width = PQfnumber(res, "avg_width");
+		i_n_distinct = PQfnumber(res, "n_distinct");
+		i_most_common_vals = PQfnumber(res, "most_common_vals");
+		i_most_common_freqs = PQfnumber(res, "most_common_freqs");
+		i_histogram_bounds = PQfnumber(res, "histogram_bounds");
+		i_correlation = PQfnumber(res, "correlation");
+		i_most_common_elems = PQfnumber(res, "most_common_elems");
+		i_most_common_elem_freqs = PQfnumber(res, "most_common_elem_freqs");
+		i_elem_count_histogram = PQfnumber(res, "elem_count_histogram");
+		i_range_length_histogram = PQfnumber(res, "range_length_histogram");
+		i_range_empty_frac = PQfnumber(res, "range_empty_frac");
+		i_range_bounds_histogram = PQfnumber(res, "range_bounds_histogram");
+		first_query = false;
+	}
 
 	/* restore attribute stats */
 	for (int rownum = 0; rownum < PQntuples(res); rownum++)
 	{
 		const char *attname;
 
-		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
-		appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
+		appendPQExpBufferStr(&out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+		appendPQExpBuffer(&out, "\t'version', '%u'::integer,\n",
 						  fout->remoteVersion);
-		appendPQExpBufferStr(out, "\t'schemaname', ");
-		appendStringLiteralAH(out, rsinfo->dobj.namespace->dobj.name, fout);
-		appendPQExpBufferStr(out, ",\n\t'relname', ");
-		appendStringLiteralAH(out, rsinfo->dobj.name, fout);
+		appendPQExpBufferStr(&out, "\t'schemaname', ");
+		appendStringLiteralAH(&out, rsinfo->dobj.namespace->dobj.name, fout);
+		appendPQExpBufferStr(&out, ",\n\t'relname', ");
+		appendStringLiteralAH(&out, rsinfo->dobj.name, fout);
 
 		if (PQgetisnull(res, rownum, i_attname))
 			pg_fatal("attname cannot be NULL");
@@ -10618,8 +10614,8 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 		 */
 		if (rsinfo->nindAttNames == 0)
 		{
-			appendPQExpBuffer(out, ",\n\t'attname', ");
-			appendStringLiteralAH(out, attname, fout);
+			appendPQExpBuffer(&out, ",\n\t'attname', ");
+			appendStringLiteralAH(&out, attname, fout);
 		}
 		else
 		{
@@ -10629,7 +10625,7 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 			{
 				if (strcmp(attname, rsinfo->indAttNames[i]) == 0)
 				{
-					appendPQExpBuffer(out, ",\n\t'attnum', '%d'::smallint",
+					appendPQExpBuffer(&out, ",\n\t'attnum', '%d'::smallint",
 									  i + 1);
 					found = true;
 					break;
@@ -10641,67 +10637,93 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 		}
 
 		if (!PQgetisnull(res, rownum, i_inherited))
-			appendNamedArgument(out, fout, "inherited", "boolean",
+			appendNamedArgument(&out, fout, "inherited", "boolean",
 								PQgetvalue(res, rownum, i_inherited));
 		if (!PQgetisnull(res, rownum, i_null_frac))
-			appendNamedArgument(out, fout, "null_frac", "real",
+			appendNamedArgument(&out, fout, "null_frac", "real",
 								PQgetvalue(res, rownum, i_null_frac));
 		if (!PQgetisnull(res, rownum, i_avg_width))
-			appendNamedArgument(out, fout, "avg_width", "integer",
+			appendNamedArgument(&out, fout, "avg_width", "integer",
 								PQgetvalue(res, rownum, i_avg_width));
 		if (!PQgetisnull(res, rownum, i_n_distinct))
-			appendNamedArgument(out, fout, "n_distinct", "real",
+			appendNamedArgument(&out, fout, "n_distinct", "real",
 								PQgetvalue(res, rownum, i_n_distinct));
 		if (!PQgetisnull(res, rownum, i_most_common_vals))
-			appendNamedArgument(out, fout, "most_common_vals", "text",
+			appendNamedArgument(&out, fout, "most_common_vals", "text",
 								PQgetvalue(res, rownum, i_most_common_vals));
 		if (!PQgetisnull(res, rownum, i_most_common_freqs))
-			appendNamedArgument(out, fout, "most_common_freqs", "real[]",
+			appendNamedArgument(&out, fout, "most_common_freqs", "real[]",
 								PQgetvalue(res, rownum, i_most_common_freqs));
 		if (!PQgetisnull(res, rownum, i_histogram_bounds))
-			appendNamedArgument(out, fout, "histogram_bounds", "text",
+			appendNamedArgument(&out, fout, "histogram_bounds", "text",
 								PQgetvalue(res, rownum, i_histogram_bounds));
 		if (!PQgetisnull(res, rownum, i_correlation))
-			appendNamedArgument(out, fout, "correlation", "real",
+			appendNamedArgument(&out, fout, "correlation", "real",
 								PQgetvalue(res, rownum, i_correlation));
 		if (!PQgetisnull(res, rownum, i_most_common_elems))
-			appendNamedArgument(out, fout, "most_common_elems", "text",
+			appendNamedArgument(&out, fout, "most_common_elems", "text",
 								PQgetvalue(res, rownum, i_most_common_elems));
 		if (!PQgetisnull(res, rownum, i_most_common_elem_freqs))
-			appendNamedArgument(out, fout, "most_common_elem_freqs", "real[]",
+			appendNamedArgument(&out, fout, "most_common_elem_freqs", "real[]",
 								PQgetvalue(res, rownum, i_most_common_elem_freqs));
 		if (!PQgetisnull(res, rownum, i_elem_count_histogram))
-			appendNamedArgument(out, fout, "elem_count_histogram", "real[]",
+			appendNamedArgument(&out, fout, "elem_count_histogram", "real[]",
 								PQgetvalue(res, rownum, i_elem_count_histogram));
 		if (fout->remoteVersion >= 170000)
 		{
 			if (!PQgetisnull(res, rownum, i_range_length_histogram))
-				appendNamedArgument(out, fout, "range_length_histogram", "text",
+				appendNamedArgument(&out, fout, "range_length_histogram", "text",
 									PQgetvalue(res, rownum, i_range_length_histogram));
 			if (!PQgetisnull(res, rownum, i_range_empty_frac))
-				appendNamedArgument(out, fout, "range_empty_frac", "real",
+				appendNamedArgument(&out, fout, "range_empty_frac", "real",
 									PQgetvalue(res, rownum, i_range_empty_frac));
 			if (!PQgetisnull(res, rownum, i_range_bounds_histogram))
-				appendNamedArgument(out, fout, "range_bounds_histogram", "text",
+				appendNamedArgument(&out, fout, "range_bounds_histogram", "text",
 									PQgetvalue(res, rownum, i_range_bounds_histogram));
 		}
-		appendPQExpBufferStr(out, "\n);\n");
+		appendPQExpBufferStr(&out, "\n);\n");
 	}
 
 	PQclear(res);
 
+	termPQExpBuffer(&query);
+	return out.data;
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	const DumpableObject *dobj = &rsinfo->dobj;
+
+	DumpId	   *deps = NULL;
+	int			ndeps = 0;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
+	/* dependent on the relation definition, if doing schema */
+	if (fout->dopt->dumpSchema)
+	{
+		deps = dobj->dependencies;
+		ndeps = dobj->nDeps;
+	}
+
 	ArchiveEntry(fout, nilCatalogId, createDumpId(),
 				 ARCHIVE_OPTS(.tag = dobj->name,
 							  .namespace = dobj->namespace->dobj.name,
 							  .description = "STATISTICS DATA",
 							  .section = rsinfo->postponed_def ?
 							  SECTION_POST_DATA : statisticsDumpSection(rsinfo),
-							  .createStmt = out->data,
+							  .createFn = printRelationStats,
+							  .createArg = rsinfo,
 							  .deps = deps,
 							  .nDeps = ndeps));
-
-	destroyPQExpBuffer(out);
-	destroyPQExpBuffer(query);
 }
 
 /*
-- 
2.48.1

v9-0001-Split-relation-into-schemaname-and-relname.patchtext/x-patch; charset=US-ASCII; name=v9-0001-Split-relation-into-schemaname-and-relname.patchDownload
From fd2cced89c11353e909e6ef9bb824a3a7536e6cb Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Tue, 4 Mar 2025 22:16:52 -0500
Subject: [PATCH v9 1/5] Split relation into schemaname and relname.

In order to further reduce potential error-failures in restores and
upgrades, replace the numerous casts of fully qualified relation names
into their schema+relname text components.

Further remove the ::name casts on attname and change the expected
datatype to text.

Add an ACL_USAGE check on the namespace oid after it is looked up.
---
 src/include/catalog/pg_proc.dat            |   8 +-
 src/include/statistics/stat_utils.h        |   2 +
 src/backend/statistics/attribute_stats.c   |  87 ++++--
 src/backend/statistics/relation_stats.c    |  65 +++--
 src/backend/statistics/stat_utils.c        |  37 +++
 src/bin/pg_dump/pg_dump.c                  |  25 +-
 src/bin/pg_dump/t/002_pg_dump.pl           |   6 +-
 src/test/regress/expected/stats_import.out | 307 +++++++++++++--------
 src/test/regress/sql/stats_import.sql      | 276 +++++++++++-------
 doc/src/sgml/func.sgml                     |  41 +--
 10 files changed, 566 insertions(+), 288 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 890822eaf79..8dee321d248 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12453,8 +12453,8 @@
   descr => 'clear statistics on relation',
   proname => 'pg_clear_relation_stats', provolatile => 'v', proisstrict => 'f',
   proparallel => 'u', prorettype => 'void',
-  proargtypes => 'regclass',
-  proargnames => '{relation}',
+  proargtypes => 'text text',
+  proargnames => '{schemaname,relname}',
   prosrc => 'pg_clear_relation_stats' },
 { oid => '8461',
   descr => 'restore statistics on attribute',
@@ -12469,8 +12469,8 @@
   descr => 'clear statistics on attribute',
   proname => 'pg_clear_attribute_stats', provolatile => 'v', proisstrict => 'f',
   proparallel => 'u', prorettype => 'void',
-  proargtypes => 'regclass name bool',
-  proargnames => '{relation,attname,inherited}',
+  proargtypes => 'text text text bool',
+  proargnames => '{schemaname,relname,attname,inherited}',
   prosrc => 'pg_clear_attribute_stats' },
 
 # GiST stratnum implementations
diff --git a/src/include/statistics/stat_utils.h b/src/include/statistics/stat_utils.h
index 0eb4decfcac..cad042c8e4a 100644
--- a/src/include/statistics/stat_utils.h
+++ b/src/include/statistics/stat_utils.h
@@ -32,6 +32,8 @@ extern bool stats_check_arg_pair(FunctionCallInfo fcinfo,
 
 extern void stats_lock_check_privileges(Oid reloid);
 
+extern Oid stats_schema_check_privileges(const char *nspname);
+
 extern bool stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
 											 FunctionCallInfo positional_fcinfo,
 											 struct StatsArgInfo *arginfo);
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index 6bcbee0edba..f87db2d6102 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -36,7 +36,8 @@
 
 enum attribute_stats_argnum
 {
-	ATTRELATION_ARG = 0,
+	ATTRELSCHEMA_ARG = 0,
+	ATTRELNAME_ARG,
 	ATTNAME_ARG,
 	ATTNUM_ARG,
 	INHERITED_ARG,
@@ -58,8 +59,9 @@ enum attribute_stats_argnum
 
 static struct StatsArgInfo attarginfo[] =
 {
-	[ATTRELATION_ARG] = {"relation", REGCLASSOID},
-	[ATTNAME_ARG] = {"attname", NAMEOID},
+	[ATTRELSCHEMA_ARG] = {"schemaname", TEXTOID},
+	[ATTRELNAME_ARG] = {"relname", TEXTOID},
+	[ATTNAME_ARG] = {"attname", TEXTOID},
 	[ATTNUM_ARG] = {"attnum", INT2OID},
 	[INHERITED_ARG] = {"inherited", BOOLOID},
 	[NULL_FRAC_ARG] = {"null_frac", FLOAT4OID},
@@ -80,7 +82,8 @@ static struct StatsArgInfo attarginfo[] =
 
 enum clear_attribute_stats_argnum
 {
-	C_ATTRELATION_ARG = 0,
+	C_ATTRELSCHEMA_ARG = 0,
+	C_ATTRELNAME_ARG,
 	C_ATTNAME_ARG,
 	C_INHERITED_ARG,
 	C_NUM_ATTRIBUTE_STATS_ARGS
@@ -88,8 +91,9 @@ enum clear_attribute_stats_argnum
 
 static struct StatsArgInfo cleararginfo[] =
 {
-	[C_ATTRELATION_ARG] = {"relation", REGCLASSOID},
-	[C_ATTNAME_ARG] = {"attname", NAMEOID},
+	[C_ATTRELSCHEMA_ARG] = {"relation", TEXTOID},
+	[C_ATTRELNAME_ARG] = {"relation", TEXTOID},
+	[C_ATTNAME_ARG] = {"attname", TEXTOID},
 	[C_INHERITED_ARG] = {"inherited", BOOLOID},
 	[C_NUM_ATTRIBUTE_STATS_ARGS] = {0}
 };
@@ -133,6 +137,9 @@ static void init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
 static bool
 attribute_statistics_update(FunctionCallInfo fcinfo)
 {
+	char	   *nspname;
+	Oid			nspoid;
+	char	   *relname;
 	Oid			reloid;
 	char	   *attname;
 	AttrNumber	attnum;
@@ -170,8 +177,23 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 	bool		result = true;
 
-	stats_check_required_arg(fcinfo, attarginfo, ATTRELATION_ARG);
-	reloid = PG_GETARG_OID(ATTRELATION_ARG);
+	stats_check_required_arg(fcinfo, attarginfo, ATTRELSCHEMA_ARG);
+	stats_check_required_arg(fcinfo, attarginfo, ATTRELNAME_ARG);
+
+	nspname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELSCHEMA_ARG));
+	nspoid = stats_schema_check_privileges(nspname);
+	if (nspoid == InvalidOid)
+		return false;
+
+	relname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELNAME_ARG));
+	reloid = get_relname_relid(relname, nspoid);
+	if (reloid == InvalidOid)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("Relation \"%s\".\"%s\" not found.", nspname, relname)));
+		return false;
+	}
 
 	if (RecoveryInProgress())
 		ereport(ERROR,
@@ -185,21 +207,18 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	/* user can specify either attname or attnum, but not both */
 	if (!PG_ARGISNULL(ATTNAME_ARG))
 	{
-		Name		attnamename;
-
 		if (!PG_ARGISNULL(ATTNUM_ARG))
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("cannot specify both attname and attnum")));
-		attnamename = PG_GETARG_NAME(ATTNAME_ARG);
-		attname = NameStr(*attnamename);
+		attname = TextDatumGetCString(PG_GETARG_DATUM(ATTNAME_ARG));
 		attnum = get_attnum(reloid, attname);
 		/* note that this test covers attisdropped cases too: */
 		if (attnum == InvalidAttrNumber)
 			ereport(ERROR,
 					(errcode(ERRCODE_UNDEFINED_COLUMN),
-					 errmsg("column \"%s\" of relation \"%s\" does not exist",
-							attname, get_rel_name(reloid))));
+					 errmsg("column \"%s\" of relation \"%s\".\"%s\" does not exist",
+							attname, nspname, relname)));
 	}
 	else if (!PG_ARGISNULL(ATTNUM_ARG))
 	{
@@ -210,8 +229,8 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 			!SearchSysCacheExistsAttName(reloid, attname))
 			ereport(ERROR,
 					(errcode(ERRCODE_UNDEFINED_COLUMN),
-					 errmsg("column %d of relation \"%s\" does not exist",
-							attnum, get_rel_name(reloid))));
+					 errmsg("column %d of relation \"%s\".\"%s\" does not exist",
+							attnum, nspname, relname)));
 	}
 	else
 	{
@@ -900,13 +919,33 @@ init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
 Datum
 pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 {
+	char	   *nspname;
+	Oid			nspoid;
+	char	   *relname;
 	Oid			reloid;
-	Name		attname;
+	char	   *attname;
 	AttrNumber	attnum;
 	bool		inherited;
 
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELATION_ARG);
-	reloid = PG_GETARG_OID(C_ATTRELATION_ARG);
+	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELSCHEMA_ARG);
+	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELNAME_ARG);
+	stats_check_required_arg(fcinfo, cleararginfo, C_ATTNAME_ARG);
+	stats_check_required_arg(fcinfo, cleararginfo, C_INHERITED_ARG);
+
+	nspname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELSCHEMA_ARG));
+	nspoid = stats_schema_check_privileges(nspname);
+	if (!OidIsValid(nspoid))
+		return false;
+
+	relname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELNAME_ARG));
+	reloid = get_relname_relid(relname, nspoid);
+	if (!OidIsValid(reloid))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("Relation \"%s\".\"%s\" not found.", nspname, relname)));
+		return false;
+	}
 
 	if (RecoveryInProgress())
 		ereport(ERROR,
@@ -916,23 +955,21 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 
 	stats_lock_check_privileges(reloid);
 
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTNAME_ARG);
-	attname = PG_GETARG_NAME(C_ATTNAME_ARG);
-	attnum = get_attnum(reloid, NameStr(*attname));
+	attname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTNAME_ARG));
+	attnum = get_attnum(reloid, attname);
 
 	if (attnum < 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot clear statistics on system column \"%s\"",
-						NameStr(*attname))));
+						attname)));
 
 	if (attnum == InvalidAttrNumber)
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 errmsg("column \"%s\" of relation \"%s\" does not exist",
-						NameStr(*attname), get_rel_name(reloid))));
+						attname, get_rel_name(reloid))));
 
-	stats_check_required_arg(fcinfo, cleararginfo, C_INHERITED_ARG);
 	inherited = PG_GETARG_BOOL(C_INHERITED_ARG);
 
 	delete_pg_statistic(reloid, attnum, inherited);
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index 52dfa477187..fdc69bc93e2 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -19,9 +19,12 @@
 
 #include "access/heapam.h"
 #include "catalog/indexing.h"
+#include "catalog/namespace.h"
 #include "statistics/stat_utils.h"
+#include "utils/builtins.h"
 #include "utils/fmgroids.h"
 #include "utils/fmgrprotos.h"
+#include "utils/lsyscache.h"
 #include "utils/syscache.h"
 
 
@@ -32,7 +35,8 @@
 
 enum relation_stats_argnum
 {
-	RELATION_ARG = 0,
+	RELSCHEMA_ARG = 0,
+	RELNAME_ARG,
 	RELPAGES_ARG,
 	RELTUPLES_ARG,
 	RELALLVISIBLE_ARG,
@@ -42,7 +46,8 @@ enum relation_stats_argnum
 
 static struct StatsArgInfo relarginfo[] =
 {
-	[RELATION_ARG] = {"relation", REGCLASSOID},
+	[RELSCHEMA_ARG] = {"schemaname", TEXTOID},
+	[RELNAME_ARG] = {"relname", TEXTOID},
 	[RELPAGES_ARG] = {"relpages", INT4OID},
 	[RELTUPLES_ARG] = {"reltuples", FLOAT4OID},
 	[RELALLVISIBLE_ARG] = {"relallvisible", INT4OID},
@@ -59,6 +64,9 @@ static bool
 relation_statistics_update(FunctionCallInfo fcinfo)
 {
 	bool		result = true;
+	char	   *nspname;
+	Oid			nspoid;
+	char	   *relname;
 	Oid			reloid;
 	Relation	crel;
 	BlockNumber relpages = 0;
@@ -76,6 +84,32 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 	bool		nulls[4] = {0};
 	int			nreplaces = 0;
 
+	stats_check_required_arg(fcinfo, relarginfo, RELSCHEMA_ARG);
+	stats_check_required_arg(fcinfo, relarginfo, RELNAME_ARG);
+
+	nspname = TextDatumGetCString(PG_GETARG_DATUM(RELSCHEMA_ARG));
+	nspoid = stats_schema_check_privileges(nspname);
+	if (!OidIsValid(nspoid))
+		return false;
+
+	relname = TextDatumGetCString(PG_GETARG_DATUM(RELNAME_ARG));
+	reloid = get_relname_relid(relname, nspoid);
+	if (!OidIsValid(reloid))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("Relation \"%s\".\"%s\" not found.", nspname, relname)));
+		return false;
+	}
+
+	if (RecoveryInProgress())
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("recovery is in progress"),
+				 errhint("Statistics cannot be modified during recovery.")));
+
+	stats_lock_check_privileges(reloid);
+
 	if (!PG_ARGISNULL(RELPAGES_ARG))
 	{
 		relpages = PG_GETARG_UINT32(RELPAGES_ARG);
@@ -108,17 +142,6 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 		update_relallfrozen = true;
 	}
 
-	stats_check_required_arg(fcinfo, relarginfo, RELATION_ARG);
-	reloid = PG_GETARG_OID(RELATION_ARG);
-
-	if (RecoveryInProgress())
-		ereport(ERROR,
-				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
-				 errmsg("recovery is in progress"),
-				 errhint("Statistics cannot be modified during recovery.")));
-
-	stats_lock_check_privileges(reloid);
-
 	/*
 	 * Take RowExclusiveLock on pg_class, consistent with
 	 * vac_update_relstats().
@@ -187,20 +210,22 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 Datum
 pg_clear_relation_stats(PG_FUNCTION_ARGS)
 {
-	LOCAL_FCINFO(newfcinfo, 5);
+	LOCAL_FCINFO(newfcinfo, 6);
 
-	InitFunctionCallInfoData(*newfcinfo, NULL, 5, InvalidOid, NULL, NULL);
+	InitFunctionCallInfoData(*newfcinfo, NULL, 6, InvalidOid, NULL, NULL);
 
-	newfcinfo->args[0].value = PG_GETARG_OID(0);
+	newfcinfo->args[0].value = PG_GETARG_DATUM(0);
 	newfcinfo->args[0].isnull = PG_ARGISNULL(0);
-	newfcinfo->args[1].value = UInt32GetDatum(0);
-	newfcinfo->args[1].isnull = false;
-	newfcinfo->args[2].value = Float4GetDatum(-1.0);
+	newfcinfo->args[1].value = PG_GETARG_DATUM(1);
+	newfcinfo->args[1].isnull = PG_ARGISNULL(1);
+	newfcinfo->args[2].value = UInt32GetDatum(0);
 	newfcinfo->args[2].isnull = false;
-	newfcinfo->args[3].value = UInt32GetDatum(0);
+	newfcinfo->args[3].value = Float4GetDatum(-1.0);
 	newfcinfo->args[3].isnull = false;
 	newfcinfo->args[4].value = UInt32GetDatum(0);
 	newfcinfo->args[4].isnull = false;
+	newfcinfo->args[5].value = UInt32GetDatum(0);
+	newfcinfo->args[5].isnull = false;
 
 	relation_statistics_update(newfcinfo);
 	PG_RETURN_VOID();
diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c
index 9647f5108b3..e037d4994e8 100644
--- a/src/backend/statistics/stat_utils.c
+++ b/src/backend/statistics/stat_utils.c
@@ -18,7 +18,9 @@
 
 #include "access/relation.h"
 #include "catalog/index.h"
+#include "catalog/namespace.h"
 #include "catalog/pg_database.h"
+#include "catalog/pg_namespace.h"
 #include "funcapi.h"
 #include "miscadmin.h"
 #include "statistics/stat_utils.h"
@@ -213,6 +215,41 @@ stats_lock_check_privileges(Oid reloid)
 	relation_close(table, NoLock);
 }
 
+
+/*
+ * Resolve a schema name into an Oid, ensure that the user has usage privs on
+ * that schema.
+ */
+Oid
+stats_schema_check_privileges(const char *nspname)
+{
+	Oid			nspoid;
+	AclResult	aclresult;
+
+	nspoid = get_namespace_oid(nspname, true);
+
+	if (nspoid == InvalidOid)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_SCHEMA_NAME),
+				 errmsg("schema %s does not exist", nspname)));
+		return InvalidOid;
+	}
+
+	aclresult = object_aclcheck(NamespaceRelationId, nspoid, GetUserId(), ACL_USAGE);
+
+	if (aclresult != ACLCHECK_OK)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for schema %s", nspname)));
+		return InvalidOid;
+	}
+
+	return nspoid;
+}
+
+
 /*
  * Find the argument number for the given argument name, returning -1 if not
  * found.
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index c371570501a..bd857bb076c 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -10490,7 +10490,6 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 	PQExpBuffer out;
 	DumpId	   *deps = NULL;
 	int			ndeps = 0;
-	char	   *qualified_name;
 	int			i_attname;
 	int			i_inherited;
 	int			i_null_frac;
@@ -10555,15 +10554,16 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 
 	out = createPQExpBuffer();
 
-	qualified_name = pg_strdup(fmtQualifiedDumpable(rsinfo));
-
 	/* restore relation stats */
 	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
 	appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
 					  fout->remoteVersion);
-	appendPQExpBufferStr(out, "\t'relation', ");
-	appendStringLiteralAH(out, qualified_name, fout);
-	appendPQExpBufferStr(out, "::regclass,\n");
+	appendPQExpBufferStr(out, "\t'schemaname', ");
+	appendStringLiteralAH(out, rsinfo->dobj.namespace->dobj.name, fout);
+	appendPQExpBufferStr(out, ",\n");
+	appendPQExpBufferStr(out, "\t'relname', ");
+	appendStringLiteralAH(out, rsinfo->dobj.name, fout);
+	appendPQExpBufferStr(out, ",\n");
 	appendPQExpBuffer(out, "\t'relpages', '%d'::integer,\n", rsinfo->relpages);
 	appendPQExpBuffer(out, "\t'reltuples', '%s'::real,\n", rsinfo->reltuples);
 	appendPQExpBuffer(out, "\t'relallvisible', '%d'::integer\n);\n",
@@ -10602,9 +10602,10 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
 		appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
 						  fout->remoteVersion);
-		appendPQExpBufferStr(out, "\t'relation', ");
-		appendStringLiteralAH(out, qualified_name, fout);
-		appendPQExpBufferStr(out, "::regclass");
+		appendPQExpBufferStr(out, "\t'schemaname', ");
+		appendStringLiteralAH(out, rsinfo->dobj.namespace->dobj.name, fout);
+		appendPQExpBufferStr(out, ",\n\t'relname', ");
+		appendStringLiteralAH(out, rsinfo->dobj.name, fout);
 
 		if (PQgetisnull(res, rownum, i_attname))
 			pg_fatal("attname cannot be NULL");
@@ -10616,7 +10617,10 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 		 * their attnames are not necessarily stable across dump/reload.
 		 */
 		if (rsinfo->nindAttNames == 0)
-			appendNamedArgument(out, fout, "attname", "name", attname);
+		{
+			appendPQExpBuffer(out, ",\n\t'attname', ");
+			appendStringLiteralAH(out, attname, fout);
+		}
 		else
 		{
 			bool		found = false;
@@ -10696,7 +10700,6 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 							  .deps = deps,
 							  .nDeps = ndeps));
 
-	free(qualified_name);
 	destroyPQExpBuffer(out);
 	destroyPQExpBuffer(query);
 }
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index c7bffc1b045..b037f239136 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -4725,14 +4725,16 @@ my %tests = (
 		regexp => qr/^
 			\QSELECT * FROM pg_catalog.pg_restore_relation_stats(\E\s+
 			'version',\s'\d+'::integer,\s+
-			'relation',\s'dump_test.dup_test_post_data_ix'::regclass,\s+
+			'schemaname',\s'dump_test',\s+
+			'relname',\s'dup_test_post_data_ix',\s+
 			'relpages',\s'\d+'::integer,\s+
 			'reltuples',\s'\d+'::real,\s+
 			'relallvisible',\s'\d+'::integer\s+
 			\);\s+
 			\QSELECT * FROM pg_catalog.pg_restore_attribute_stats(\E\s+
 			'version',\s'\d+'::integer,\s+
-			'relation',\s'dump_test.dup_test_post_data_ix'::regclass,\s+
+			'schemaname',\s'dump_test',\s+
+			'relname',\s'dup_test_post_data_ix',\s+
 			'attnum',\s'2'::smallint,\s+
 			'inherited',\s'f'::boolean,\s+
 			'null_frac',\s'0'::real,\s+
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index 1f46d5e7854..2f1295f2149 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -14,7 +14,8 @@ CREATE TABLE stats_import.test(
 ) WITH (autovacuum_enabled = false);
 SELECT
     pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', 18::integer,
         'reltuples', 21::real,
         'relallvisible', 24::integer,
@@ -36,7 +37,7 @@ ORDER BY relname;
  test    |       18 |        21 |            24 |           27
 (1 row)
 
-SELECT pg_clear_relation_stats('stats_import.test'::regclass);
+SELECT pg_clear_relation_stats('stats_import', 'test');
  pg_clear_relation_stats 
 -------------------------
  
@@ -45,33 +46,54 @@ SELECT pg_clear_relation_stats('stats_import.test'::regclass);
 --
 -- relstats tests
 --
---- error: relation is wrong type
+-- error: schemaname missing
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::oid,
+        'relname', 'test',
         'relpages', 17::integer);
-WARNING:  argument "relation" has type "oid", expected type "regclass"
-ERROR:  "relation" cannot be NULL
+ERROR:  "schemaname" cannot be NULL
+-- error: relname missing
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relpages', 17::integer);
+ERROR:  "relname" cannot be NULL
+--- error: schemaname is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 3.6::float,
+        'relname', 'test',
+        'relpages', 17::integer);
+WARNING:  argument "schemaname" has type "double precision", expected type "text"
+ERROR:  "schemaname" cannot be NULL
+--- error: relname is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relname', 0::oid,
+        'relpages', 17::integer);
+WARNING:  argument "relname" has type "oid", expected type "text"
+ERROR:  "relname" cannot be NULL
 -- error: relation not found
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::oid::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'nope',
         'relpages', 17::integer);
-ERROR:  could not open relation with OID 0
+WARNING:  Relation "stats_import"."nope" not found.
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
 -- error: odd number of variadic arguments cannot be pairs
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relallvisible');
 ERROR:  variadic arguments must be name/value pairs
 HINT:  Provide an even number of variadic arguments that can be divided into pairs.
 -- error: argument name is NULL
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         NULL, '17'::integer);
-ERROR:  name at variadic position 3 is NULL
--- error: argument name is not a text type
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        17, '17'::integer);
-ERROR:  name at variadic position 3 has type "integer", expected type "text"
+ERROR:  name at variadic position 5 is NULL
 -- starting stats
 SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
@@ -84,7 +106,8 @@ WHERE oid = 'stats_import.test_i'::regclass;
 -- regular indexes have special case locking rules
 BEGIN;
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.test_i'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test_i',
         'relpages', 18::integer);
  pg_restore_relation_stats 
 ---------------------------
@@ -132,7 +155,8 @@ WHERE oid = 'stats_import.part_parent'::regclass;
 --
 BEGIN;
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent_i'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'part_parent_i',
         'relpages', 2::integer);
  pg_restore_relation_stats 
 ---------------------------
@@ -166,7 +190,8 @@ WHERE oid = 'stats_import.part_parent_i'::regclass;
 
 -- ok: set all relstats, with version, no bounds checking
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'version', 150000::integer,
         'relpages', '-17'::integer,
         'reltuples', 400::real,
@@ -187,7 +212,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just relpages, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', '16'::integer);
  pg_restore_relation_stats 
 ---------------------------
@@ -204,7 +230,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just reltuples, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'reltuples', '500'::real);
  pg_restore_relation_stats 
 ---------------------------
@@ -221,7 +248,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just relallvisible, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relallvisible', 5::integer);
  pg_restore_relation_stats 
 ---------------------------
@@ -238,7 +266,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: just relallfrozen
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'version', 150000::integer,
         'relallfrozen', 3::integer);
  pg_restore_relation_stats 
@@ -256,7 +285,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- warn: bad relpages type, rest updated
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', 'nope'::text,
         'reltuples', 400.0::real,
         'relallvisible', 4::integer,
@@ -277,7 +307,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- unrecognized argument name, rest ok
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', '171'::integer,
         'nope', 10::integer);
 WARNING:  unrecognized argument name: "nope"
@@ -295,8 +326,7 @@ WHERE oid = 'stats_import.test'::regclass;
 (1 row)
 
 -- ok: clear stats
-SELECT pg_catalog.pg_clear_relation_stats(
-    relation => 'stats_import.test'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'test');
  pg_clear_relation_stats 
 -------------------------
  
@@ -313,87 +343,123 @@ WHERE oid = 'stats_import.test'::regclass;
 -- invalid relkinds for statistics
 CREATE SEQUENCE stats_import.testseq;
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.testseq'::regclass);
+        'schemaname', 'stats_import',
+        'relname', 'testseq');
 ERROR:  cannot modify statistics for relation "testseq"
 DETAIL:  This operation is not supported for sequences.
-SELECT pg_catalog.pg_clear_relation_stats(
-        'stats_import.testseq'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testseq');
 ERROR:  cannot modify statistics for relation "testseq"
 DETAIL:  This operation is not supported for sequences.
 CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
-SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.testview'::regclass);
-ERROR:  cannot modify statistics for relation "testview"
-DETAIL:  This operation is not supported for views.
-SELECT pg_catalog.pg_clear_relation_stats(
-        'stats_import.testview'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testview');
 ERROR:  cannot modify statistics for relation "testview"
 DETAIL:  This operation is not supported for views.
 --
 -- attribute stats
 --
--- error: object does not exist
+-- error: schemaname missing
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', '0'::oid::regclass,
-    'attname', 'id'::name,
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  could not open relation with OID 0
--- error: relation null
+ERROR:  "schemaname" cannot be NULL
+-- error: schema does not exist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', NULL::oid::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'nope',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  "relation" cannot be NULL
+WARNING:  schema nope does not exist
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- error: relname missing
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+ERROR:  "relname" cannot be NULL
+-- error: relname does not exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'relname', 'nope',
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+WARNING:  Relation "stats_import"."nope" not found.
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- error: relname null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'relname', NULL,
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+ERROR:  "relname" cannot be NULL
 -- error: NULL attname
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', NULL::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', NULL,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 ERROR:  must specify either attname or attnum
 -- error: attname doesn't exist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'nope'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'nope',
     'inherited', false::boolean,
     'null_frac', 0.1::real,
     'avg_width', 2::integer,
     'n_distinct', 0.3::real);
-ERROR:  column "nope" of relation "test" does not exist
+ERROR:  column "nope" of relation "stats_import"."test" does not exist
 -- error: both attname and attnum
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'attnum', 1::smallint,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 ERROR:  cannot specify both attname and attnum
 -- error: neither attname nor attnum
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
+    'schemaname', 'stats_import',
+    'relname', 'test',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 ERROR:  must specify either attname or attnum
 -- error: attribute is system column
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'xmin'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'xmin',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 ERROR:  cannot modify statistics on system column "xmin"
 -- error: inherited null
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', NULL::boolean,
     'null_frac', 0.1::real);
 ERROR:  "inherited" cannot be NULL
 -- ok: just the fixed values, with version, no stakinds
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'version', 150000::integer,
     'null_frac', 0.2::real,
@@ -421,7 +487,8 @@ AND attname = 'id';
 -- for any stat-having relation.
 --
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
+    'schemaname', 'stats_import',
+    'relname', 'test',
     'attnum', 1::smallint,
     'inherited', false::boolean,
     'null_frac', 0.4::real);
@@ -443,8 +510,9 @@ AND attname = 'id';
 
 -- warn: unrecognized argument name, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.2::real,
     'nope', 0.5::real);
@@ -467,8 +535,9 @@ AND attname = 'id';
 
 -- warn: mcv / mcf null mismatch part 1, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.21::real,
     'most_common_freqs', '{0.1,0.2,0.3}'::real[]
@@ -492,8 +561,9 @@ AND attname = 'id';
 
 -- warn: mcv / mcf null mismatch part 2, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.21::real,
     'most_common_vals', '{1,2,3}'::text
@@ -517,8 +587,9 @@ AND attname = 'id';
 
 -- warn: mcf type mismatch, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.22::real,
     'most_common_vals', '{2,1,3}'::text,
@@ -544,8 +615,9 @@ AND attname = 'id';
 
 -- warn: mcv cast failure, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.23::real,
     'most_common_vals', '{2,four,3}'::text,
@@ -570,8 +642,9 @@ AND attname = 'id';
 
 -- ok: mcv+mcf
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'most_common_vals', '{2,1,3}'::text,
     'most_common_freqs', '{0.3,0.25,0.05}'::real[]
@@ -594,8 +667,9 @@ AND attname = 'id';
 
 -- warn: NULL in histogram array, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.24::real,
     'histogram_bounds', '{1,NULL,3,4}'::text
@@ -619,8 +693,9 @@ AND attname = 'id';
 
 -- ok: histogram_bounds
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'histogram_bounds', '{1,2,3,4}'::text
     );
@@ -642,8 +717,9 @@ AND attname = 'id';
 
 -- warn: elem_count_histogram null element, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.25::real,
     'elem_count_histogram', '{1,1,NULL,1,1,1,1,1}'::real[]
@@ -667,8 +743,9 @@ AND attname = 'tags';
 
 -- ok: elem_count_histogram
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.26::real,
     'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
@@ -691,8 +768,9 @@ AND attname = 'tags';
 
 -- warn: range stats on a scalar type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.27::real,
     'range_empty_frac', 0.5::real,
@@ -718,8 +796,9 @@ AND attname = 'id';
 
 -- warn: range_empty_frac range_length_hist null mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.28::real,
     'range_length_histogram', '{399,499,Infinity}'::text
@@ -743,8 +822,9 @@ AND attname = 'arange';
 
 -- warn: range_empty_frac range_length_hist null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.29::real,
     'range_empty_frac', 0.5::real
@@ -768,8 +848,9 @@ AND attname = 'arange';
 
 -- ok: range_empty_frac + range_length_hist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'range_empty_frac', 0.5::real,
     'range_length_histogram', '{399,499,Infinity}'::text
@@ -792,8 +873,9 @@ AND attname = 'arange';
 
 -- warn: range bounds histogram on scalar, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.31::real,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
@@ -818,8 +900,9 @@ AND attname = 'id';
 
 -- ok: range_bounds_histogram
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
     );
@@ -841,8 +924,9 @@ AND attname = 'arange';
 
 -- warn: cannot set most_common_elems for range type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.32::real,
     'most_common_elems', '{3,1}'::text,
@@ -868,8 +952,9 @@ AND attname = 'arange';
 
 -- warn: scalars can't have mcelem, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.33::real,
     'most_common_elems', '{1,3}'::text,
@@ -895,8 +980,9 @@ AND attname = 'id';
 
 -- warn: mcelem / mcelem mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.34::real,
     'most_common_elems', '{one,two}'::text
@@ -920,8 +1006,9 @@ AND attname = 'tags';
 
 -- warn: mcelem / mcelem null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.35::real,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
@@ -945,8 +1032,9 @@ AND attname = 'tags';
 
 -- ok: mcelem
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'most_common_elems', '{one,three}'::text,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
@@ -969,8 +1057,9 @@ AND attname = 'tags';
 
 -- warn: scalars can't have elem_count_histogram, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.36::real,
     'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1}'::real[]
@@ -1022,8 +1111,9 @@ SELECT s.schemaname, s.tablename, s.attname, s.inherited, r.*
 FROM pg_catalog.pg_stats AS s
 CROSS JOIN LATERAL
     pg_catalog.pg_restore_attribute_stats(
-        'relation', ('stats_import.' || s.tablename || '_clone')::regclass,
-        'attname', s.attname,
+        'schemaname', 'stats_import',
+        'relname', s.tablename::text || '_clone',
+        'attname', s.attname::text,
         'inherited', s.inherited,
         'version', 150000,
         'null_frac', s.null_frac,
@@ -1200,9 +1290,10 @@ AND attname = 'arange';
 (1 row)
 
 SELECT pg_catalog.pg_clear_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'arange'::name,
-    inherited => false::boolean);
+    schemaname => 'stats_import',
+    relname => 'test',
+    attname => 'arange',
+    inherited => false);
  pg_clear_attribute_stats 
 --------------------------
  
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index 0ec590688c2..ccdc44e9236 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -17,7 +17,8 @@ CREATE TABLE stats_import.test(
 
 SELECT
     pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', 18::integer,
         'reltuples', 21::real,
         'relallvisible', 24::integer,
@@ -32,37 +33,52 @@ FROM pg_class
 WHERE oid = 'stats_import.test'::regclass
 ORDER BY relname;
 
-SELECT pg_clear_relation_stats('stats_import.test'::regclass);
+SELECT pg_clear_relation_stats('stats_import', 'test');
 
 --
 -- relstats tests
 --
 
---- error: relation is wrong type
+-- error: schemaname missing
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::oid,
+        'relname', 'test',
+        'relpages', 17::integer);
+
+-- error: relname missing
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relpages', 17::integer);
+
+--- error: schemaname is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 3.6::float,
+        'relname', 'test',
+        'relpages', 17::integer);
+
+--- error: relname is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relname', 0::oid,
         'relpages', 17::integer);
 
 -- error: relation not found
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::oid::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'nope',
         'relpages', 17::integer);
 
 -- error: odd number of variadic arguments cannot be pairs
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relallvisible');
 
 -- error: argument name is NULL
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         NULL, '17'::integer);
 
--- error: argument name is not a text type
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        17, '17'::integer);
-
 -- starting stats
 SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
@@ -71,7 +87,8 @@ WHERE oid = 'stats_import.test_i'::regclass;
 -- regular indexes have special case locking rules
 BEGIN;
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.test_i'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test_i',
         'relpages', 18::integer);
 
 SELECT mode FROM pg_locks
@@ -108,7 +125,8 @@ WHERE oid = 'stats_import.part_parent'::regclass;
 BEGIN;
 
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent_i'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'part_parent_i',
         'relpages', 2::integer);
 
 SELECT mode FROM pg_locks
@@ -127,7 +145,8 @@ WHERE oid = 'stats_import.part_parent_i'::regclass;
 
 -- ok: set all relstats, with version, no bounds checking
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'version', 150000::integer,
         'relpages', '-17'::integer,
         'reltuples', 400::real,
@@ -140,7 +159,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just relpages, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', '16'::integer);
 
 SELECT relpages, reltuples, relallvisible, relallfrozen
@@ -149,7 +169,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just reltuples, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'reltuples', '500'::real);
 
 SELECT relpages, reltuples, relallvisible, relallfrozen
@@ -158,7 +179,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just relallvisible, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relallvisible', 5::integer);
 
 SELECT relpages, reltuples, relallvisible, relallfrozen
@@ -167,7 +189,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: just relallfrozen
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'version', 150000::integer,
         'relallfrozen', 3::integer);
 
@@ -177,7 +200,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- warn: bad relpages type, rest updated
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', 'nope'::text,
         'reltuples', 400.0::real,
         'relallvisible', 4::integer,
@@ -189,7 +213,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- unrecognized argument name, rest ok
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', '171'::integer,
         'nope', 10::integer);
 
@@ -198,8 +223,7 @@ FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: clear stats
-SELECT pg_catalog.pg_clear_relation_stats(
-    relation => 'stats_import.test'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'test');
 
 SELECT relpages, reltuples, relallvisible
 FROM pg_class
@@ -209,48 +233,70 @@ WHERE oid = 'stats_import.test'::regclass;
 CREATE SEQUENCE stats_import.testseq;
 
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.testseq'::regclass);
+        'schemaname', 'stats_import',
+        'relname', 'testseq');
 
-SELECT pg_catalog.pg_clear_relation_stats(
-        'stats_import.testseq'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testseq');
 
 CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
 
-SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.testview'::regclass);
-
-SELECT pg_catalog.pg_clear_relation_stats(
-        'stats_import.testview'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testview');
 
 --
 -- attribute stats
 --
 
--- error: object does not exist
+-- error: schemaname missing
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', '0'::oid::regclass,
-    'attname', 'id'::name,
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: relation null
+-- error: schema does not exist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', NULL::oid::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'nope',
+    'relname', 'test',
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: relname missing
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: relname does not exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'relname', 'nope',
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: relname null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'relname', NULL,
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
 -- error: NULL attname
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', NULL::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', NULL,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
 -- error: attname doesn't exist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'nope'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'nope',
     'inherited', false::boolean,
     'null_frac', 0.1::real,
     'avg_width', 2::integer,
@@ -258,36 +304,41 @@ SELECT pg_catalog.pg_restore_attribute_stats(
 
 -- error: both attname and attnum
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'attnum', 1::smallint,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
 -- error: neither attname nor attnum
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
+    'schemaname', 'stats_import',
+    'relname', 'test',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
 -- error: attribute is system column
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'xmin'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'xmin',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
 -- error: inherited null
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', NULL::boolean,
     'null_frac', 0.1::real);
 
 -- ok: just the fixed values, with version, no stakinds
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'version', 150000::integer,
     'null_frac', 0.2::real,
@@ -307,7 +358,8 @@ AND attname = 'id';
 -- for any stat-having relation.
 --
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
+    'schemaname', 'stats_import',
+    'relname', 'test',
     'attnum', 1::smallint,
     'inherited', false::boolean,
     'null_frac', 0.4::real);
@@ -321,8 +373,9 @@ AND attname = 'id';
 
 -- warn: unrecognized argument name, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.2::real,
     'nope', 0.5::real);
@@ -336,8 +389,9 @@ AND attname = 'id';
 
 -- warn: mcv / mcf null mismatch part 1, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.21::real,
     'most_common_freqs', '{0.1,0.2,0.3}'::real[]
@@ -352,8 +406,9 @@ AND attname = 'id';
 
 -- warn: mcv / mcf null mismatch part 2, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.21::real,
     'most_common_vals', '{1,2,3}'::text
@@ -368,8 +423,9 @@ AND attname = 'id';
 
 -- warn: mcf type mismatch, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.22::real,
     'most_common_vals', '{2,1,3}'::text,
@@ -385,8 +441,9 @@ AND attname = 'id';
 
 -- warn: mcv cast failure, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.23::real,
     'most_common_vals', '{2,four,3}'::text,
@@ -402,8 +459,9 @@ AND attname = 'id';
 
 -- ok: mcv+mcf
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'most_common_vals', '{2,1,3}'::text,
     'most_common_freqs', '{0.3,0.25,0.05}'::real[]
@@ -418,8 +476,9 @@ AND attname = 'id';
 
 -- warn: NULL in histogram array, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.24::real,
     'histogram_bounds', '{1,NULL,3,4}'::text
@@ -434,8 +493,9 @@ AND attname = 'id';
 
 -- ok: histogram_bounds
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'histogram_bounds', '{1,2,3,4}'::text
     );
@@ -449,8 +509,9 @@ AND attname = 'id';
 
 -- warn: elem_count_histogram null element, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.25::real,
     'elem_count_histogram', '{1,1,NULL,1,1,1,1,1}'::real[]
@@ -465,8 +526,9 @@ AND attname = 'tags';
 
 -- ok: elem_count_histogram
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.26::real,
     'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
@@ -481,8 +543,9 @@ AND attname = 'tags';
 
 -- warn: range stats on a scalar type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.27::real,
     'range_empty_frac', 0.5::real,
@@ -498,8 +561,9 @@ AND attname = 'id';
 
 -- warn: range_empty_frac range_length_hist null mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.28::real,
     'range_length_histogram', '{399,499,Infinity}'::text
@@ -514,8 +578,9 @@ AND attname = 'arange';
 
 -- warn: range_empty_frac range_length_hist null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.29::real,
     'range_empty_frac', 0.5::real
@@ -530,8 +595,9 @@ AND attname = 'arange';
 
 -- ok: range_empty_frac + range_length_hist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'range_empty_frac', 0.5::real,
     'range_length_histogram', '{399,499,Infinity}'::text
@@ -546,8 +612,9 @@ AND attname = 'arange';
 
 -- warn: range bounds histogram on scalar, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.31::real,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
@@ -562,8 +629,9 @@ AND attname = 'id';
 
 -- ok: range_bounds_histogram
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
     );
@@ -577,8 +645,9 @@ AND attname = 'arange';
 
 -- warn: cannot set most_common_elems for range type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.32::real,
     'most_common_elems', '{3,1}'::text,
@@ -594,8 +663,9 @@ AND attname = 'arange';
 
 -- warn: scalars can't have mcelem, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.33::real,
     'most_common_elems', '{1,3}'::text,
@@ -611,8 +681,9 @@ AND attname = 'id';
 
 -- warn: mcelem / mcelem mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.34::real,
     'most_common_elems', '{one,two}'::text
@@ -627,8 +698,9 @@ AND attname = 'tags';
 
 -- warn: mcelem / mcelem null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.35::real,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
@@ -643,8 +715,9 @@ AND attname = 'tags';
 
 -- ok: mcelem
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'most_common_elems', '{one,three}'::text,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
@@ -659,8 +732,9 @@ AND attname = 'tags';
 
 -- warn: scalars can't have elem_count_histogram, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.36::real,
     'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1}'::real[]
@@ -707,8 +781,9 @@ SELECT s.schemaname, s.tablename, s.attname, s.inherited, r.*
 FROM pg_catalog.pg_stats AS s
 CROSS JOIN LATERAL
     pg_catalog.pg_restore_attribute_stats(
-        'relation', ('stats_import.' || s.tablename || '_clone')::regclass,
-        'attname', s.attname,
+        'schemaname', 'stats_import',
+        'relname', s.tablename::text || '_clone',
+        'attname', s.attname::text,
         'inherited', s.inherited,
         'version', 150000,
         'null_frac', s.null_frac,
@@ -853,9 +928,10 @@ AND inherited = false
 AND attname = 'arange';
 
 SELECT pg_catalog.pg_clear_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'arange'::name,
-    inherited => false::boolean);
+    schemaname => 'stats_import',
+    relname => 'test',
+    attname => 'arange',
+    inherited => false);
 
 SELECT COUNT(*)
 FROM pg_stats
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 1c3810e1a04..a75e95bc5fd 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30365,22 +30365,24 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          <structname>mytable</structname>:
 <programlisting>
  SELECT pg_restore_relation_stats(
-    'relation',  'mytable'::regclass,
-    'relpages',  173::integer,
-    'reltuples', 10000::real);
+    'schemaname', 'myschema',
+    'relname',    'mytable',
+    'relpages',   173::integer,
+    'reltuples',  10000::real);
 </programlisting>
         </para>
         <para>
-         The argument <literal>relation</literal> with a value of type
-         <type>regclass</type> is required, and specifies the table. Other
-         arguments are the names and values of statistics corresponding to
-         certain columns in <link
+         The arguments <literal>schemaname</literal> with a value of type
+         <type>regclass</type> and <literal>relname</literal> are required,
+         and specifies the table. Other arguments are the names and values
+         of statistics corresponding to certain columns in <link
          linkend="catalog-pg-class"><structname>pg_class</structname></link>.
          The currently-supported relation statistics are
          <literal>relpages</literal> with a value of type
          <type>integer</type>, <literal>reltuples</literal> with a value of
-         type <type>real</type>, and <literal>relallvisible</literal> with a
-         value of type <type>integer</type>.
+         type <type>real</type>, <literal>relallvisible</literal> with a
+         value of type <type>integer</type>, and <literal>relallfrozen</literal>
+         with a value of type <type>integer</type>.
         </para>
         <para>
          Additionally, this function accepts argument name
@@ -30408,7 +30410,7 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          <indexterm>
           <primary>pg_clear_relation_stats</primary>
          </indexterm>
-         <function>pg_clear_relation_stats</function> ( <parameter>relation</parameter> <type>regclass</type> )
+         <function>pg_clear_relation_stats</function> ( <parameter>schemaname</parameter> <type>text</type>, <parameter>relname</parameter> <type>text</type> )
          <returnvalue>void</returnvalue>
         </para>
         <para>
@@ -30457,16 +30459,18 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          <structname>mytable</structname>:
 <programlisting>
  SELECT pg_restore_attribute_stats(
-    'relation',    'mytable'::regclass,
-    'attname',     'col1'::name,
-    'inherited',   false,
-    'avg_width',   125::integer,
-    'null_frac',   0.5::real);
+    'schemaname', 'myschema',
+    'relname',    'mytable',
+    'attname',    'col1',
+    'inherited',  false,
+    'avg_width',  125::integer,
+    'null_frac',  0.5::real);
 </programlisting>
         </para>
         <para>
-         The required arguments are <literal>relation</literal> with a value
-         of type <type>regclass</type>, which specifies the table; either
+         The required arguments are <literal>schemaname</literal> with a value
+         of type <type>regclass</type> and <literal>relname</literal> with a value
+         of type <type>text</type> which specify the table; either
          <literal>attname</literal> with a value of type <type>name</type> or
          <literal>attnum</literal> with a value of type <type>smallint</type>,
          which specifies the column; and <literal>inherited</literal>, which
@@ -30502,7 +30506,8 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
           <primary>pg_clear_attribute_stats</primary>
          </indexterm>
          <function>pg_clear_attribute_stats</function> (
-         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>schemaname</parameter> <type>text</type>,
+         <parameter>relname</parameter> <type>text</type>,
          <parameter>attname</parameter> <type>name</type>,
          <parameter>inherited</parameter> <type>boolean</type> )
          <returnvalue>void</returnvalue>

base-commit: 5eabd91a83adae75f53b61857343660919fef4c7
-- 
2.48.1

v9-0002-Downgrade-as-man-pg_restore_-_stats-errors-to-war.patchtext/x-patch; charset=US-ASCII; name=v9-0002-Downgrade-as-man-pg_restore_-_stats-errors-to-war.patchDownload
From b5b96d4a2fe4119ef26689cc8f3e1a5b8d24bdda Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 8 Mar 2025 00:52:41 -0500
Subject: [PATCH v9 2/5] Downgrade as man pg_restore_*_stats errors to
 warnings.

We want to avoid errors that can potentially stop an otherwise
successful pg_upgrade or pg_restore operation. With that in mind, change
as many ERROR reports to WARNING + early termination with no data
updated.
---
 src/include/statistics/stat_utils.h        |   4 +-
 src/backend/statistics/attribute_stats.c   | 124 +++++++++++-----
 src/backend/statistics/relation_stats.c    |  10 +-
 src/backend/statistics/stat_utils.c        |  51 +++++--
 src/test/regress/expected/stats_import.out | 163 ++++++++++++++++-----
 src/test/regress/sql/stats_import.sql      |  36 ++---
 6 files changed, 277 insertions(+), 111 deletions(-)

diff --git a/src/include/statistics/stat_utils.h b/src/include/statistics/stat_utils.h
index cad042c8e4a..298cbae3436 100644
--- a/src/include/statistics/stat_utils.h
+++ b/src/include/statistics/stat_utils.h
@@ -21,7 +21,7 @@ struct StatsArgInfo
 	Oid			argtype;
 };
 
-extern void stats_check_required_arg(FunctionCallInfo fcinfo,
+extern bool stats_check_required_arg(FunctionCallInfo fcinfo,
 									 struct StatsArgInfo *arginfo,
 									 int argnum);
 extern bool stats_check_arg_array(FunctionCallInfo fcinfo,
@@ -30,7 +30,7 @@ extern bool stats_check_arg_pair(FunctionCallInfo fcinfo,
 								 struct StatsArgInfo *arginfo,
 								 int argnum1, int argnum2);
 
-extern void stats_lock_check_privileges(Oid reloid);
+extern bool stats_lock_check_privileges(Oid reloid);
 
 extern Oid stats_schema_check_privileges(const char *nspname);
 
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index f87db2d6102..4f9bc18f8c6 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -100,7 +100,7 @@ static struct StatsArgInfo cleararginfo[] =
 
 static bool attribute_statistics_update(FunctionCallInfo fcinfo);
 static Node *get_attr_expr(Relation rel, int attnum);
-static void get_attr_stat_type(Oid reloid, AttrNumber attnum,
+static bool get_attr_stat_type(Oid reloid, AttrNumber attnum,
 							   Oid *atttypid, int32 *atttypmod,
 							   char *atttyptype, Oid *atttypcoll,
 							   Oid *eq_opr, Oid *lt_opr);
@@ -129,10 +129,12 @@ static void init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
  * stored as an anyarray, and the representation of the array needs to store
  * the correct element type, which must be derived from the attribute.
  *
- * Major errors, such as the table not existing, the attribute not existing,
- * or a permissions failure are always reported at ERROR. Other errors, such
- * as a conversion failure on one statistic kind, are reported as a WARNING
- * and other statistic kinds may still be updated.
+ * This function is called during database upgrades and restorations, therefore
+ * it is imperative to avoid ERRORs that could potentially end the upgrade or
+ * restore unless. Major errors, such as the table not existing, the attribute
+ * not existing, or permissions failure are reported as WARNINGs with an end to
+ * the function, thus allowing the upgrade/restore to continue, but without the
+ * stats that can be regenereated once the database is online again.
  */
 static bool
 attribute_statistics_update(FunctionCallInfo fcinfo)
@@ -149,8 +151,8 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	HeapTuple	statup;
 
 	Oid			atttypid = InvalidOid;
-	int32		atttypmod;
-	char		atttyptype;
+	int32		atttypmod = -1;
+	char		atttyptype = TYPTYPE_PSEUDO; /* Not a great default, but there is no TYPTYPE_INVALID */
 	Oid			atttypcoll = InvalidOid;
 	Oid			eq_opr = InvalidOid;
 	Oid			lt_opr = InvalidOid;
@@ -177,17 +179,19 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 	bool		result = true;
 
-	stats_check_required_arg(fcinfo, attarginfo, ATTRELSCHEMA_ARG);
-	stats_check_required_arg(fcinfo, attarginfo, ATTRELNAME_ARG);
+	if (!stats_check_required_arg(fcinfo, attarginfo, ATTRELSCHEMA_ARG))
+		return false;
+	if (!stats_check_required_arg(fcinfo, attarginfo, ATTRELNAME_ARG))
+		return false;
 
 	nspname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELSCHEMA_ARG));
 	nspoid = stats_schema_check_privileges(nspname);
-	if (nspoid == InvalidOid)
+	if (!OidIsValid(nspoid))
 		return false;
 
 	relname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELNAME_ARG));
 	reloid = get_relname_relid(relname, nspoid);
-	if (reloid == InvalidOid)
+	if (!OidIsValid(reloid))
 	{
 		ereport(WARNING,
 				(errcode(ERRCODE_UNDEFINED_OBJECT),
@@ -196,29 +200,39 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	}
 
 	if (RecoveryInProgress())
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("recovery is in progress"),
 				 errhint("Statistics cannot be modified during recovery.")));
+		return false;
+	}
 
 	/* lock before looking up attribute */
-	stats_lock_check_privileges(reloid);
+	if (!stats_lock_check_privileges(reloid))
+		return false;
 
 	/* user can specify either attname or attnum, but not both */
 	if (!PG_ARGISNULL(ATTNAME_ARG))
 	{
 		if (!PG_ARGISNULL(ATTNUM_ARG))
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("cannot specify both attname and attnum")));
+			return false;
+		}
 		attname = TextDatumGetCString(PG_GETARG_DATUM(ATTNAME_ARG));
 		attnum = get_attnum(reloid, attname);
 		/* note that this test covers attisdropped cases too: */
 		if (attnum == InvalidAttrNumber)
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 					(errcode(ERRCODE_UNDEFINED_COLUMN),
 					 errmsg("column \"%s\" of relation \"%s\".\"%s\" does not exist",
 							attname, nspname, relname)));
+			return false;
+		}
 	}
 	else if (!PG_ARGISNULL(ATTNUM_ARG))
 	{
@@ -227,27 +241,33 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 		/* annoyingly, get_attname doesn't check attisdropped */
 		if (attname == NULL ||
 			!SearchSysCacheExistsAttName(reloid, attname))
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 					(errcode(ERRCODE_UNDEFINED_COLUMN),
 					 errmsg("column %d of relation \"%s\".\"%s\" does not exist",
 							attnum, nspname, relname)));
+			return false;
+		}
 	}
 	else
 	{
-		ereport(ERROR,
+		ereport(WARNING,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("must specify either attname or attnum")));
-		attname = NULL;			/* keep compiler quiet */
-		attnum = 0;
+		return false;
 	}
 
 	if (attnum < 0)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot modify statistics on system column \"%s\"",
 						attname)));
+		return false;
+	}
 
-	stats_check_required_arg(fcinfo, attarginfo, INHERITED_ARG);
+	if (!stats_check_required_arg(fcinfo, attarginfo, INHERITED_ARG))
+		return false;
 	inherited = PG_GETARG_BOOL(INHERITED_ARG);
 
 	/*
@@ -296,10 +316,11 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	}
 
 	/* derive information from attribute */
-	get_attr_stat_type(reloid, attnum,
-					   &atttypid, &atttypmod,
-					   &atttyptype, &atttypcoll,
-					   &eq_opr, &lt_opr);
+	if (!get_attr_stat_type(reloid, attnum,
+							&atttypid, &atttypmod,
+							&atttyptype, &atttypcoll,
+							&eq_opr, &lt_opr))
+		result = false;
 
 	/* if needed, derive element type */
 	if (do_mcelem || do_dechist)
@@ -579,7 +600,7 @@ get_attr_expr(Relation rel, int attnum)
 /*
  * Derive type information from the attribute.
  */
-static void
+static bool
 get_attr_stat_type(Oid reloid, AttrNumber attnum,
 				   Oid *atttypid, int32 *atttypmod,
 				   char *atttyptype, Oid *atttypcoll,
@@ -596,18 +617,26 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum,
 
 	/* Attribute not found */
 	if (!HeapTupleIsValid(atup))
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 errmsg("attribute %d of relation \"%s\" does not exist",
 						attnum, RelationGetRelationName(rel))));
+		relation_close(rel, NoLock);
+		return false;
+	}
 
 	attr = (Form_pg_attribute) GETSTRUCT(atup);
 
 	if (attr->attisdropped)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 errmsg("attribute %d of relation \"%s\" does not exist",
 						attnum, RelationGetRelationName(rel))));
+		relation_close(rel, NoLock);
+		return false;
+	}
 
 	expr = get_attr_expr(rel, attr->attnum);
 
@@ -656,6 +685,7 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum,
 		*atttypcoll = DEFAULT_COLLATION_OID;
 
 	relation_close(rel, NoLock);
+	return true;
 }
 
 /*
@@ -781,6 +811,10 @@ set_stats_slot(Datum *values, bool *nulls, bool *replaces,
 	if (slotidx >= STATISTIC_NUM_SLOTS && first_empty >= 0)
 		slotidx = first_empty;
 
+	/*
+	 * Currently there is no datatype that can have more than STATISTIC_NUM_SLOTS
+	 * statistic kinds, so this can safely remain an ERROR for now.
+	 */
 	if (slotidx >= STATISTIC_NUM_SLOTS)
 		ereport(ERROR,
 				(errmsg("maximum number of statistics slots exceeded: %d",
@@ -927,15 +961,19 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 	AttrNumber	attnum;
 	bool		inherited;
 
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELSCHEMA_ARG);
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELNAME_ARG);
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTNAME_ARG);
-	stats_check_required_arg(fcinfo, cleararginfo, C_INHERITED_ARG);
+	if (!stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELSCHEMA_ARG))
+		PG_RETURN_VOID();
+	if (!stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELNAME_ARG))
+		PG_RETURN_VOID();
+	if (!stats_check_required_arg(fcinfo, cleararginfo, C_ATTNAME_ARG))
+		PG_RETURN_VOID();
+	if (!stats_check_required_arg(fcinfo, cleararginfo, C_INHERITED_ARG))
+		PG_RETURN_VOID();
 
 	nspname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELSCHEMA_ARG));
 	nspoid = stats_schema_check_privileges(nspname);
 	if (!OidIsValid(nspoid))
-		return false;
+		PG_RETURN_VOID();
 
 	relname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELNAME_ARG));
 	reloid = get_relname_relid(relname, nspoid);
@@ -944,31 +982,41 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 		ereport(WARNING,
 				(errcode(ERRCODE_UNDEFINED_OBJECT),
 				 errmsg("Relation \"%s\".\"%s\" not found.", nspname, relname)));
-		return false;
+		PG_RETURN_VOID();
 	}
 
 	if (RecoveryInProgress())
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("recovery is in progress"),
 				 errhint("Statistics cannot be modified during recovery.")));
+		PG_RETURN_VOID();
+	}
 
-	stats_lock_check_privileges(reloid);
+	if (!stats_lock_check_privileges(reloid))
+		PG_RETURN_VOID();
 
 	attname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTNAME_ARG));
 	attnum = get_attnum(reloid, attname);
 
 	if (attnum < 0)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot clear statistics on system column \"%s\"",
 						attname)));
+		PG_RETURN_VOID();
+	}
 
 	if (attnum == InvalidAttrNumber)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 errmsg("column \"%s\" of relation \"%s\" does not exist",
 						attname, get_rel_name(reloid))));
+		PG_RETURN_VOID();
+	}
 
 	inherited = PG_GETARG_BOOL(C_INHERITED_ARG);
 
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index fdc69bc93e2..49109cf721d 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -84,8 +84,11 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 	bool		nulls[4] = {0};
 	int			nreplaces = 0;
 
-	stats_check_required_arg(fcinfo, relarginfo, RELSCHEMA_ARG);
-	stats_check_required_arg(fcinfo, relarginfo, RELNAME_ARG);
+	if (!stats_check_required_arg(fcinfo, relarginfo, RELSCHEMA_ARG))
+		return false;
+
+	if (!stats_check_required_arg(fcinfo, relarginfo, RELNAME_ARG))
+		return false;
 
 	nspname = TextDatumGetCString(PG_GETARG_DATUM(RELSCHEMA_ARG));
 	nspoid = stats_schema_check_privileges(nspname);
@@ -108,7 +111,8 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 				 errmsg("recovery is in progress"),
 				 errhint("Statistics cannot be modified during recovery.")));
 
-	stats_lock_check_privileges(reloid);
+	if (!stats_lock_check_privileges(reloid))
+		return false;
 
 	if (!PG_ARGISNULL(RELPAGES_ARG))
 	{
diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c
index e037d4994e8..dd9d88ac1c5 100644
--- a/src/backend/statistics/stat_utils.c
+++ b/src/backend/statistics/stat_utils.c
@@ -34,16 +34,20 @@
 /*
  * Ensure that a given argument is not null.
  */
-void
+bool
 stats_check_required_arg(FunctionCallInfo fcinfo,
 						 struct StatsArgInfo *arginfo,
 						 int argnum)
 {
 	if (PG_ARGISNULL(argnum))
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("\"%s\" cannot be NULL",
 						arginfo[argnum].argname)));
+		return false;
+	}
+	return true;
 }
 
 /*
@@ -128,13 +132,14 @@ stats_check_arg_pair(FunctionCallInfo fcinfo,
  *   - the role owns the current database and the relation is not shared
  *   - the role has the MAINTAIN privilege on the relation
  */
-void
+bool
 stats_lock_check_privileges(Oid reloid)
 {
 	Relation	table;
 	Oid			table_oid = reloid;
 	Oid			index_oid = InvalidOid;
 	LOCKMODE	index_lockmode = NoLock;
+	bool		ok = true;
 
 	/*
 	 * For indexes, we follow the locking behavior in do_analyze_rel() and
@@ -174,14 +179,15 @@ stats_lock_check_privileges(Oid reloid)
 		case RELKIND_PARTITIONED_TABLE:
 			break;
 		default:
-			ereport(ERROR,
+			ereport(WARNING,
 					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 					 errmsg("cannot modify statistics for relation \"%s\"",
 							RelationGetRelationName(table)),
 					 errdetail_relkind_not_supported(table->rd_rel->relkind)));
+		ok = false;
 	}
 
-	if (OidIsValid(index_oid))
+	if (ok && (OidIsValid(index_oid)))
 	{
 		Relation	index;
 
@@ -194,25 +200,33 @@ stats_lock_check_privileges(Oid reloid)
 		relation_close(index, NoLock);
 	}
 
-	if (table->rd_rel->relisshared)
-		ereport(ERROR,
+	if (ok && (table->rd_rel->relisshared))
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot modify statistics for shared relation")));
+		ok = false;
+	}
 
-	if (!object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId()))
+	if (ok && (!object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId())))
 	{
 		AclResult	aclresult = pg_class_aclcheck(RelationGetRelid(table),
 												  GetUserId(),
 												  ACL_MAINTAIN);
 
 		if (aclresult != ACLCHECK_OK)
-			aclcheck_error(aclresult,
-						   get_relkind_objtype(table->rd_rel->relkind),
-						   NameStr(table->rd_rel->relname));
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+						errmsg("permission denied for relation %s",
+							   NameStr(table->rd_rel->relname))));
+			ok = false;
+		}
 	}
 
 	/* retain lock on table */
 	relation_close(table, NoLock);
+	return ok;
 }
 
 
@@ -318,9 +332,12 @@ stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
 								  &args, &types, &argnulls);
 
 	if (nargs % 2 != 0)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				errmsg("variadic arguments must be name/value pairs"),
 				errhint("Provide an even number of variadic arguments that can be divided into pairs."));
+		return false;
+	}
 
 	/*
 	 * For each argument name/value pair, find corresponding positional
@@ -333,14 +350,20 @@ stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
 		char	   *argname;
 
 		if (argnulls[i])
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 					(errmsg("name at variadic position %d is NULL", i + 1)));
+			return false;
+		}
 
 		if (types[i] != TEXTOID)
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 					(errmsg("name at variadic position %d has type \"%s\", expected type \"%s\"",
 							i + 1, format_type_be(types[i]),
 							format_type_be(TEXTOID))));
+			return false;
+		}
 
 		if (argnulls[i + 1])
 			continue;
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index 2f1295f2149..6551d6bf099 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -46,31 +46,51 @@ SELECT pg_clear_relation_stats('stats_import', 'test');
 --
 -- relstats tests
 --
--- error: schemaname missing
+-- warning: schemaname missing, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'relname', 'test',
         'relpages', 17::integer);
-ERROR:  "schemaname" cannot be NULL
--- error: relname missing
+WARNING:  "schemaname" cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+-- warning: relname missing, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relpages', 17::integer);
-ERROR:  "relname" cannot be NULL
---- error: schemaname is wrong type
+WARNING:  "relname" cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+--- warning: schemaname is wrong type, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 3.6::float,
         'relname', 'test',
         'relpages', 17::integer);
 WARNING:  argument "schemaname" has type "double precision", expected type "text"
-ERROR:  "schemaname" cannot be NULL
---- error: relname is wrong type
+WARNING:  "schemaname" cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+--- warning: relname is wrong type, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 0::oid,
         'relpages', 17::integer);
 WARNING:  argument "relname" has type "oid", expected type "text"
-ERROR:  "relname" cannot be NULL
--- error: relation not found
+WARNING:  "relname" cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+-- warning: relation not found, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'nope',
@@ -81,19 +101,30 @@ WARNING:  Relation "stats_import"."nope" not found.
  f
 (1 row)
 
--- error: odd number of variadic arguments cannot be pairs
+-- warning: odd number of variadic arguments cannot be pairs, nothing updated
 SELECT pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'test',
         'relallvisible');
-ERROR:  variadic arguments must be name/value pairs
+WARNING:  variadic arguments must be name/value pairs
 HINT:  Provide an even number of variadic arguments that can be divided into pairs.
--- error: argument name is NULL
+WARNING:  "schemaname" cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+-- warning: argument name is NULL, nothing updated
 SELECT pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'test',
         NULL, '17'::integer);
-ERROR:  name at variadic position 5 is NULL
+WARNING:  name at variadic position 5 is NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
 -- starting stats
 SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
@@ -345,26 +376,46 @@ CREATE SEQUENCE stats_import.testseq;
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'testseq');
-ERROR:  cannot modify statistics for relation "testseq"
+WARNING:  cannot modify statistics for relation "testseq"
 DETAIL:  This operation is not supported for sequences.
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
 SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testseq');
-ERROR:  cannot modify statistics for relation "testseq"
+WARNING:  cannot modify statistics for relation "testseq"
 DETAIL:  This operation is not supported for sequences.
+ pg_clear_relation_stats 
+-------------------------
+ 
+(1 row)
+
 CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
 SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testview');
-ERROR:  cannot modify statistics for relation "testview"
+WARNING:  cannot modify statistics for relation "testview"
 DETAIL:  This operation is not supported for views.
+ pg_clear_relation_stats 
+-------------------------
+ 
+(1 row)
+
 --
 -- attribute stats
 --
--- error: schemaname missing
+-- warning: schemaname missing, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relname', 'test',
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  "schemaname" cannot be NULL
--- error: schema does not exist
+WARNING:  "schemaname" cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: schema does not exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'nope',
     'relname', 'test',
@@ -377,14 +428,19 @@ WARNING:  schema nope does not exist
  f
 (1 row)
 
--- error: relname missing
+-- warning: relname missing, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  "relname" cannot be NULL
--- error: relname does not exist
+WARNING:  "relname" cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: relname does not exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'nope',
@@ -397,23 +453,33 @@ WARNING:  Relation "stats_import"."nope" not found.
  f
 (1 row)
 
--- error: relname null
+-- warning: relname null, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', NULL,
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  "relname" cannot be NULL
--- error: NULL attname
+WARNING:  "relname" cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: NULL attname, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
     'attname', NULL,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  must specify either attname or attnum
--- error: attname doesn't exist
+WARNING:  must specify either attname or attnum
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: attname doesn't exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -422,8 +488,13 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'null_frac', 0.1::real,
     'avg_width', 2::integer,
     'n_distinct', 0.3::real);
-ERROR:  column "nope" of relation "stats_import"."test" does not exist
--- error: both attname and attnum
+WARNING:  column "nope" of relation "stats_import"."test" does not exist
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: both attname and attnum, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -431,30 +502,50 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'attnum', 1::smallint,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  cannot specify both attname and attnum
--- error: neither attname nor attnum
+WARNING:  cannot specify both attname and attnum
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: neither attname nor attnum, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  must specify either attname or attnum
--- error: attribute is system column
+WARNING:  must specify either attname or attnum
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: attribute is system column, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
     'attname', 'xmin',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  cannot modify statistics on system column "xmin"
--- error: inherited null
+WARNING:  cannot modify statistics on system column "xmin"
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: inherited null, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
     'attname', 'id',
     'inherited', NULL::boolean,
     'null_frac', 0.1::real);
-ERROR:  "inherited" cannot be NULL
+WARNING:  "inherited" cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
 -- ok: just the fixed values, with version, no stakinds
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index ccdc44e9236..dbbebce1673 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -39,41 +39,41 @@ SELECT pg_clear_relation_stats('stats_import', 'test');
 -- relstats tests
 --
 
--- error: schemaname missing
+-- warning: schemaname missing, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'relname', 'test',
         'relpages', 17::integer);
 
--- error: relname missing
+-- warning: relname missing, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relpages', 17::integer);
 
---- error: schemaname is wrong type
+--- warning: schemaname is wrong type, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 3.6::float,
         'relname', 'test',
         'relpages', 17::integer);
 
---- error: relname is wrong type
+--- warning: relname is wrong type, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 0::oid,
         'relpages', 17::integer);
 
--- error: relation not found
+-- warning: relation not found, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'nope',
         'relpages', 17::integer);
 
--- error: odd number of variadic arguments cannot be pairs
+-- warning: odd number of variadic arguments cannot be pairs, nothing updated
 SELECT pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'test',
         'relallvisible');
 
--- error: argument name is NULL
+-- warning: argument name is NULL, nothing updated
 SELECT pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'test',
@@ -246,14 +246,14 @@ SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname
 -- attribute stats
 --
 
--- error: schemaname missing
+-- warning: schemaname missing, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relname', 'test',
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: schema does not exist
+-- warning: schema does not exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'nope',
     'relname', 'test',
@@ -261,14 +261,14 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: relname missing
+-- warning: relname missing, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: relname does not exist
+-- warning: relname does not exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'nope',
@@ -276,7 +276,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: relname null
+-- warning: relname null, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', NULL,
@@ -284,7 +284,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: NULL attname
+-- warning: NULL attname, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -292,7 +292,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: attname doesn't exist
+-- warning: attname doesn't exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -302,7 +302,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'avg_width', 2::integer,
     'n_distinct', 0.3::real);
 
--- error: both attname and attnum
+-- warning: both attname and attnum, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -311,14 +311,14 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: neither attname nor attnum
+-- warning: neither attname nor attnum, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: attribute is system column
+-- warning: attribute is system column, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -326,7 +326,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: inherited null
+-- warning: inherited null, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
-- 
2.48.1

v9-0004-Batching-getAttributeStats.patchtext/x-patch; charset=US-ASCII; name=v9-0004-Batching-getAttributeStats.patchDownload
From d9ab97b8ca00d03ab370de5b99546d01e0480bd5 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Fri, 14 Mar 2025 03:54:26 -0400
Subject: [PATCH v9 4/5] Batching getAttributeStats().

The prepared statement getAttributeStats() is fairly heavyweight and
could greatly increase pg_dump/pg_upgrade runtime. To alleviate this,
create a result set buffer of all of the attribute stats fetched for a
batch of 100 relations that could potentially have stats.

The query ensures that the order of results exactly matches the needs of
the code walking the TOC to print the stats calls.
---
 src/bin/pg_dump/pg_dump.c | 556 ++++++++++++++++++++++++++------------
 1 file changed, 385 insertions(+), 171 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 38ba6a90106..0c26dc7a1b4 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -143,6 +143,25 @@ typedef enum OidOptions
 	zeroAsNone = 4,
 } OidOptions;
 
+typedef enum StatsBufferState
+{
+	STATSBUF_UNINITIALIZED = 0,
+	STATSBUF_ACTIVE,
+	STATSBUF_EXHAUSTED
+}			StatsBufferState;
+
+typedef struct
+{
+	PGresult   *res;			/* results from most recent
+								 * getAttributeStats() */
+	int			idx;			/* first un-consumed row of results */
+	TocEntry   *te;				/* next TOC entry to search for statsitics
+								 * data */
+
+	StatsBufferState state;		/* current state of the buffer */
+}			AttributeStatsBuffer;
+
+
 /* global decls */
 static bool dosync = true;		/* Issue fsync() to make dump durable on disk. */
 
@@ -209,6 +228,18 @@ static int	nbinaryUpgradeClassOids = 0;
 static SequenceItem *sequences = NULL;
 static int	nsequences = 0;
 
+static AttributeStatsBuffer attrstats =
+{
+	NULL, 0, NULL, STATSBUF_UNINITIALIZED
+};
+
+/*
+ * The maximum number of relations that should be fetched in any one
+ * getAttributeStats() call.
+ */
+
+#define MAX_ATTR_STATS_RELS 100
+
 /*
  * The default number of rows per INSERT when
  * --inserts is specified without --rows-per-insert
@@ -222,6 +253,10 @@ static int	nsequences = 0;
  */
 #define MAX_BLOBS_PER_ARCHIVE_ENTRY 1000
 
+
+
+/* TODO: fmtId(const char *rawid) */
+
 /*
  * Macro for producing quoted, schema-qualified name of a dumpable object.
  */
@@ -399,6 +434,9 @@ static void setupDumpWorker(Archive *AH);
 static TableInfo *getRootTableInfo(const TableInfo *tbinfo);
 static bool forcePartitionRootLoad(const TableInfo *tbinfo);
 static void read_dump_filters(const char *filename, DumpOptions *dopt);
+static void appendNamedArgument(PQExpBuffer out, Archive *fout,
+								const char *argname, const char *argtype,
+								const char *argval);
 
 
 int
@@ -10477,7 +10515,286 @@ statisticsDumpSection(const RelStatsInfo *rsinfo)
 }
 
 /*
- * printDumpRelationStats --
+ * Fetch next batch of rows from getAttributeStats()
+ */
+static void
+fetchNextAttributeStats(Archive *fout)
+{
+	ArchiveHandle *AH = (ArchiveHandle *) fout;
+	PQExpBufferData schemas;
+	PQExpBufferData relations;
+	int			numoids = 0;
+
+	Assert(AH != NULL);
+
+	/* free last result set, if any */
+	if (attrstats.state == STATSBUF_ACTIVE)
+		PQclear(attrstats.res);
+
+	/* If we have looped around to the start of the TOC, restart */
+	if (attrstats.te == AH->toc)
+		attrstats.te = AH->toc->next;
+
+	initPQExpBuffer(&schemas);
+	initPQExpBuffer(&relations);
+
+	/*
+	 * Walk ahead looking for relstats entries that are active in this
+	 * section, adding the names to the schemas and relations lists.
+	 */
+	while ((attrstats.te != AH->toc) && (numoids < MAX_ATTR_STATS_RELS))
+	{
+		if (attrstats.te->reqs != 0 &&
+			strcmp(attrstats.te->desc, "STATISTICS DATA") == 0)
+		{
+			RelStatsInfo *rsinfo = (RelStatsInfo *) attrstats.te->createDumperArg;
+
+			Assert(rsinfo != NULL);
+
+			if (numoids > 0)
+			{
+				appendPQExpBufferStr(&schemas, ",");
+				appendPQExpBufferStr(&relations, ",");
+			}
+			appendPQExpBufferStr(&schemas, fmtId(rsinfo->dobj.namespace->dobj.name));
+			appendPQExpBufferStr(&relations, fmtId(rsinfo->dobj.name));
+			numoids++;
+		}
+
+		attrstats.te = attrstats.te->next;
+	}
+
+	if (numoids > 0)
+	{
+		PQExpBufferData query;
+
+		initPQExpBuffer(&query);
+		appendPQExpBuffer(&query,
+						  "EXECUTE getAttributeStats('{%s}'::pg_catalog.text[],'{%s}'::pg_catalog.text[])",
+						  schemas.data, relations.data);
+		attrstats.res = ExecuteSqlQuery(fout, query.data, PGRES_TUPLES_OK);
+		attrstats.idx = 0;
+	}
+	else
+	{
+		attrstats.state = STATSBUF_EXHAUSTED;
+		attrstats.res = NULL;
+		attrstats.idx = -1;
+	}
+
+	termPQExpBuffer(&schemas);
+	termPQExpBuffer(&relations);
+}
+
+/*
+ * Prepare the getAttributeStats() statement
+ *
+ * This is done automatically if the user specified dumpStatistics.
+ */
+static void
+initAttributeStats(Archive *fout)
+{
+	ArchiveHandle *AH = (ArchiveHandle *) fout;
+	PQExpBufferData query;
+
+	Assert(AH != NULL);
+	initPQExpBuffer(&query);
+
+	appendPQExpBufferStr(&query,
+						 "PREPARE getAttributeStats(pg_catalog.text[], pg_catalog.text[]) AS\n"
+						 "SELECT s.schemaname, s.tablename, s.attname, s.inherited, "
+						 "s.null_frac, s.avg_width, s.n_distinct, s.most_common_vals, "
+						 "s.most_common_freqs, s.histogram_bounds, s.correlation, "
+						 "s.most_common_elems, s.most_common_elem_freqs, "
+						 "s.elem_count_histogram, ");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(&query,
+							 "s.range_length_histogram, "
+							 "s.range_empty_frac, "
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(&query,
+							 "NULL AS range_length_histogram, "
+							 "NULL AS range_empty_frac, "
+							 " NULL AS range_bounds_histogram ");
+
+	/*
+	 * The results must be in the order of relations supplied in the
+	 * parameters to ensure that they are in sync with a walk of the TOC.
+	 *
+	 * The redundant (and incomplete) filter clause on s.tablename = ANY(...)
+	 * is a way to lead the query into using the index
+	 * pg_class_relname_nsp_index which in turn allows the planner to avoid an
+	 * expensive full scan of pg_stats.
+	 *
+	 * We may need to adjust this query for versions that are not so easily
+	 * led.
+	 */
+	appendPQExpBufferStr(&query,
+						 "FROM pg_catalog.pg_stats AS s "
+						 "JOIN unnest($1, $2) WITH ORDINALITY AS u(schemaname, tablename, ord) "
+						 "ON s.schemaname = u.schemaname "
+						 "AND s.tablename = u.tablename "
+						 "WHERE s.tablename = ANY($2) "
+						 "ORDER BY u.ord, s.attname, s.inherited");
+
+	ExecuteSqlStatement(fout, query.data);
+
+	termPQExpBuffer(&query);
+
+	attrstats.te = AH->toc->next;
+
+	fetchNextAttributeStats(fout);
+
+	attrstats.state = STATSBUF_ACTIVE;
+}
+
+
+/*
+ * append a single attribute stat to the buffer for this relation.
+ */
+static void
+appendAttributeStats(Archive *fout, PQExpBuffer out,
+					 const RelStatsInfo *rsinfo)
+{
+	PGresult   *res = attrstats.res;
+	int			tup_num = attrstats.idx;
+
+	const char *attname;
+
+	static bool indexes_set = false;
+	static int	i_attname,
+				i_inherited,
+				i_null_frac,
+				i_avg_width,
+				i_n_distinct,
+				i_most_common_vals,
+				i_most_common_freqs,
+				i_histogram_bounds,
+				i_correlation,
+				i_most_common_elems,
+				i_most_common_elem_freqs,
+				i_elem_count_histogram,
+				i_range_length_histogram,
+				i_range_empty_frac,
+				i_range_bounds_histogram;
+
+	if (!indexes_set)
+	{
+		/*
+		 * It's a prepared statement, so the indexes will be the same for all
+		 * result sets, so we only need to set them once.
+		 */
+		i_attname = PQfnumber(res, "attname");
+		i_inherited = PQfnumber(res, "inherited");
+		i_null_frac = PQfnumber(res, "null_frac");
+		i_avg_width = PQfnumber(res, "avg_width");
+		i_n_distinct = PQfnumber(res, "n_distinct");
+		i_most_common_vals = PQfnumber(res, "most_common_vals");
+		i_most_common_freqs = PQfnumber(res, "most_common_freqs");
+		i_histogram_bounds = PQfnumber(res, "histogram_bounds");
+		i_correlation = PQfnumber(res, "correlation");
+		i_most_common_elems = PQfnumber(res, "most_common_elems");
+		i_most_common_elem_freqs = PQfnumber(res, "most_common_elem_freqs");
+		i_elem_count_histogram = PQfnumber(res, "elem_count_histogram");
+		i_range_length_histogram = PQfnumber(res, "range_length_histogram");
+		i_range_empty_frac = PQfnumber(res, "range_empty_frac");
+		i_range_bounds_histogram = PQfnumber(res, "range_bounds_histogram");
+		indexes_set = true;
+	}
+
+	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+	appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
+					  fout->remoteVersion);
+	appendPQExpBufferStr(out, "\t'schemaname', ");
+	appendStringLiteralAH(out, rsinfo->dobj.namespace->dobj.name, fout);
+	appendPQExpBufferStr(out, ",\n\t'relname', ");
+	appendStringLiteralAH(out, rsinfo->dobj.name, fout);
+
+	if (PQgetisnull(res, tup_num, i_attname))
+		pg_fatal("attname cannot be NULL");
+	attname = PQgetvalue(res, tup_num, i_attname);
+
+	/*
+	 * Indexes look up attname in indAttNames to derive attnum, all others use
+	 * attname directly.  We must specify attnum for indexes, since their
+	 * attnames are not necessarily stable across dump/reload.
+	 */
+	if (rsinfo->nindAttNames == 0)
+	{
+		appendPQExpBuffer(out, ",\n\t'attname', ");
+		appendStringLiteralAH(out, attname, fout);
+	}
+	else
+	{
+		bool		found = false;
+
+		for (int i = 0; i < rsinfo->nindAttNames; i++)
+			if (strcmp(attname, rsinfo->indAttNames[i]) == 0)
+			{
+				appendPQExpBuffer(out, ",\n\t'attnum', '%d'::smallint",
+								  i + 1);
+				found = true;
+				break;
+			}
+
+		if (!found)
+			pg_fatal("could not find index attname \"%s\"", attname);
+	}
+
+	if (!PQgetisnull(res, tup_num, i_inherited))
+		appendNamedArgument(out, fout, "inherited", "boolean",
+							PQgetvalue(res, tup_num, i_inherited));
+	if (!PQgetisnull(res, tup_num, i_null_frac))
+		appendNamedArgument(out, fout, "null_frac", "real",
+							PQgetvalue(res, tup_num, i_null_frac));
+	if (!PQgetisnull(res, tup_num, i_avg_width))
+		appendNamedArgument(out, fout, "avg_width", "integer",
+							PQgetvalue(res, tup_num, i_avg_width));
+	if (!PQgetisnull(res, tup_num, i_n_distinct))
+		appendNamedArgument(out, fout, "n_distinct", "real",
+							PQgetvalue(res, tup_num, i_n_distinct));
+	if (!PQgetisnull(res, tup_num, i_most_common_vals))
+		appendNamedArgument(out, fout, "most_common_vals", "text",
+							PQgetvalue(res, tup_num, i_most_common_vals));
+	if (!PQgetisnull(res, tup_num, i_most_common_freqs))
+		appendNamedArgument(out, fout, "most_common_freqs", "real[]",
+							PQgetvalue(res, tup_num, i_most_common_freqs));
+	if (!PQgetisnull(res, tup_num, i_histogram_bounds))
+		appendNamedArgument(out, fout, "histogram_bounds", "text",
+							PQgetvalue(res, tup_num, i_histogram_bounds));
+	if (!PQgetisnull(res, tup_num, i_correlation))
+		appendNamedArgument(out, fout, "correlation", "real",
+							PQgetvalue(res, tup_num, i_correlation));
+	if (!PQgetisnull(res, tup_num, i_most_common_elems))
+		appendNamedArgument(out, fout, "most_common_elems", "text",
+							PQgetvalue(res, tup_num, i_most_common_elems));
+	if (!PQgetisnull(res, tup_num, i_most_common_elem_freqs))
+		appendNamedArgument(out, fout, "most_common_elem_freqs", "real[]",
+							PQgetvalue(res, tup_num, i_most_common_elem_freqs));
+	if (!PQgetisnull(res, tup_num, i_elem_count_histogram))
+		appendNamedArgument(out, fout, "elem_count_histogram", "real[]",
+							PQgetvalue(res, tup_num, i_elem_count_histogram));
+	if (fout->remoteVersion >= 170000)
+	{
+		if (!PQgetisnull(res, tup_num, i_range_length_histogram))
+			appendNamedArgument(out, fout, "range_length_histogram", "text",
+								PQgetvalue(res, tup_num, i_range_length_histogram));
+		if (!PQgetisnull(res, tup_num, i_range_empty_frac))
+			appendNamedArgument(out, fout, "range_empty_frac", "real",
+								PQgetvalue(res, tup_num, i_range_empty_frac));
+		if (!PQgetisnull(res, tup_num, i_range_bounds_histogram))
+			appendNamedArgument(out, fout, "range_bounds_histogram", "text",
+								PQgetvalue(res, tup_num, i_range_bounds_histogram));
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+
+
+/*
+ * printRelationStats --
  *
  * Generate the SQL statements needed to restore a relation's statistics.
  */
@@ -10485,64 +10802,21 @@ static char *
 printRelationStats(Archive *fout, const void *userArg)
 {
 	const RelStatsInfo *rsinfo = (RelStatsInfo *) userArg;
-	const DumpableObject *dobj = &rsinfo->dobj;
+	const DumpableObject *dobj;
+	const char *relschema;
+	const char *relname;
+
+	ArchiveHandle *AH = (ArchiveHandle *) fout;
 
-	PQExpBufferData query;
 	PQExpBufferData out;
 
-	PGresult   *res;
-
-	static bool first_query = true;
-	static int	i_attname;
-	static int	i_inherited;
-	static int	i_null_frac;
-	static int	i_avg_width;
-	static int	i_n_distinct;
-	static int	i_most_common_vals;
-	static int	i_most_common_freqs;
-	static int	i_histogram_bounds;
-	static int	i_correlation;
-	static int	i_most_common_elems;
-	static int	i_most_common_elem_freqs;
-	static int	i_elem_count_histogram;
-	static int	i_range_length_histogram;
-	static int	i_range_empty_frac;
-	static int	i_range_bounds_histogram;
-
-	initPQExpBuffer(&query);
-
-	if (first_query)
-	{
-		appendPQExpBufferStr(&query,
-							 "PREPARE getAttributeStats(pg_catalog.text, pg_catalog.text) AS\n"
-							 "SELECT s.attname, s.inherited, "
-							 "s.null_frac, s.avg_width, s.n_distinct, "
-							 "s.most_common_vals, s.most_common_freqs, "
-							 "s.histogram_bounds, s.correlation, "
-							 "s.most_common_elems, s.most_common_elem_freqs, "
-							 "s.elem_count_histogram, ");
-
-		if (fout->remoteVersion >= 170000)
-			appendPQExpBufferStr(&query,
-								 "s.range_length_histogram, "
-								 "s.range_empty_frac, "
-								 "s.range_bounds_histogram ");
-		else
-			appendPQExpBufferStr(&query,
-								 "NULL AS range_length_histogram,"
-								 "NULL AS range_empty_frac,"
-								 "NULL AS range_bounds_histogram ");
-
-		appendPQExpBufferStr(&query,
-							 "FROM pg_catalog.pg_stats s "
-							 "WHERE s.schemaname = $1 "
-							 "AND s.tablename = $2 "
-							 "ORDER BY s.attname, s.inherited");
-
-		ExecuteSqlStatement(fout, query.data);
-
-		resetPQExpBuffer(&query);
-	}
+	Assert(rsinfo != NULL);
+	dobj = &rsinfo->dobj;
+	Assert(dobj != NULL);
+	relschema = dobj->namespace->dobj.name;
+	Assert(relschema != NULL);
+	relname = dobj->name;
+	Assert(relname != NULL);
 
 	initPQExpBuffer(&out);
 
@@ -10561,132 +10835,72 @@ printRelationStats(Archive *fout, const void *userArg)
 	appendPQExpBuffer(&out, "\t'relallvisible', '%d'::integer\n);\n",
 					  rsinfo->relallvisible);
 
-	/* fetch attribute stats */
-	appendPQExpBufferStr(&query, "EXECUTE getAttributeStats(");
-	appendStringLiteralAH(&query, dobj->namespace->dobj.name, fout);
-	appendPQExpBufferStr(&query, ", ");
-	appendStringLiteralAH(&query, dobj->name, fout);
-	appendPQExpBufferStr(&query, ")");
+	AH->txnCount++;
 
-	res = ExecuteSqlQuery(fout, query.data, PGRES_TUPLES_OK);
+	if (attrstats.state == STATSBUF_UNINITIALIZED)
+		initAttributeStats(fout);
 
-	if (first_query)
+	/*
+	 * Because the query returns rows in the same order as the relations
+	 * requested, and because every relation gets at least one row in the
+	 * result set, the first row for this relation must correspond either to
+	 * the current row of this result set (if one exists) or the first row of
+	 * the next result set (if this one is already consumed).
+	 */
+	if (attrstats.state != STATSBUF_ACTIVE)
+		pg_fatal("Exhausted getAttributeStats() before processing %s.%s",
+				 rsinfo->dobj.namespace->dobj.name,
+				 rsinfo->dobj.name);
+
+	/*
+	 * If the current result set has been fully consumed, then the row(s) we
+	 * need (if any) would be found in the next one. This will update
+	 * attrstats.res and attrstats.idx.
+	 */
+	if (PQntuples(attrstats.res) <= attrstats.idx)
+		fetchNextAttributeStats(fout);
+
+	while (true)
 	{
-		i_attname = PQfnumber(res, "attname");
-		i_inherited = PQfnumber(res, "inherited");
-		i_null_frac = PQfnumber(res, "null_frac");
-		i_avg_width = PQfnumber(res, "avg_width");
-		i_n_distinct = PQfnumber(res, "n_distinct");
-		i_most_common_vals = PQfnumber(res, "most_common_vals");
-		i_most_common_freqs = PQfnumber(res, "most_common_freqs");
-		i_histogram_bounds = PQfnumber(res, "histogram_bounds");
-		i_correlation = PQfnumber(res, "correlation");
-		i_most_common_elems = PQfnumber(res, "most_common_elems");
-		i_most_common_elem_freqs = PQfnumber(res, "most_common_elem_freqs");
-		i_elem_count_histogram = PQfnumber(res, "elem_count_histogram");
-		i_range_length_histogram = PQfnumber(res, "range_length_histogram");
-		i_range_empty_frac = PQfnumber(res, "range_empty_frac");
-		i_range_bounds_histogram = PQfnumber(res, "range_bounds_histogram");
-		first_query = false;
-	}
-
-	/* restore attribute stats */
-	for (int rownum = 0; rownum < PQntuples(res); rownum++)
-	{
-		const char *attname;
-
-		appendPQExpBufferStr(&out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
-		appendPQExpBuffer(&out, "\t'version', '%u'::integer,\n",
-						  fout->remoteVersion);
-		appendPQExpBufferStr(&out, "\t'schemaname', ");
-		appendStringLiteralAH(&out, rsinfo->dobj.namespace->dobj.name, fout);
-		appendPQExpBufferStr(&out, ",\n\t'relname', ");
-		appendStringLiteralAH(&out, rsinfo->dobj.name, fout);
-
-		if (PQgetisnull(res, rownum, i_attname))
-			pg_fatal("attname cannot be NULL");
-		attname = PQgetvalue(res, rownum, i_attname);
+		int			i_schemaname;
+		int			i_tablename;
+		char	   *schemaname;
+		char	   *tablename;	/* misnomer, following pg_stats naming */
 
 		/*
-		 * Indexes look up attname in indAttNames to derive attnum, all others
-		 * use attname directly.  We must specify attnum for indexes, since
-		 * their attnames are not necessarily stable across dump/reload.
+		 * If we hit the end of the result set, then there are no more records
+		 * for this relation, so we should stop, but first get the next result
+		 * set for the next batch of relations.
 		 */
-		if (rsinfo->nindAttNames == 0)
+		if (PQntuples(attrstats.res) <= attrstats.idx)
 		{
-			appendPQExpBuffer(&out, ",\n\t'attname', ");
-			appendStringLiteralAH(&out, attname, fout);
-		}
-		else
-		{
-			bool		found = false;
-
-			for (int i = 0; i < rsinfo->nindAttNames; i++)
-			{
-				if (strcmp(attname, rsinfo->indAttNames[i]) == 0)
-				{
-					appendPQExpBuffer(&out, ",\n\t'attnum', '%d'::smallint",
-									  i + 1);
-					found = true;
-					break;
-				}
-			}
-
-			if (!found)
-				pg_fatal("could not find index attname \"%s\"", attname);
+			fetchNextAttributeStats(fout);
+			break;
 		}
 
-		if (!PQgetisnull(res, rownum, i_inherited))
-			appendNamedArgument(&out, fout, "inherited", "boolean",
-								PQgetvalue(res, rownum, i_inherited));
-		if (!PQgetisnull(res, rownum, i_null_frac))
-			appendNamedArgument(&out, fout, "null_frac", "real",
-								PQgetvalue(res, rownum, i_null_frac));
-		if (!PQgetisnull(res, rownum, i_avg_width))
-			appendNamedArgument(&out, fout, "avg_width", "integer",
-								PQgetvalue(res, rownum, i_avg_width));
-		if (!PQgetisnull(res, rownum, i_n_distinct))
-			appendNamedArgument(&out, fout, "n_distinct", "real",
-								PQgetvalue(res, rownum, i_n_distinct));
-		if (!PQgetisnull(res, rownum, i_most_common_vals))
-			appendNamedArgument(&out, fout, "most_common_vals", "text",
-								PQgetvalue(res, rownum, i_most_common_vals));
-		if (!PQgetisnull(res, rownum, i_most_common_freqs))
-			appendNamedArgument(&out, fout, "most_common_freqs", "real[]",
-								PQgetvalue(res, rownum, i_most_common_freqs));
-		if (!PQgetisnull(res, rownum, i_histogram_bounds))
-			appendNamedArgument(&out, fout, "histogram_bounds", "text",
-								PQgetvalue(res, rownum, i_histogram_bounds));
-		if (!PQgetisnull(res, rownum, i_correlation))
-			appendNamedArgument(&out, fout, "correlation", "real",
-								PQgetvalue(res, rownum, i_correlation));
-		if (!PQgetisnull(res, rownum, i_most_common_elems))
-			appendNamedArgument(&out, fout, "most_common_elems", "text",
-								PQgetvalue(res, rownum, i_most_common_elems));
-		if (!PQgetisnull(res, rownum, i_most_common_elem_freqs))
-			appendNamedArgument(&out, fout, "most_common_elem_freqs", "real[]",
-								PQgetvalue(res, rownum, i_most_common_elem_freqs));
-		if (!PQgetisnull(res, rownum, i_elem_count_histogram))
-			appendNamedArgument(&out, fout, "elem_count_histogram", "real[]",
-								PQgetvalue(res, rownum, i_elem_count_histogram));
-		if (fout->remoteVersion >= 170000)
-		{
-			if (!PQgetisnull(res, rownum, i_range_length_histogram))
-				appendNamedArgument(&out, fout, "range_length_histogram", "text",
-									PQgetvalue(res, rownum, i_range_length_histogram));
-			if (!PQgetisnull(res, rownum, i_range_empty_frac))
-				appendNamedArgument(&out, fout, "range_empty_frac", "real",
-									PQgetvalue(res, rownum, i_range_empty_frac));
-			if (!PQgetisnull(res, rownum, i_range_bounds_histogram))
-				appendNamedArgument(&out, fout, "range_bounds_histogram", "text",
-									PQgetvalue(res, rownum, i_range_bounds_histogram));
-		}
-		appendPQExpBufferStr(&out, "\n);\n");
+		i_schemaname = PQfnumber(attrstats.res, "schemaname");
+		Assert(i_schemaname >= 0);
+		i_tablename = PQfnumber(attrstats.res, "tablename");
+		Assert(i_tablename >= 0);
+
+		if (PQgetisnull(attrstats.res, attrstats.idx, i_schemaname))
+			pg_fatal("getAttributeStats() schemaname cannot be NULL");
+
+		if (PQgetisnull(attrstats.res, attrstats.idx, i_tablename))
+			pg_fatal("getAttributeStats() tablename cannot be NULL");
+
+		schemaname = PQgetvalue(attrstats.res, attrstats.idx, i_schemaname);
+		tablename = PQgetvalue(attrstats.res, attrstats.idx, i_tablename);
+
+		/* stop if current stat row isn't for this relation */
+		if (strcmp(relname, tablename) != 0 || strcmp(relschema, schemaname) != 0)
+			break;
+
+		appendAttributeStats(fout, &out, rsinfo);
+		AH->txnCount++;
+		attrstats.idx++;
 	}
 
-	PQclear(res);
-
-	termPQExpBuffer(&query);
 	return out.data;
 }
 
-- 
2.48.1

#486Nathan Bossart
nathandbossart@gmail.com
In reply to: Corey Huinker (#484)
Re: Statistics Import and Export

Thanks for working on this, Corey.

On Fri, Mar 14, 2025 at 04:03:16PM -0400, Corey Huinker wrote:

0003 -

Storing the restore function calls in the archive entry hogged a lot of
memory and made people nervous. This introduces a new function pointer that
generates those restore SQL calls right before they're written to disk,
thus reducing the memory load from "stats for every object to be dumped" to
just one object. Thanks to Nathan for diagnosing some weird quirks with
various formats.

0004 -

This replaces the query in the prepared statement with one that batches
them 100 relations at a time, and then maintains that result set until it
is consumed. It seems to have obvious speedups.

I've been doing a variety of tests with my toy database of 100K relations
[0]: /messages/by-id/Z9R9-mFbxukqKmg4@nathan
than without stats, but that's still a pretty nice improvement.

I'd propose two small changes to the design:

* I tested a variety of batch sizes, and to my suprise, I saw the best
results with around 64 relations per batch. I imagine the absolute best
batch size will vary greatly depending on the workload. It might also
depend on work_mem and friends.

* The custom format actually does two WriteToc() calls, and since these
patches move the queries to this part of pg_dump, it means we'll run all
the queries twice. The comments around this code suggest that the second
pass isn't strictly necessary and that it is really only useful for
data/parallel restore, so we could probably skip it for no-data dumps.

With those two changes, a pg_upgrade-style dump of my test database goes
from ~21.6 seconds without these patches to ~11.2 seconds with them. For
reference, the same dump without stats takes ~7 seconds on HEAD.

[0]: /messages/by-id/Z9R9-mFbxukqKmg4@nathan

--
nathan

#487Corey Huinker
corey.huinker@gmail.com
In reply to: Nathan Bossart (#486)
Re: Statistics Import and Export

* The custom format actually does two WriteToc() calls, and since these
patches move the queries to this part of pg_dump, it means we'll run all
the queries twice. The comments around this code suggest that the second
pass isn't strictly necessary and that it is really only useful for
data/parallel restore, so we could probably skip it for no-data dumps.

Is there any reason we couldn't have stats objects remove themselves from
the list after completion?

#488Nathan Bossart
nathandbossart@gmail.com
In reply to: Corey Huinker (#487)
Re: Statistics Import and Export

On Sun, Mar 16, 2025 at 05:32:15PM -0400, Corey Huinker wrote:

* The custom format actually does two WriteToc() calls, and since these
patches move the queries to this part of pg_dump, it means we'll run all
the queries twice. The comments around this code suggest that the second
pass isn't strictly necessary and that it is really only useful for
data/parallel restore, so we could probably skip it for no-data dumps.

Is there any reason we couldn't have stats objects remove themselves from
the list after completion?

I'm assuming that writing a completely different TOC on the second pass
would corrupt the dump file. Perhaps we could teach it to skip stats
entries on the second pass or something, but I'm not too wild about adding
to the list of invasive changes we're making last-minute for v18.

--
nathan

#489Corey Huinker
corey.huinker@gmail.com
In reply to: Nathan Bossart (#488)
Re: Statistics Import and Export

On Mon, Mar 17, 2025 at 10:24 AM Nathan Bossart <nathandbossart@gmail.com>
wrote:

On Sun, Mar 16, 2025 at 05:32:15PM -0400, Corey Huinker wrote:

* The custom format actually does two WriteToc() calls, and since these
patches move the queries to this part of pg_dump, it means we'll run

all

the queries twice. The comments around this code suggest that the

second

pass isn't strictly necessary and that it is really only useful for
data/parallel restore, so we could probably skip it for no-data dumps.

Is there any reason we couldn't have stats objects remove themselves from
the list after completion?

I'm assuming that writing a completely different TOC on the second pass
would corrupt the dump file. Perhaps we could teach it to skip stats
entries on the second pass or something, but I'm not too wild about adding
to the list of invasive changes we're making last-minute for v18.

I'm confused, are they needed in both places? If so, would it make sense to
write out each stat entry to a file and then re-read the file on the second
pass, or maybe do a \i filename in the sql script?

Not suggesting we do any of this for v18, but when I hear about doing
something twice when that thing was painful the first time, I look for ways
to avoid doing it, or set pan_is_hot = true for the next person.

#490Nathan Bossart
nathandbossart@gmail.com
In reply to: Corey Huinker (#489)
Re: Statistics Import and Export

On Mon, Mar 17, 2025 at 07:24:46PM -0400, Corey Huinker wrote:

On Mon, Mar 17, 2025 at 10:24 AM Nathan Bossart <nathandbossart@gmail.com>
wrote:

I'm assuming that writing a completely different TOC on the second pass
would corrupt the dump file. Perhaps we could teach it to skip stats
entries on the second pass or something, but I'm not too wild about adding
to the list of invasive changes we're making last-minute for v18.

I'm confused, are they needed in both places?

AFAICT yes. The second pass rewrites the TOC to udpate the data offset
information. If we wrote a different TOC the second time around, then the
dump file would be broken, right?

/*
* If possible, re-write the TOC in order to update the data offset
* information. This is not essential, as pg_restore can cope in most
* cases without it; but it can make pg_restore significantly faster
* in some situations (especially parallel restore).
*/
if (ctx->hasSeek &&
fseeko(AH->FH, tpos, SEEK_SET) == 0)
WriteToc(AH);

--
nathan

#491Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#485)
Re: Statistics Import and Export

On Sat, 2025-03-15 at 21:37 -0400, Corey Huinker wrote:

0001 - no changes, but the longer I go the more I'm certain this is
something we want to do.

This replaces regclassin with custom lookups of the namespace and
relname, but misses some of the complexities that regclassin is
handling. For instance, it calls RangeVarGetRelid(), which calls
LookupExplicitNamespace(), which handles temp tables and
InvokeNamespaceSearchHook().

At first it looked like a bit too much code to copy, but regclassin()
passes NoLock, which means we basically just have to call
LookupExplicitNamespace().

Regards,
Jeff Davis

#492Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#491)
Re: Statistics Import and Export

This replaces regclassin with custom lookups of the namespace and
relname, but misses some of the complexities that regclassin is
handling. For instance, it calls RangeVarGetRelid(), which calls
LookupExplicitNamespace(), which handles temp tables and
InvokeNamespaceSearchHook().

At first it looked like a bit too much code to copy, but regclassin()
passes NoLock, which means we basically just have to call
LookupExplicitNamespace().

To be clear, LookupExplicitNamespace() can call aclcheck_error(), which is
something we cannot presently step-down into a WARNING, so an aclcheck
failure inside a restore/upgrade would fail the upgrade. I want to make
sure we can live with that because it might be hard to explain what's an
error we can nerf and what isn't.

#493Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#479)
Re: Statistics Import and Export

On Sat, 2025-03-08 at 14:09 -0500, Corey Huinker wrote:

except it is perfectly clear that you *asked for* data and
statistics, so you get what you asked for. however the user
conjures in their heads what they are looking for, the logic is
simple, you get what you asked for. 

They *asked for* that because they didn't have the mechanism to say
"hold the mayo" or "everything except pickles". That's reducing their
choice, and then blaming them for their choice.

Can we reach a decision here and move forward?

Regards,
Jeff Davis

#494Jeff Davis
pgsql@j-davis.com
In reply to: Jeff Davis (#491)
1 attachment(s)
Re: Statistics Import and Export

On Wed, 2025-03-19 at 15:17 -0700, Jeff Davis wrote:

On Sat, 2025-03-15 at 21:37 -0400, Corey Huinker wrote:

0001 - no changes, but the longer I go the more I'm certain this
is
something we want to do.

This replaces regclassin with custom lookups of the namespace and
relname, but misses some of the complexities that regclassin is
handling. For instance, it calls RangeVarGetRelid(), which calls
LookupExplicitNamespace(), which handles temp tables and
InvokeNamespaceSearchHook().

At first it looked like a bit too much code to copy, but regclassin()
passes NoLock, which means we basically just have to call
LookupExplicitNamespace().

Attached new version 9j:

* Changed to use LookupExplicitNamespace()
* Added test for temp tables
* Doc fixes

Regards,
Jeff Davis

Attachments:

v9j-0001-Stats-use-schemaname-relname-instead-of-regclass.patchtext/x-patch; charset=UTF-8; name=v9j-0001-Stats-use-schemaname-relname-instead-of-regclass.patchDownload
From 72d4b9fc128e6d4ef73bb24ebba41797d06a7d9e Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Tue, 4 Mar 2025 22:16:52 -0500
Subject: [PATCH v9j] Stats: use schemaname/relname instead of regclass.

For import and export, use schemaname/relname rather than
regclass.

This is more natural during export, fits with the other arguments
better, and it gives better control over error handling in case we
need to downgrade more errors to warnings.

Also, use text for the argument types for schemaname, relname, and
attname so that casts to "name" are not required.

Author: Corey Huinker <corey.huinker@gmail.com>
Discussion: https://postgr.es/m/CADkLM=ceOSsx_=oe73QQ-BxUFR2Cwqum7-UP_fPe22DBY0NerA@mail.gmail.com
---
 doc/src/sgml/func.sgml                     |  50 +--
 src/backend/statistics/attribute_stats.c   |  87 +++--
 src/backend/statistics/relation_stats.c    |  65 ++--
 src/backend/statistics/stat_utils.c        |  37 +++
 src/bin/pg_dump/pg_dump.c                  |  25 +-
 src/bin/pg_dump/t/002_pg_dump.pl           |   6 +-
 src/include/catalog/catversion.h           |   2 +-
 src/include/catalog/pg_proc.dat            |   8 +-
 src/include/statistics/stat_utils.h        |   2 +
 src/test/regress/expected/stats_import.out | 353 ++++++++++++++-------
 src/test/regress/sql/stats_import.sql      | 306 ++++++++++++------
 11 files changed, 647 insertions(+), 294 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 6fa1d6586b8..f8c1deb04ee 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30364,22 +30364,24 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          <structname>mytable</structname>:
 <programlisting>
  SELECT pg_restore_relation_stats(
-    'relation',  'mytable'::regclass,
-    'relpages',  173::integer,
-    'reltuples', 10000::real);
+    'schemaname', 'myschema',
+    'relname',    'mytable',
+    'relpages',   173::integer,
+    'reltuples',  10000::real);
 </programlisting>
         </para>
         <para>
-         The argument <literal>relation</literal> with a value of type
-         <type>regclass</type> is required, and specifies the table. Other
+         The arguments <literal>schemaname</literal> and
+         <literal>relname</literal> are required, and specify the table. Other
          arguments are the names and values of statistics corresponding to
          certain columns in <link
          linkend="catalog-pg-class"><structname>pg_class</structname></link>.
          The currently-supported relation statistics are
          <literal>relpages</literal> with a value of type
          <type>integer</type>, <literal>reltuples</literal> with a value of
-         type <type>real</type>, and <literal>relallvisible</literal> with a
-         value of type <type>integer</type>.
+         type <type>real</type>, <literal>relallvisible</literal> with a value
+         of type <type>integer</type>, and <literal>relallfrozen</literal>
+         with a value of type <type>integer</type>.
         </para>
         <para>
          Additionally, this function accepts argument name
@@ -30407,7 +30409,7 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          <indexterm>
           <primary>pg_clear_relation_stats</primary>
          </indexterm>
-         <function>pg_clear_relation_stats</function> ( <parameter>relation</parameter> <type>regclass</type> )
+         <function>pg_clear_relation_stats</function> ( <parameter>schemaname</parameter> <type>text</type>, <parameter>relname</parameter> <type>text</type> )
          <returnvalue>void</returnvalue>
         </para>
         <para>
@@ -30456,22 +30458,23 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
          <structname>mytable</structname>:
 <programlisting>
  SELECT pg_restore_attribute_stats(
-    'relation',    'mytable'::regclass,
-    'attname',     'col1'::name,
-    'inherited',   false,
-    'avg_width',   125::integer,
-    'null_frac',   0.5::real);
+    'schemaname', 'myschema',
+    'relname',    'mytable',
+    'attname',    'col1',
+    'inherited',  false,
+    'avg_width',  125::integer,
+    'null_frac',  0.5::real);
 </programlisting>
         </para>
         <para>
-         The required arguments are <literal>relation</literal> with a value
-         of type <type>regclass</type>, which specifies the table; either
-         <literal>attname</literal> with a value of type <type>name</type> or
-         <literal>attnum</literal> with a value of type <type>smallint</type>,
-         which specifies the column; and <literal>inherited</literal>, which
-         specifies whether the statistics include values from child tables.
-         Other arguments are the names and values of statistics corresponding
-         to columns in <link
+         The required arguments are <literal>schemaname</literal> and
+         <literal>relname</literal> with a value of type <type>text</type>
+         which specify the table; either <literal>attname</literal> with a
+         value of type <type>text</type> or <literal>attnum</literal> with a
+         value of type <type>smallint</type>, which specifies the column; and
+         <literal>inherited</literal>, which specifies whether the statistics
+         include values from child tables.  Other arguments are the names and
+         values of statistics corresponding to columns in <link
          linkend="view-pg-stats"><structname>pg_stats</structname></link>.
         </para>
         <para>
@@ -30501,8 +30504,9 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
           <primary>pg_clear_attribute_stats</primary>
          </indexterm>
          <function>pg_clear_attribute_stats</function> (
-         <parameter>relation</parameter> <type>regclass</type>,
-         <parameter>attname</parameter> <type>name</type>,
+         <parameter>schemaname</parameter> <type>text</type>,
+         <parameter>relname</parameter> <type>text</type>,
+         <parameter>attname</parameter> <type>text</type>,
          <parameter>inherited</parameter> <type>boolean</type> )
          <returnvalue>void</returnvalue>
         </para>
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index 6bcbee0edba..f87db2d6102 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -36,7 +36,8 @@
 
 enum attribute_stats_argnum
 {
-	ATTRELATION_ARG = 0,
+	ATTRELSCHEMA_ARG = 0,
+	ATTRELNAME_ARG,
 	ATTNAME_ARG,
 	ATTNUM_ARG,
 	INHERITED_ARG,
@@ -58,8 +59,9 @@ enum attribute_stats_argnum
 
 static struct StatsArgInfo attarginfo[] =
 {
-	[ATTRELATION_ARG] = {"relation", REGCLASSOID},
-	[ATTNAME_ARG] = {"attname", NAMEOID},
+	[ATTRELSCHEMA_ARG] = {"schemaname", TEXTOID},
+	[ATTRELNAME_ARG] = {"relname", TEXTOID},
+	[ATTNAME_ARG] = {"attname", TEXTOID},
 	[ATTNUM_ARG] = {"attnum", INT2OID},
 	[INHERITED_ARG] = {"inherited", BOOLOID},
 	[NULL_FRAC_ARG] = {"null_frac", FLOAT4OID},
@@ -80,7 +82,8 @@ static struct StatsArgInfo attarginfo[] =
 
 enum clear_attribute_stats_argnum
 {
-	C_ATTRELATION_ARG = 0,
+	C_ATTRELSCHEMA_ARG = 0,
+	C_ATTRELNAME_ARG,
 	C_ATTNAME_ARG,
 	C_INHERITED_ARG,
 	C_NUM_ATTRIBUTE_STATS_ARGS
@@ -88,8 +91,9 @@ enum clear_attribute_stats_argnum
 
 static struct StatsArgInfo cleararginfo[] =
 {
-	[C_ATTRELATION_ARG] = {"relation", REGCLASSOID},
-	[C_ATTNAME_ARG] = {"attname", NAMEOID},
+	[C_ATTRELSCHEMA_ARG] = {"relation", TEXTOID},
+	[C_ATTRELNAME_ARG] = {"relation", TEXTOID},
+	[C_ATTNAME_ARG] = {"attname", TEXTOID},
 	[C_INHERITED_ARG] = {"inherited", BOOLOID},
 	[C_NUM_ATTRIBUTE_STATS_ARGS] = {0}
 };
@@ -133,6 +137,9 @@ static void init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
 static bool
 attribute_statistics_update(FunctionCallInfo fcinfo)
 {
+	char	   *nspname;
+	Oid			nspoid;
+	char	   *relname;
 	Oid			reloid;
 	char	   *attname;
 	AttrNumber	attnum;
@@ -170,8 +177,23 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 	bool		result = true;
 
-	stats_check_required_arg(fcinfo, attarginfo, ATTRELATION_ARG);
-	reloid = PG_GETARG_OID(ATTRELATION_ARG);
+	stats_check_required_arg(fcinfo, attarginfo, ATTRELSCHEMA_ARG);
+	stats_check_required_arg(fcinfo, attarginfo, ATTRELNAME_ARG);
+
+	nspname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELSCHEMA_ARG));
+	nspoid = stats_schema_check_privileges(nspname);
+	if (nspoid == InvalidOid)
+		return false;
+
+	relname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELNAME_ARG));
+	reloid = get_relname_relid(relname, nspoid);
+	if (reloid == InvalidOid)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("Relation \"%s\".\"%s\" not found.", nspname, relname)));
+		return false;
+	}
 
 	if (RecoveryInProgress())
 		ereport(ERROR,
@@ -185,21 +207,18 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	/* user can specify either attname or attnum, but not both */
 	if (!PG_ARGISNULL(ATTNAME_ARG))
 	{
-		Name		attnamename;
-
 		if (!PG_ARGISNULL(ATTNUM_ARG))
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("cannot specify both attname and attnum")));
-		attnamename = PG_GETARG_NAME(ATTNAME_ARG);
-		attname = NameStr(*attnamename);
+		attname = TextDatumGetCString(PG_GETARG_DATUM(ATTNAME_ARG));
 		attnum = get_attnum(reloid, attname);
 		/* note that this test covers attisdropped cases too: */
 		if (attnum == InvalidAttrNumber)
 			ereport(ERROR,
 					(errcode(ERRCODE_UNDEFINED_COLUMN),
-					 errmsg("column \"%s\" of relation \"%s\" does not exist",
-							attname, get_rel_name(reloid))));
+					 errmsg("column \"%s\" of relation \"%s\".\"%s\" does not exist",
+							attname, nspname, relname)));
 	}
 	else if (!PG_ARGISNULL(ATTNUM_ARG))
 	{
@@ -210,8 +229,8 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 			!SearchSysCacheExistsAttName(reloid, attname))
 			ereport(ERROR,
 					(errcode(ERRCODE_UNDEFINED_COLUMN),
-					 errmsg("column %d of relation \"%s\" does not exist",
-							attnum, get_rel_name(reloid))));
+					 errmsg("column %d of relation \"%s\".\"%s\" does not exist",
+							attnum, nspname, relname)));
 	}
 	else
 	{
@@ -900,13 +919,33 @@ init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
 Datum
 pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 {
+	char	   *nspname;
+	Oid			nspoid;
+	char	   *relname;
 	Oid			reloid;
-	Name		attname;
+	char	   *attname;
 	AttrNumber	attnum;
 	bool		inherited;
 
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELATION_ARG);
-	reloid = PG_GETARG_OID(C_ATTRELATION_ARG);
+	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELSCHEMA_ARG);
+	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELNAME_ARG);
+	stats_check_required_arg(fcinfo, cleararginfo, C_ATTNAME_ARG);
+	stats_check_required_arg(fcinfo, cleararginfo, C_INHERITED_ARG);
+
+	nspname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELSCHEMA_ARG));
+	nspoid = stats_schema_check_privileges(nspname);
+	if (!OidIsValid(nspoid))
+		return false;
+
+	relname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELNAME_ARG));
+	reloid = get_relname_relid(relname, nspoid);
+	if (!OidIsValid(reloid))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("Relation \"%s\".\"%s\" not found.", nspname, relname)));
+		return false;
+	}
 
 	if (RecoveryInProgress())
 		ereport(ERROR,
@@ -916,23 +955,21 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 
 	stats_lock_check_privileges(reloid);
 
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTNAME_ARG);
-	attname = PG_GETARG_NAME(C_ATTNAME_ARG);
-	attnum = get_attnum(reloid, NameStr(*attname));
+	attname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTNAME_ARG));
+	attnum = get_attnum(reloid, attname);
 
 	if (attnum < 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot clear statistics on system column \"%s\"",
-						NameStr(*attname))));
+						attname)));
 
 	if (attnum == InvalidAttrNumber)
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 errmsg("column \"%s\" of relation \"%s\" does not exist",
-						NameStr(*attname), get_rel_name(reloid))));
+						attname, get_rel_name(reloid))));
 
-	stats_check_required_arg(fcinfo, cleararginfo, C_INHERITED_ARG);
 	inherited = PG_GETARG_BOOL(C_INHERITED_ARG);
 
 	delete_pg_statistic(reloid, attnum, inherited);
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index 52dfa477187..fdc69bc93e2 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -19,9 +19,12 @@
 
 #include "access/heapam.h"
 #include "catalog/indexing.h"
+#include "catalog/namespace.h"
 #include "statistics/stat_utils.h"
+#include "utils/builtins.h"
 #include "utils/fmgroids.h"
 #include "utils/fmgrprotos.h"
+#include "utils/lsyscache.h"
 #include "utils/syscache.h"
 
 
@@ -32,7 +35,8 @@
 
 enum relation_stats_argnum
 {
-	RELATION_ARG = 0,
+	RELSCHEMA_ARG = 0,
+	RELNAME_ARG,
 	RELPAGES_ARG,
 	RELTUPLES_ARG,
 	RELALLVISIBLE_ARG,
@@ -42,7 +46,8 @@ enum relation_stats_argnum
 
 static struct StatsArgInfo relarginfo[] =
 {
-	[RELATION_ARG] = {"relation", REGCLASSOID},
+	[RELSCHEMA_ARG] = {"schemaname", TEXTOID},
+	[RELNAME_ARG] = {"relname", TEXTOID},
 	[RELPAGES_ARG] = {"relpages", INT4OID},
 	[RELTUPLES_ARG] = {"reltuples", FLOAT4OID},
 	[RELALLVISIBLE_ARG] = {"relallvisible", INT4OID},
@@ -59,6 +64,9 @@ static bool
 relation_statistics_update(FunctionCallInfo fcinfo)
 {
 	bool		result = true;
+	char	   *nspname;
+	Oid			nspoid;
+	char	   *relname;
 	Oid			reloid;
 	Relation	crel;
 	BlockNumber relpages = 0;
@@ -76,6 +84,32 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 	bool		nulls[4] = {0};
 	int			nreplaces = 0;
 
+	stats_check_required_arg(fcinfo, relarginfo, RELSCHEMA_ARG);
+	stats_check_required_arg(fcinfo, relarginfo, RELNAME_ARG);
+
+	nspname = TextDatumGetCString(PG_GETARG_DATUM(RELSCHEMA_ARG));
+	nspoid = stats_schema_check_privileges(nspname);
+	if (!OidIsValid(nspoid))
+		return false;
+
+	relname = TextDatumGetCString(PG_GETARG_DATUM(RELNAME_ARG));
+	reloid = get_relname_relid(relname, nspoid);
+	if (!OidIsValid(reloid))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("Relation \"%s\".\"%s\" not found.", nspname, relname)));
+		return false;
+	}
+
+	if (RecoveryInProgress())
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("recovery is in progress"),
+				 errhint("Statistics cannot be modified during recovery.")));
+
+	stats_lock_check_privileges(reloid);
+
 	if (!PG_ARGISNULL(RELPAGES_ARG))
 	{
 		relpages = PG_GETARG_UINT32(RELPAGES_ARG);
@@ -108,17 +142,6 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 		update_relallfrozen = true;
 	}
 
-	stats_check_required_arg(fcinfo, relarginfo, RELATION_ARG);
-	reloid = PG_GETARG_OID(RELATION_ARG);
-
-	if (RecoveryInProgress())
-		ereport(ERROR,
-				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
-				 errmsg("recovery is in progress"),
-				 errhint("Statistics cannot be modified during recovery.")));
-
-	stats_lock_check_privileges(reloid);
-
 	/*
 	 * Take RowExclusiveLock on pg_class, consistent with
 	 * vac_update_relstats().
@@ -187,20 +210,22 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 Datum
 pg_clear_relation_stats(PG_FUNCTION_ARGS)
 {
-	LOCAL_FCINFO(newfcinfo, 5);
+	LOCAL_FCINFO(newfcinfo, 6);
 
-	InitFunctionCallInfoData(*newfcinfo, NULL, 5, InvalidOid, NULL, NULL);
+	InitFunctionCallInfoData(*newfcinfo, NULL, 6, InvalidOid, NULL, NULL);
 
-	newfcinfo->args[0].value = PG_GETARG_OID(0);
+	newfcinfo->args[0].value = PG_GETARG_DATUM(0);
 	newfcinfo->args[0].isnull = PG_ARGISNULL(0);
-	newfcinfo->args[1].value = UInt32GetDatum(0);
-	newfcinfo->args[1].isnull = false;
-	newfcinfo->args[2].value = Float4GetDatum(-1.0);
+	newfcinfo->args[1].value = PG_GETARG_DATUM(1);
+	newfcinfo->args[1].isnull = PG_ARGISNULL(1);
+	newfcinfo->args[2].value = UInt32GetDatum(0);
 	newfcinfo->args[2].isnull = false;
-	newfcinfo->args[3].value = UInt32GetDatum(0);
+	newfcinfo->args[3].value = Float4GetDatum(-1.0);
 	newfcinfo->args[3].isnull = false;
 	newfcinfo->args[4].value = UInt32GetDatum(0);
 	newfcinfo->args[4].isnull = false;
+	newfcinfo->args[5].value = UInt32GetDatum(0);
+	newfcinfo->args[5].isnull = false;
 
 	relation_statistics_update(newfcinfo);
 	PG_RETURN_VOID();
diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c
index 9647f5108b3..b444f6871df 100644
--- a/src/backend/statistics/stat_utils.c
+++ b/src/backend/statistics/stat_utils.c
@@ -18,7 +18,9 @@
 
 #include "access/relation.h"
 #include "catalog/index.h"
+#include "catalog/namespace.h"
 #include "catalog/pg_database.h"
+#include "catalog/pg_namespace.h"
 #include "funcapi.h"
 #include "miscadmin.h"
 #include "statistics/stat_utils.h"
@@ -213,6 +215,41 @@ stats_lock_check_privileges(Oid reloid)
 	relation_close(table, NoLock);
 }
 
+
+/*
+ * Resolve a schema name into an Oid, ensure that the user has usage privs on
+ * that schema.
+ */
+Oid
+stats_schema_check_privileges(const char *nspname)
+{
+	Oid			nspoid;
+	AclResult	aclresult;
+
+	nspoid = LookupExplicitNamespace(nspname, true);
+
+	if (nspoid == InvalidOid)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_SCHEMA_NAME),
+				 errmsg("schema %s does not exist", nspname)));
+		return InvalidOid;
+	}
+
+	aclresult = object_aclcheck(NamespaceRelationId, nspoid, GetUserId(), ACL_USAGE);
+
+	if (aclresult != ACLCHECK_OK)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for schema %s", nspname)));
+		return InvalidOid;
+	}
+
+	return nspoid;
+}
+
+
 /*
  * Find the argument number for the given argument name, returning -1 if not
  * found.
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 428ed2d60fc..239664c459d 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -10498,7 +10498,6 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 	PQExpBuffer out;
 	DumpId	   *deps = NULL;
 	int			ndeps = 0;
-	char	   *qualified_name;
 	int			i_attname;
 	int			i_inherited;
 	int			i_null_frac;
@@ -10563,15 +10562,16 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 
 	out = createPQExpBuffer();
 
-	qualified_name = pg_strdup(fmtQualifiedDumpable(rsinfo));
-
 	/* restore relation stats */
 	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
 	appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
 					  fout->remoteVersion);
-	appendPQExpBufferStr(out, "\t'relation', ");
-	appendStringLiteralAH(out, qualified_name, fout);
-	appendPQExpBufferStr(out, "::regclass,\n");
+	appendPQExpBufferStr(out, "\t'schemaname', ");
+	appendStringLiteralAH(out, rsinfo->dobj.namespace->dobj.name, fout);
+	appendPQExpBufferStr(out, ",\n");
+	appendPQExpBufferStr(out, "\t'relname', ");
+	appendStringLiteralAH(out, rsinfo->dobj.name, fout);
+	appendPQExpBufferStr(out, ",\n");
 	appendPQExpBuffer(out, "\t'relpages', '%d'::integer,\n", rsinfo->relpages);
 	appendPQExpBuffer(out, "\t'reltuples', '%s'::real,\n", rsinfo->reltuples);
 	appendPQExpBuffer(out, "\t'relallvisible', '%d'::integer\n);\n",
@@ -10610,9 +10610,10 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
 		appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
 						  fout->remoteVersion);
-		appendPQExpBufferStr(out, "\t'relation', ");
-		appendStringLiteralAH(out, qualified_name, fout);
-		appendPQExpBufferStr(out, "::regclass");
+		appendPQExpBufferStr(out, "\t'schemaname', ");
+		appendStringLiteralAH(out, rsinfo->dobj.namespace->dobj.name, fout);
+		appendPQExpBufferStr(out, ",\n\t'relname', ");
+		appendStringLiteralAH(out, rsinfo->dobj.name, fout);
 
 		if (PQgetisnull(res, rownum, i_attname))
 			pg_fatal("attname cannot be NULL");
@@ -10624,7 +10625,10 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 		 * their attnames are not necessarily stable across dump/reload.
 		 */
 		if (rsinfo->nindAttNames == 0)
-			appendNamedArgument(out, fout, "attname", "name", attname);
+		{
+			appendPQExpBuffer(out, ",\n\t'attname', ");
+			appendStringLiteralAH(out, attname, fout);
+		}
 		else
 		{
 			bool		found = false;
@@ -10704,7 +10708,6 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 							  .deps = deps,
 							  .nDeps = ndeps));
 
-	free(qualified_name);
 	destroyPQExpBuffer(out);
 	destroyPQExpBuffer(query);
 }
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index d281e27aa67..d3e84f44c6c 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -4741,14 +4741,16 @@ my %tests = (
 		regexp => qr/^
 			\QSELECT * FROM pg_catalog.pg_restore_relation_stats(\E\s+
 			'version',\s'\d+'::integer,\s+
-			'relation',\s'dump_test.dup_test_post_data_ix'::regclass,\s+
+			'schemaname',\s'dump_test',\s+
+			'relname',\s'dup_test_post_data_ix',\s+
 			'relpages',\s'\d+'::integer,\s+
 			'reltuples',\s'\d+'::real,\s+
 			'relallvisible',\s'\d+'::integer\s+
 			\);\s+
 			\QSELECT * FROM pg_catalog.pg_restore_attribute_stats(\E\s+
 			'version',\s'\d+'::integer,\s+
-			'relation',\s'dump_test.dup_test_post_data_ix'::regclass,\s+
+			'schemaname',\s'dump_test',\s+
+			'relname',\s'dup_test_post_data_ix',\s+
 			'attnum',\s'2'::smallint,\s+
 			'inherited',\s'f'::boolean,\s+
 			'null_frac',\s'0'::real,\s+
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index cf381867e40..c68ff9cbf83 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -57,6 +57,6 @@
  */
 
 /*							yyyymmddN */
-#define CATALOG_VERSION_NO	202503241
+#define CATALOG_VERSION_NO	202503242
 
 #endif
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0d29ef50ff2..3f7b82e02bb 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12453,8 +12453,8 @@
   descr => 'clear statistics on relation',
   proname => 'pg_clear_relation_stats', provolatile => 'v', proisstrict => 'f',
   proparallel => 'u', prorettype => 'void',
-  proargtypes => 'regclass',
-  proargnames => '{relation}',
+  proargtypes => 'text text',
+  proargnames => '{schemaname,relname}',
   prosrc => 'pg_clear_relation_stats' },
 { oid => '8461',
   descr => 'restore statistics on attribute',
@@ -12469,8 +12469,8 @@
   descr => 'clear statistics on attribute',
   proname => 'pg_clear_attribute_stats', provolatile => 'v', proisstrict => 'f',
   proparallel => 'u', prorettype => 'void',
-  proargtypes => 'regclass name bool',
-  proargnames => '{relation,attname,inherited}',
+  proargtypes => 'text text text bool',
+  proargnames => '{schemaname,relname,attname,inherited}',
   prosrc => 'pg_clear_attribute_stats' },
 
 # GiST stratnum implementations
diff --git a/src/include/statistics/stat_utils.h b/src/include/statistics/stat_utils.h
index 0eb4decfcac..ba09b431c11 100644
--- a/src/include/statistics/stat_utils.h
+++ b/src/include/statistics/stat_utils.h
@@ -32,6 +32,8 @@ extern bool stats_check_arg_pair(FunctionCallInfo fcinfo,
 
 extern void stats_lock_check_privileges(Oid reloid);
 
+extern Oid	stats_schema_check_privileges(const char *nspname);
+
 extern bool stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
 											 FunctionCallInfo positional_fcinfo,
 											 struct StatsArgInfo *arginfo);
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index 1f46d5e7854..302e77743e3 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -14,7 +14,8 @@ CREATE TABLE stats_import.test(
 ) WITH (autovacuum_enabled = false);
 SELECT
     pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', 18::integer,
         'reltuples', 21::real,
         'relallvisible', 24::integer,
@@ -36,7 +37,7 @@ ORDER BY relname;
  test    |       18 |        21 |            24 |           27
 (1 row)
 
-SELECT pg_clear_relation_stats('stats_import.test'::regclass);
+SELECT pg_clear_relation_stats('stats_import', 'test');
  pg_clear_relation_stats 
 -------------------------
  
@@ -45,33 +46,54 @@ SELECT pg_clear_relation_stats('stats_import.test'::regclass);
 --
 -- relstats tests
 --
---- error: relation is wrong type
+-- error: schemaname missing
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::oid,
+        'relname', 'test',
         'relpages', 17::integer);
-WARNING:  argument "relation" has type "oid", expected type "regclass"
-ERROR:  "relation" cannot be NULL
+ERROR:  "schemaname" cannot be NULL
+-- error: relname missing
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relpages', 17::integer);
+ERROR:  "relname" cannot be NULL
+--- error: schemaname is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 3.6::float,
+        'relname', 'test',
+        'relpages', 17::integer);
+WARNING:  argument "schemaname" has type "double precision", expected type "text"
+ERROR:  "schemaname" cannot be NULL
+--- error: relname is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relname', 0::oid,
+        'relpages', 17::integer);
+WARNING:  argument "relname" has type "oid", expected type "text"
+ERROR:  "relname" cannot be NULL
 -- error: relation not found
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::oid::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'nope',
         'relpages', 17::integer);
-ERROR:  could not open relation with OID 0
+WARNING:  Relation "stats_import"."nope" not found.
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
 -- error: odd number of variadic arguments cannot be pairs
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relallvisible');
 ERROR:  variadic arguments must be name/value pairs
 HINT:  Provide an even number of variadic arguments that can be divided into pairs.
 -- error: argument name is NULL
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         NULL, '17'::integer);
-ERROR:  name at variadic position 3 is NULL
--- error: argument name is not a text type
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        17, '17'::integer);
-ERROR:  name at variadic position 3 has type "integer", expected type "text"
+ERROR:  name at variadic position 5 is NULL
 -- starting stats
 SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
@@ -84,7 +106,8 @@ WHERE oid = 'stats_import.test_i'::regclass;
 -- regular indexes have special case locking rules
 BEGIN;
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.test_i'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test_i',
         'relpages', 18::integer);
  pg_restore_relation_stats 
 ---------------------------
@@ -132,7 +155,8 @@ WHERE oid = 'stats_import.part_parent'::regclass;
 --
 BEGIN;
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent_i'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'part_parent_i',
         'relpages', 2::integer);
  pg_restore_relation_stats 
 ---------------------------
@@ -166,7 +190,8 @@ WHERE oid = 'stats_import.part_parent_i'::regclass;
 
 -- ok: set all relstats, with version, no bounds checking
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'version', 150000::integer,
         'relpages', '-17'::integer,
         'reltuples', 400::real,
@@ -187,7 +212,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just relpages, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', '16'::integer);
  pg_restore_relation_stats 
 ---------------------------
@@ -204,7 +230,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just reltuples, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'reltuples', '500'::real);
  pg_restore_relation_stats 
 ---------------------------
@@ -221,7 +248,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just relallvisible, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relallvisible', 5::integer);
  pg_restore_relation_stats 
 ---------------------------
@@ -238,7 +266,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: just relallfrozen
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'version', 150000::integer,
         'relallfrozen', 3::integer);
  pg_restore_relation_stats 
@@ -256,7 +285,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- warn: bad relpages type, rest updated
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', 'nope'::text,
         'reltuples', 400.0::real,
         'relallvisible', 4::integer,
@@ -277,7 +307,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- unrecognized argument name, rest ok
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', '171'::integer,
         'nope', 10::integer);
 WARNING:  unrecognized argument name: "nope"
@@ -295,8 +326,7 @@ WHERE oid = 'stats_import.test'::regclass;
 (1 row)
 
 -- ok: clear stats
-SELECT pg_catalog.pg_clear_relation_stats(
-    relation => 'stats_import.test'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'test');
  pg_clear_relation_stats 
 -------------------------
  
@@ -313,87 +343,123 @@ WHERE oid = 'stats_import.test'::regclass;
 -- invalid relkinds for statistics
 CREATE SEQUENCE stats_import.testseq;
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.testseq'::regclass);
+        'schemaname', 'stats_import',
+        'relname', 'testseq');
 ERROR:  cannot modify statistics for relation "testseq"
 DETAIL:  This operation is not supported for sequences.
-SELECT pg_catalog.pg_clear_relation_stats(
-        'stats_import.testseq'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testseq');
 ERROR:  cannot modify statistics for relation "testseq"
 DETAIL:  This operation is not supported for sequences.
 CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
-SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.testview'::regclass);
-ERROR:  cannot modify statistics for relation "testview"
-DETAIL:  This operation is not supported for views.
-SELECT pg_catalog.pg_clear_relation_stats(
-        'stats_import.testview'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testview');
 ERROR:  cannot modify statistics for relation "testview"
 DETAIL:  This operation is not supported for views.
 --
 -- attribute stats
 --
--- error: object does not exist
+-- error: schemaname missing
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relname', 'test',
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+ERROR:  "schemaname" cannot be NULL
+-- error: schema does not exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'nope',
+    'relname', 'test',
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+WARNING:  schema nope does not exist
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- error: relname missing
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+ERROR:  "relname" cannot be NULL
+-- error: relname does not exist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', '0'::oid::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'nope',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  could not open relation with OID 0
--- error: relation null
+WARNING:  Relation "stats_import"."nope" not found.
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- error: relname null
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', NULL::oid::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', NULL,
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  "relation" cannot be NULL
+ERROR:  "relname" cannot be NULL
 -- error: NULL attname
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', NULL::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', NULL,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 ERROR:  must specify either attname or attnum
 -- error: attname doesn't exist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'nope'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'nope',
     'inherited', false::boolean,
     'null_frac', 0.1::real,
     'avg_width', 2::integer,
     'n_distinct', 0.3::real);
-ERROR:  column "nope" of relation "test" does not exist
+ERROR:  column "nope" of relation "stats_import"."test" does not exist
 -- error: both attname and attnum
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'attnum', 1::smallint,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 ERROR:  cannot specify both attname and attnum
 -- error: neither attname nor attnum
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
+    'schemaname', 'stats_import',
+    'relname', 'test',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 ERROR:  must specify either attname or attnum
 -- error: attribute is system column
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'xmin'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'xmin',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 ERROR:  cannot modify statistics on system column "xmin"
 -- error: inherited null
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', NULL::boolean,
     'null_frac', 0.1::real);
 ERROR:  "inherited" cannot be NULL
 -- ok: just the fixed values, with version, no stakinds
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'version', 150000::integer,
     'null_frac', 0.2::real,
@@ -421,7 +487,8 @@ AND attname = 'id';
 -- for any stat-having relation.
 --
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
+    'schemaname', 'stats_import',
+    'relname', 'test',
     'attnum', 1::smallint,
     'inherited', false::boolean,
     'null_frac', 0.4::real);
@@ -443,8 +510,9 @@ AND attname = 'id';
 
 -- warn: unrecognized argument name, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.2::real,
     'nope', 0.5::real);
@@ -467,8 +535,9 @@ AND attname = 'id';
 
 -- warn: mcv / mcf null mismatch part 1, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.21::real,
     'most_common_freqs', '{0.1,0.2,0.3}'::real[]
@@ -492,8 +561,9 @@ AND attname = 'id';
 
 -- warn: mcv / mcf null mismatch part 2, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.21::real,
     'most_common_vals', '{1,2,3}'::text
@@ -517,8 +587,9 @@ AND attname = 'id';
 
 -- warn: mcf type mismatch, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.22::real,
     'most_common_vals', '{2,1,3}'::text,
@@ -544,8 +615,9 @@ AND attname = 'id';
 
 -- warn: mcv cast failure, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.23::real,
     'most_common_vals', '{2,four,3}'::text,
@@ -570,8 +642,9 @@ AND attname = 'id';
 
 -- ok: mcv+mcf
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'most_common_vals', '{2,1,3}'::text,
     'most_common_freqs', '{0.3,0.25,0.05}'::real[]
@@ -594,8 +667,9 @@ AND attname = 'id';
 
 -- warn: NULL in histogram array, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.24::real,
     'histogram_bounds', '{1,NULL,3,4}'::text
@@ -619,8 +693,9 @@ AND attname = 'id';
 
 -- ok: histogram_bounds
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'histogram_bounds', '{1,2,3,4}'::text
     );
@@ -642,8 +717,9 @@ AND attname = 'id';
 
 -- warn: elem_count_histogram null element, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.25::real,
     'elem_count_histogram', '{1,1,NULL,1,1,1,1,1}'::real[]
@@ -667,8 +743,9 @@ AND attname = 'tags';
 
 -- ok: elem_count_histogram
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.26::real,
     'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
@@ -691,8 +768,9 @@ AND attname = 'tags';
 
 -- warn: range stats on a scalar type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.27::real,
     'range_empty_frac', 0.5::real,
@@ -718,8 +796,9 @@ AND attname = 'id';
 
 -- warn: range_empty_frac range_length_hist null mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.28::real,
     'range_length_histogram', '{399,499,Infinity}'::text
@@ -743,8 +822,9 @@ AND attname = 'arange';
 
 -- warn: range_empty_frac range_length_hist null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.29::real,
     'range_empty_frac', 0.5::real
@@ -768,8 +848,9 @@ AND attname = 'arange';
 
 -- ok: range_empty_frac + range_length_hist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'range_empty_frac', 0.5::real,
     'range_length_histogram', '{399,499,Infinity}'::text
@@ -792,8 +873,9 @@ AND attname = 'arange';
 
 -- warn: range bounds histogram on scalar, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.31::real,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
@@ -818,8 +900,9 @@ AND attname = 'id';
 
 -- ok: range_bounds_histogram
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
     );
@@ -841,8 +924,9 @@ AND attname = 'arange';
 
 -- warn: cannot set most_common_elems for range type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.32::real,
     'most_common_elems', '{3,1}'::text,
@@ -868,8 +952,9 @@ AND attname = 'arange';
 
 -- warn: scalars can't have mcelem, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.33::real,
     'most_common_elems', '{1,3}'::text,
@@ -895,8 +980,9 @@ AND attname = 'id';
 
 -- warn: mcelem / mcelem mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.34::real,
     'most_common_elems', '{one,two}'::text
@@ -920,8 +1006,9 @@ AND attname = 'tags';
 
 -- warn: mcelem / mcelem null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.35::real,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
@@ -945,8 +1032,9 @@ AND attname = 'tags';
 
 -- ok: mcelem
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'most_common_elems', '{one,three}'::text,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
@@ -969,8 +1057,9 @@ AND attname = 'tags';
 
 -- warn: scalars can't have elem_count_histogram, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.36::real,
     'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1}'::real[]
@@ -1022,8 +1111,9 @@ SELECT s.schemaname, s.tablename, s.attname, s.inherited, r.*
 FROM pg_catalog.pg_stats AS s
 CROSS JOIN LATERAL
     pg_catalog.pg_restore_attribute_stats(
-        'relation', ('stats_import.' || s.tablename || '_clone')::regclass,
-        'attname', s.attname,
+        'schemaname', 'stats_import',
+        'relname', s.tablename::text || '_clone',
+        'attname', s.attname::text,
         'inherited', s.inherited,
         'version', 150000,
         'null_frac', s.null_frac,
@@ -1200,9 +1290,10 @@ AND attname = 'arange';
 (1 row)
 
 SELECT pg_catalog.pg_clear_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'arange'::name,
-    inherited => false::boolean);
+    schemaname => 'stats_import',
+    relname => 'test',
+    attname => 'arange',
+    inherited => false);
  pg_clear_attribute_stats 
 --------------------------
  
@@ -1219,6 +1310,52 @@ AND attname = 'arange';
      0
 (1 row)
 
+-- temp tables
+CREATE TEMP TABLE stats_temp(i int);
+SELECT pg_restore_relation_stats(
+        'schemaname', 'pg_temp',
+        'relname', 'stats_temp',
+        'relpages', '-19'::integer,
+        'reltuples', 401::real,
+        'relallvisible', 5::integer,
+        'relallfrozen', 3::integer);
+ pg_restore_relation_stats 
+---------------------------
+ t
+(1 row)
+
+SELECT relname, relpages, reltuples, relallvisible, relallfrozen
+FROM pg_class
+WHERE oid = 'pg_temp.stats_temp'::regclass
+ORDER BY relname;
+  relname   | relpages | reltuples | relallvisible | relallfrozen 
+------------+----------+-----------+---------------+--------------
+ stats_temp |      -19 |       401 |             5 |            3
+(1 row)
+
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'pg_temp',
+    'relname', 'stats_temp',
+    'attname', 'i',
+    'inherited', false::boolean,
+    'null_frac', 0.0123::real
+    );
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT tablename, null_frac
+FROM pg_stats
+WHERE schemaname like 'pg_temp%'
+AND tablename = 'stats_temp'
+AND inherited = false
+AND attname = 'i';
+ tablename  | null_frac 
+------------+-----------
+ stats_temp |    0.0123
+(1 row)
+
 DROP SCHEMA stats_import CASCADE;
 NOTICE:  drop cascades to 6 other objects
 DETAIL:  drop cascades to type stats_import.complex_type
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index 0ec590688c2..35a9a1e3e7a 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -17,7 +17,8 @@ CREATE TABLE stats_import.test(
 
 SELECT
     pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', 18::integer,
         'reltuples', 21::real,
         'relallvisible', 24::integer,
@@ -32,37 +33,52 @@ FROM pg_class
 WHERE oid = 'stats_import.test'::regclass
 ORDER BY relname;
 
-SELECT pg_clear_relation_stats('stats_import.test'::regclass);
+SELECT pg_clear_relation_stats('stats_import', 'test');
 
 --
 -- relstats tests
 --
 
---- error: relation is wrong type
+-- error: schemaname missing
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::oid,
+        'relname', 'test',
+        'relpages', 17::integer);
+
+-- error: relname missing
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relpages', 17::integer);
+
+--- error: schemaname is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 3.6::float,
+        'relname', 'test',
+        'relpages', 17::integer);
+
+--- error: relname is wrong type
+SELECT pg_catalog.pg_restore_relation_stats(
+        'schemaname', 'stats_import',
+        'relname', 0::oid,
         'relpages', 17::integer);
 
 -- error: relation not found
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 0::oid::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'nope',
         'relpages', 17::integer);
 
 -- error: odd number of variadic arguments cannot be pairs
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relallvisible');
 
 -- error: argument name is NULL
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         NULL, '17'::integer);
 
--- error: argument name is not a text type
-SELECT pg_restore_relation_stats(
-        'relation', '0'::oid::regclass,
-        17, '17'::integer);
-
 -- starting stats
 SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
@@ -71,7 +87,8 @@ WHERE oid = 'stats_import.test_i'::regclass;
 -- regular indexes have special case locking rules
 BEGIN;
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.test_i'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test_i',
         'relpages', 18::integer);
 
 SELECT mode FROM pg_locks
@@ -108,7 +125,8 @@ WHERE oid = 'stats_import.part_parent'::regclass;
 BEGIN;
 
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.part_parent_i'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'part_parent_i',
         'relpages', 2::integer);
 
 SELECT mode FROM pg_locks
@@ -127,7 +145,8 @@ WHERE oid = 'stats_import.part_parent_i'::regclass;
 
 -- ok: set all relstats, with version, no bounds checking
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'version', 150000::integer,
         'relpages', '-17'::integer,
         'reltuples', 400::real,
@@ -140,7 +159,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just relpages, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', '16'::integer);
 
 SELECT relpages, reltuples, relallvisible, relallfrozen
@@ -149,7 +169,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just reltuples, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'reltuples', '500'::real);
 
 SELECT relpages, reltuples, relallvisible, relallfrozen
@@ -158,7 +179,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: set just relallvisible, rest stay same
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relallvisible', 5::integer);
 
 SELECT relpages, reltuples, relallvisible, relallfrozen
@@ -167,7 +189,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: just relallfrozen
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'version', 150000::integer,
         'relallfrozen', 3::integer);
 
@@ -177,7 +200,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- warn: bad relpages type, rest updated
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', 'nope'::text,
         'reltuples', 400.0::real,
         'relallvisible', 4::integer,
@@ -189,7 +213,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- unrecognized argument name, rest ok
 SELECT pg_restore_relation_stats(
-        'relation', 'stats_import.test'::regclass,
+        'schemaname', 'stats_import',
+        'relname', 'test',
         'relpages', '171'::integer,
         'nope', 10::integer);
 
@@ -198,8 +223,7 @@ FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: clear stats
-SELECT pg_catalog.pg_clear_relation_stats(
-    relation => 'stats_import.test'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'test');
 
 SELECT relpages, reltuples, relallvisible
 FROM pg_class
@@ -209,48 +233,70 @@ WHERE oid = 'stats_import.test'::regclass;
 CREATE SEQUENCE stats_import.testseq;
 
 SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.testseq'::regclass);
+        'schemaname', 'stats_import',
+        'relname', 'testseq');
 
-SELECT pg_catalog.pg_clear_relation_stats(
-        'stats_import.testseq'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testseq');
 
 CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
 
-SELECT pg_catalog.pg_restore_relation_stats(
-        'relation', 'stats_import.testview'::regclass);
-
-SELECT pg_catalog.pg_clear_relation_stats(
-        'stats_import.testview'::regclass);
+SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testview');
 
 --
 -- attribute stats
 --
 
--- error: object does not exist
+-- error: schemaname missing
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'relname', 'test',
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: schema does not exist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', '0'::oid::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'nope',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: relation null
+-- error: relname missing
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', NULL::oid::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: relname does not exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'relname', 'nope',
+    'attname', 'id',
+    'inherited', false::boolean,
+    'null_frac', 0.1::real);
+
+-- error: relname null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'stats_import',
+    'relname', NULL,
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
 -- error: NULL attname
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', NULL::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', NULL,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
 -- error: attname doesn't exist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'nope'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'nope',
     'inherited', false::boolean,
     'null_frac', 0.1::real,
     'avg_width', 2::integer,
@@ -258,36 +304,41 @@ SELECT pg_catalog.pg_restore_attribute_stats(
 
 -- error: both attname and attnum
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'attnum', 1::smallint,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
 -- error: neither attname nor attnum
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
+    'schemaname', 'stats_import',
+    'relname', 'test',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
 -- error: attribute is system column
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'xmin'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'xmin',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
 -- error: inherited null
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', NULL::boolean,
     'null_frac', 0.1::real);
 
 -- ok: just the fixed values, with version, no stakinds
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'version', 150000::integer,
     'null_frac', 0.2::real,
@@ -307,7 +358,8 @@ AND attname = 'id';
 -- for any stat-having relation.
 --
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
+    'schemaname', 'stats_import',
+    'relname', 'test',
     'attnum', 1::smallint,
     'inherited', false::boolean,
     'null_frac', 0.4::real);
@@ -321,8 +373,9 @@ AND attname = 'id';
 
 -- warn: unrecognized argument name, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.2::real,
     'nope', 0.5::real);
@@ -336,8 +389,9 @@ AND attname = 'id';
 
 -- warn: mcv / mcf null mismatch part 1, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.21::real,
     'most_common_freqs', '{0.1,0.2,0.3}'::real[]
@@ -352,8 +406,9 @@ AND attname = 'id';
 
 -- warn: mcv / mcf null mismatch part 2, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.21::real,
     'most_common_vals', '{1,2,3}'::text
@@ -368,8 +423,9 @@ AND attname = 'id';
 
 -- warn: mcf type mismatch, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.22::real,
     'most_common_vals', '{2,1,3}'::text,
@@ -385,8 +441,9 @@ AND attname = 'id';
 
 -- warn: mcv cast failure, mcv-pair fails, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.23::real,
     'most_common_vals', '{2,four,3}'::text,
@@ -402,8 +459,9 @@ AND attname = 'id';
 
 -- ok: mcv+mcf
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'most_common_vals', '{2,1,3}'::text,
     'most_common_freqs', '{0.3,0.25,0.05}'::real[]
@@ -418,8 +476,9 @@ AND attname = 'id';
 
 -- warn: NULL in histogram array, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.24::real,
     'histogram_bounds', '{1,NULL,3,4}'::text
@@ -434,8 +493,9 @@ AND attname = 'id';
 
 -- ok: histogram_bounds
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'histogram_bounds', '{1,2,3,4}'::text
     );
@@ -449,8 +509,9 @@ AND attname = 'id';
 
 -- warn: elem_count_histogram null element, rest get set
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.25::real,
     'elem_count_histogram', '{1,1,NULL,1,1,1,1,1}'::real[]
@@ -465,8 +526,9 @@ AND attname = 'tags';
 
 -- ok: elem_count_histogram
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.26::real,
     'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
@@ -481,8 +543,9 @@ AND attname = 'tags';
 
 -- warn: range stats on a scalar type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.27::real,
     'range_empty_frac', 0.5::real,
@@ -498,8 +561,9 @@ AND attname = 'id';
 
 -- warn: range_empty_frac range_length_hist null mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.28::real,
     'range_length_histogram', '{399,499,Infinity}'::text
@@ -514,8 +578,9 @@ AND attname = 'arange';
 
 -- warn: range_empty_frac range_length_hist null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.29::real,
     'range_empty_frac', 0.5::real
@@ -530,8 +595,9 @@ AND attname = 'arange';
 
 -- ok: range_empty_frac + range_length_hist
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'range_empty_frac', 0.5::real,
     'range_length_histogram', '{399,499,Infinity}'::text
@@ -546,8 +612,9 @@ AND attname = 'arange';
 
 -- warn: range bounds histogram on scalar, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.31::real,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
@@ -562,8 +629,9 @@ AND attname = 'id';
 
 -- ok: range_bounds_histogram
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
     );
@@ -577,8 +645,9 @@ AND attname = 'arange';
 
 -- warn: cannot set most_common_elems for range type, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'arange'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'arange',
     'inherited', false::boolean,
     'null_frac', 0.32::real,
     'most_common_elems', '{3,1}'::text,
@@ -594,8 +663,9 @@ AND attname = 'arange';
 
 -- warn: scalars can't have mcelem, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.33::real,
     'most_common_elems', '{1,3}'::text,
@@ -611,8 +681,9 @@ AND attname = 'id';
 
 -- warn: mcelem / mcelem mismatch, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.34::real,
     'most_common_elems', '{one,two}'::text
@@ -627,8 +698,9 @@ AND attname = 'tags';
 
 -- warn: mcelem / mcelem null mismatch part 2, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'null_frac', 0.35::real,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
@@ -643,8 +715,9 @@ AND attname = 'tags';
 
 -- ok: mcelem
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'tags'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'tags',
     'inherited', false::boolean,
     'most_common_elems', '{one,three}'::text,
     'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
@@ -659,8 +732,9 @@ AND attname = 'tags';
 
 -- warn: scalars can't have elem_count_histogram, rest ok
 SELECT pg_catalog.pg_restore_attribute_stats(
-    'relation', 'stats_import.test'::regclass,
-    'attname', 'id'::name,
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.36::real,
     'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1}'::real[]
@@ -707,8 +781,9 @@ SELECT s.schemaname, s.tablename, s.attname, s.inherited, r.*
 FROM pg_catalog.pg_stats AS s
 CROSS JOIN LATERAL
     pg_catalog.pg_restore_attribute_stats(
-        'relation', ('stats_import.' || s.tablename || '_clone')::regclass,
-        'attname', s.attname,
+        'schemaname', 'stats_import',
+        'relname', s.tablename::text || '_clone',
+        'attname', s.attname::text,
         'inherited', s.inherited,
         'version', 150000,
         'null_frac', s.null_frac,
@@ -853,9 +928,10 @@ AND inherited = false
 AND attname = 'arange';
 
 SELECT pg_catalog.pg_clear_attribute_stats(
-    relation => 'stats_import.test'::regclass,
-    attname => 'arange'::name,
-    inherited => false::boolean);
+    schemaname => 'stats_import',
+    relname => 'test',
+    attname => 'arange',
+    inherited => false);
 
 SELECT COUNT(*)
 FROM pg_stats
@@ -864,4 +940,34 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'arange';
 
+-- temp tables
+CREATE TEMP TABLE stats_temp(i int);
+SELECT pg_restore_relation_stats(
+        'schemaname', 'pg_temp',
+        'relname', 'stats_temp',
+        'relpages', '-19'::integer,
+        'reltuples', 401::real,
+        'relallvisible', 5::integer,
+        'relallfrozen', 3::integer);
+
+SELECT relname, relpages, reltuples, relallvisible, relallfrozen
+FROM pg_class
+WHERE oid = 'pg_temp.stats_temp'::regclass
+ORDER BY relname;
+
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'schemaname', 'pg_temp',
+    'relname', 'stats_temp',
+    'attname', 'i',
+    'inherited', false::boolean,
+    'null_frac', 0.0123::real
+    );
+
+SELECT tablename, null_frac
+FROM pg_stats
+WHERE schemaname like 'pg_temp%'
+AND tablename = 'stats_temp'
+AND inherited = false
+AND attname = 'i';
+
 DROP SCHEMA stats_import CASCADE;
-- 
2.34.1

#495Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#494)
Re: Statistics Import and Export

* Changed to use LookupExplicitNamespace()

Seems good.

* Added test for temp tables

+1

* Doc fixes

So this patch swings the pendulum a bit back towards accepting some things
as errors. That's understandable, as we're never going to have a situation
where we can guarantee that the restore functions never generate an error,
so the best we can do is to draw the error-versus-warning line at a place
that:

* doesn't mess up flawed restores that we would otherwise expect to
complete at least partially
* is easy for us to understand
* is easy for us to explain
* we can live with for the next couple of decades

I don't know where that line should be drawn, so if people are happy with
Jeff's demarcation, then less roll with it.

#496Robert Treat
rob@xzilla.net
In reply to: Jeff Davis (#493)
Re: Statistics Import and Export

On Tue, Mar 25, 2025 at 1:32 AM Jeff Davis <pgsql@j-davis.com> wrote:

On Sat, 2025-03-08 at 14:09 -0500, Corey Huinker wrote:

except it is perfectly clear that you *asked for* data and
statistics, so you get what you asked for. however the user
conjures in their heads what they are looking for, the logic is
simple, you get what you asked for.

They *asked for* that because they didn't have the mechanism to say
"hold the mayo" or "everything except pickles". That's reducing their
choice, and then blaming them for their choice.

Can we reach a decision here and move forward?

AFAIK the issue has been settled, or at the least we've agreed to move on.

Robert Treat
https://xzilla.net

#497Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#495)
Re: Statistics Import and Export

On Tue, 2025-03-25 at 10:53 -0400, Corey Huinker wrote:

So this patch swings the pendulum a bit back towards accepting some
things as errors.

Not exactly. I see patch 0001 as a change to the function signatures
from regclass to schemaname/relname, both for usability as well as
control over ERROR vs WARNING.

There's agreement to do so, so I went ahead and committed that part.

the best we can do is to draw the error-versus-warning line at a
place that:

* doesn't mess up flawed restores that we would otherwise expect to
complete at least partially
* is easy for us to understand
* is easy for us to explain
* we can live with for the next couple of decades

The original reason we wanted to issue warnings was to allow ourselves
a chance to change the meaning of parameters, add new parameters, or
even remove parameters without causing restore failures. If there are
any ERRORs that might limit our flexibility I think we should downgrade
those to WARNINGs.

Also, out of a sense of paranoia, it might be good to downgrade some
other ERRORs to WARNINGs, like in 0002. I don't think it's quite as
important as you seem to think, however. It doesn't make a lot of
difference unless the user is running restore with --single-transaction
or --exit-on-error, in which case they probably don't want the restore
to continue if something unexpected happens. I'm fine having the
discussion, though, or we can wait until beta to see what kinds of
problems people encounter.

Regards,
Jeff Davis

#498Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#497)
Re: Statistics Import and Export

The original reason we wanted to issue warnings was to allow ourselves
a chance to change the meaning of parameters, add new parameters, or
even remove parameters without causing restore failures. If there are
any ERRORs that might limit our flexibility I think we should downgrade
those to WARNINGs.

+1

Also, out of a sense of paranoia, it might be good to downgrade some
other ERRORs to WARNINGs, like in 0002. I don't think it's quite as
important as you seem to think, however. It doesn't make a lot of
difference unless the user is running restore with --single-transaction
or --exit-on-error, in which case they probably don't want the restore
to continue if something unexpected happens. I'm fine having the
discussion, though, or we can wait until beta to see what kinds of
problems people encounter.

At this point, I feel I've demonstrated the limit of what can be made into
WARNINGs, giving us a range of options for now and into the beta. I'll
rebase and move the 0002 patch to be in last position so as to tee up
0003-0004 for consideration.

#499Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#498)
4 attachment(s)
Re: Statistics Import and Export

At this point, I feel I've demonstrated the limit of what can be made into
WARNINGs, giving us a range of options for now and into the beta. I'll
rebase and move the 0002 patch to be in last position so as to tee up
0003-0004 for consideration.

And here's the rebase (after bde2fb797aaebcbe06bf60f330ba5a068f17dda7).

The order of the patches is different, but the purpose of each is the same
as before.

Attachments:

v10-0002-Batching-getAttributeStats.patchtext/x-patch; charset=US-ASCII; name=v10-0002-Batching-getAttributeStats.patchDownload
From 1f9b2578f55fa1233121bcf5949a6f69d6cf8cee Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Fri, 14 Mar 2025 03:54:26 -0400
Subject: [PATCH v10 2/4] Batching getAttributeStats().

The prepared statement getAttributeStats() is fairly heavyweight and
could greatly increase pg_dump/pg_upgrade runtime. To alleviate this,
create a result set buffer of all of the attribute stats fetched for a
batch of 100 relations that could potentially have stats.

The query ensures that the order of results exactly matches the needs of
the code walking the TOC to print the stats calls.
---
 src/bin/pg_dump/pg_dump.c | 554 ++++++++++++++++++++++++++------------
 1 file changed, 383 insertions(+), 171 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 224dc8c9330..e3f2dac33ec 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -143,6 +143,25 @@ typedef enum OidOptions
 	zeroAsNone = 4,
 } OidOptions;
 
+typedef enum StatsBufferState
+{
+	STATSBUF_UNINITIALIZED = 0,
+	STATSBUF_ACTIVE,
+	STATSBUF_EXHAUSTED
+}			StatsBufferState;
+
+typedef struct
+{
+	PGresult   *res;			/* results from most recent
+								 * getAttributeStats() */
+	int			idx;			/* first un-consumed row of results */
+	TocEntry   *te;				/* next TOC entry to search for statsitics
+								 * data */
+
+	StatsBufferState state;		/* current state of the buffer */
+}			AttributeStatsBuffer;
+
+
 /* global decls */
 static bool dosync = true;		/* Issue fsync() to make dump durable on disk. */
 
@@ -209,6 +228,18 @@ static int	nbinaryUpgradeClassOids = 0;
 static SequenceItem *sequences = NULL;
 static int	nsequences = 0;
 
+static AttributeStatsBuffer attrstats =
+{
+	NULL, 0, NULL, STATSBUF_UNINITIALIZED
+};
+
+/*
+ * The maximum number of relations that should be fetched in any one
+ * getAttributeStats() call.
+ */
+
+#define MAX_ATTR_STATS_RELS 100
+
 /*
  * The default number of rows per INSERT when
  * --inserts is specified without --rows-per-insert
@@ -222,6 +253,8 @@ static int	nsequences = 0;
  */
 #define MAX_BLOBS_PER_ARCHIVE_ENTRY 1000
 
+
+
 /*
  * Macro for producing quoted, schema-qualified name of a dumpable object.
  */
@@ -399,6 +432,9 @@ static void setupDumpWorker(Archive *AH);
 static TableInfo *getRootTableInfo(const TableInfo *tbinfo);
 static bool forcePartitionRootLoad(const TableInfo *tbinfo);
 static void read_dump_filters(const char *filename, DumpOptions *dopt);
+static void appendNamedArgument(PQExpBuffer out, Archive *fout,
+								const char *argname, const char *argtype,
+								const char *argval);
 
 
 int
@@ -10520,7 +10556,286 @@ statisticsDumpSection(const RelStatsInfo *rsinfo)
 }
 
 /*
- * printDumpRelationStats --
+ * Fetch next batch of rows from getAttributeStats()
+ */
+static void
+fetchNextAttributeStats(Archive *fout)
+{
+	ArchiveHandle *AH = (ArchiveHandle *) fout;
+	PQExpBufferData schemas;
+	PQExpBufferData relations;
+	int			numoids = 0;
+
+	Assert(AH != NULL);
+
+	/* free last result set, if any */
+	if (attrstats.state == STATSBUF_ACTIVE)
+		PQclear(attrstats.res);
+
+	/* If we have looped around to the start of the TOC, restart */
+	if (attrstats.te == AH->toc)
+		attrstats.te = AH->toc->next;
+
+	initPQExpBuffer(&schemas);
+	initPQExpBuffer(&relations);
+
+	/*
+	 * Walk ahead looking for relstats entries that are active in this
+	 * section, adding the names to the schemas and relations lists.
+	 */
+	while ((attrstats.te != AH->toc) && (numoids < MAX_ATTR_STATS_RELS))
+	{
+		if (attrstats.te->reqs != 0 &&
+			strcmp(attrstats.te->desc, "STATISTICS DATA") == 0)
+		{
+			RelStatsInfo *rsinfo = (RelStatsInfo *) attrstats.te->createDumperArg;
+
+			Assert(rsinfo != NULL);
+
+			if (numoids > 0)
+			{
+				appendPQExpBufferStr(&schemas, ",");
+				appendPQExpBufferStr(&relations, ",");
+			}
+			appendPQExpBufferStr(&schemas, fmtId(rsinfo->dobj.namespace->dobj.name));
+			appendPQExpBufferStr(&relations, fmtId(rsinfo->dobj.name));
+			numoids++;
+		}
+
+		attrstats.te = attrstats.te->next;
+	}
+
+	if (numoids > 0)
+	{
+		PQExpBufferData query;
+
+		initPQExpBuffer(&query);
+		appendPQExpBuffer(&query,
+						  "EXECUTE getAttributeStats('{%s}'::pg_catalog.text[],'{%s}'::pg_catalog.text[])",
+						  schemas.data, relations.data);
+		attrstats.res = ExecuteSqlQuery(fout, query.data, PGRES_TUPLES_OK);
+		attrstats.idx = 0;
+	}
+	else
+	{
+		attrstats.state = STATSBUF_EXHAUSTED;
+		attrstats.res = NULL;
+		attrstats.idx = -1;
+	}
+
+	termPQExpBuffer(&schemas);
+	termPQExpBuffer(&relations);
+}
+
+/*
+ * Prepare the getAttributeStats() statement
+ *
+ * This is done automatically if the user specified dumpStatistics.
+ */
+static void
+initAttributeStats(Archive *fout)
+{
+	ArchiveHandle *AH = (ArchiveHandle *) fout;
+	PQExpBufferData query;
+
+	Assert(AH != NULL);
+	initPQExpBuffer(&query);
+
+	appendPQExpBufferStr(&query,
+						 "PREPARE getAttributeStats(pg_catalog.text[], pg_catalog.text[]) AS\n"
+						 "SELECT s.schemaname, s.tablename, s.attname, s.inherited, "
+						 "s.null_frac, s.avg_width, s.n_distinct, s.most_common_vals, "
+						 "s.most_common_freqs, s.histogram_bounds, s.correlation, "
+						 "s.most_common_elems, s.most_common_elem_freqs, "
+						 "s.elem_count_histogram, ");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(&query,
+							 "s.range_length_histogram, "
+							 "s.range_empty_frac, "
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(&query,
+							 "NULL AS range_length_histogram, "
+							 "NULL AS range_empty_frac, "
+							 " NULL AS range_bounds_histogram ");
+
+	/*
+	 * The results must be in the order of relations supplied in the
+	 * parameters to ensure that they are in sync with a walk of the TOC.
+	 *
+	 * The redundant (and incomplete) filter clause on s.tablename = ANY(...)
+	 * is a way to lead the query into using the index
+	 * pg_class_relname_nsp_index which in turn allows the planner to avoid an
+	 * expensive full scan of pg_stats.
+	 *
+	 * We may need to adjust this query for versions that are not so easily
+	 * led.
+	 */
+	appendPQExpBufferStr(&query,
+						 "FROM pg_catalog.pg_stats AS s "
+						 "JOIN unnest($1, $2) WITH ORDINALITY AS u(schemaname, tablename, ord) "
+						 "ON s.schemaname = u.schemaname "
+						 "AND s.tablename = u.tablename "
+						 "WHERE s.tablename = ANY($2) "
+						 "ORDER BY u.ord, s.attname, s.inherited");
+
+	ExecuteSqlStatement(fout, query.data);
+
+	termPQExpBuffer(&query);
+
+	attrstats.te = AH->toc->next;
+
+	fetchNextAttributeStats(fout);
+
+	attrstats.state = STATSBUF_ACTIVE;
+}
+
+
+/*
+ * append a single attribute stat to the buffer for this relation.
+ */
+static void
+appendAttributeStats(Archive *fout, PQExpBuffer out,
+					 const RelStatsInfo *rsinfo)
+{
+	PGresult   *res = attrstats.res;
+	int			tup_num = attrstats.idx;
+
+	const char *attname;
+
+	static bool indexes_set = false;
+	static int	i_attname,
+				i_inherited,
+				i_null_frac,
+				i_avg_width,
+				i_n_distinct,
+				i_most_common_vals,
+				i_most_common_freqs,
+				i_histogram_bounds,
+				i_correlation,
+				i_most_common_elems,
+				i_most_common_elem_freqs,
+				i_elem_count_histogram,
+				i_range_length_histogram,
+				i_range_empty_frac,
+				i_range_bounds_histogram;
+
+	if (!indexes_set)
+	{
+		/*
+		 * It's a prepared statement, so the indexes will be the same for all
+		 * result sets, so we only need to set them once.
+		 */
+		i_attname = PQfnumber(res, "attname");
+		i_inherited = PQfnumber(res, "inherited");
+		i_null_frac = PQfnumber(res, "null_frac");
+		i_avg_width = PQfnumber(res, "avg_width");
+		i_n_distinct = PQfnumber(res, "n_distinct");
+		i_most_common_vals = PQfnumber(res, "most_common_vals");
+		i_most_common_freqs = PQfnumber(res, "most_common_freqs");
+		i_histogram_bounds = PQfnumber(res, "histogram_bounds");
+		i_correlation = PQfnumber(res, "correlation");
+		i_most_common_elems = PQfnumber(res, "most_common_elems");
+		i_most_common_elem_freqs = PQfnumber(res, "most_common_elem_freqs");
+		i_elem_count_histogram = PQfnumber(res, "elem_count_histogram");
+		i_range_length_histogram = PQfnumber(res, "range_length_histogram");
+		i_range_empty_frac = PQfnumber(res, "range_empty_frac");
+		i_range_bounds_histogram = PQfnumber(res, "range_bounds_histogram");
+		indexes_set = true;
+	}
+
+	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+	appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
+					  fout->remoteVersion);
+	appendPQExpBufferStr(out, "\t'schemaname', ");
+	appendStringLiteralAH(out, rsinfo->dobj.namespace->dobj.name, fout);
+	appendPQExpBufferStr(out, ",\n\t'relname', ");
+	appendStringLiteralAH(out, rsinfo->dobj.name, fout);
+
+	if (PQgetisnull(res, tup_num, i_attname))
+		pg_fatal("attname cannot be NULL");
+	attname = PQgetvalue(res, tup_num, i_attname);
+
+	/*
+	 * Indexes look up attname in indAttNames to derive attnum, all others use
+	 * attname directly.  We must specify attnum for indexes, since their
+	 * attnames are not necessarily stable across dump/reload.
+	 */
+	if (rsinfo->nindAttNames == 0)
+	{
+		appendPQExpBuffer(out, ",\n\t'attname', ");
+		appendStringLiteralAH(out, attname, fout);
+	}
+	else
+	{
+		bool		found = false;
+
+		for (int i = 0; i < rsinfo->nindAttNames; i++)
+			if (strcmp(attname, rsinfo->indAttNames[i]) == 0)
+			{
+				appendPQExpBuffer(out, ",\n\t'attnum', '%d'::smallint",
+								  i + 1);
+				found = true;
+				break;
+			}
+
+		if (!found)
+			pg_fatal("could not find index attname \"%s\"", attname);
+	}
+
+	if (!PQgetisnull(res, tup_num, i_inherited))
+		appendNamedArgument(out, fout, "inherited", "boolean",
+							PQgetvalue(res, tup_num, i_inherited));
+	if (!PQgetisnull(res, tup_num, i_null_frac))
+		appendNamedArgument(out, fout, "null_frac", "real",
+							PQgetvalue(res, tup_num, i_null_frac));
+	if (!PQgetisnull(res, tup_num, i_avg_width))
+		appendNamedArgument(out, fout, "avg_width", "integer",
+							PQgetvalue(res, tup_num, i_avg_width));
+	if (!PQgetisnull(res, tup_num, i_n_distinct))
+		appendNamedArgument(out, fout, "n_distinct", "real",
+							PQgetvalue(res, tup_num, i_n_distinct));
+	if (!PQgetisnull(res, tup_num, i_most_common_vals))
+		appendNamedArgument(out, fout, "most_common_vals", "text",
+							PQgetvalue(res, tup_num, i_most_common_vals));
+	if (!PQgetisnull(res, tup_num, i_most_common_freqs))
+		appendNamedArgument(out, fout, "most_common_freqs", "real[]",
+							PQgetvalue(res, tup_num, i_most_common_freqs));
+	if (!PQgetisnull(res, tup_num, i_histogram_bounds))
+		appendNamedArgument(out, fout, "histogram_bounds", "text",
+							PQgetvalue(res, tup_num, i_histogram_bounds));
+	if (!PQgetisnull(res, tup_num, i_correlation))
+		appendNamedArgument(out, fout, "correlation", "real",
+							PQgetvalue(res, tup_num, i_correlation));
+	if (!PQgetisnull(res, tup_num, i_most_common_elems))
+		appendNamedArgument(out, fout, "most_common_elems", "text",
+							PQgetvalue(res, tup_num, i_most_common_elems));
+	if (!PQgetisnull(res, tup_num, i_most_common_elem_freqs))
+		appendNamedArgument(out, fout, "most_common_elem_freqs", "real[]",
+							PQgetvalue(res, tup_num, i_most_common_elem_freqs));
+	if (!PQgetisnull(res, tup_num, i_elem_count_histogram))
+		appendNamedArgument(out, fout, "elem_count_histogram", "real[]",
+							PQgetvalue(res, tup_num, i_elem_count_histogram));
+	if (fout->remoteVersion >= 170000)
+	{
+		if (!PQgetisnull(res, tup_num, i_range_length_histogram))
+			appendNamedArgument(out, fout, "range_length_histogram", "text",
+								PQgetvalue(res, tup_num, i_range_length_histogram));
+		if (!PQgetisnull(res, tup_num, i_range_empty_frac))
+			appendNamedArgument(out, fout, "range_empty_frac", "real",
+								PQgetvalue(res, tup_num, i_range_empty_frac));
+		if (!PQgetisnull(res, tup_num, i_range_bounds_histogram))
+			appendNamedArgument(out, fout, "range_bounds_histogram", "text",
+								PQgetvalue(res, tup_num, i_range_bounds_histogram));
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+
+
+/*
+ * printRelationStats --
  *
  * Generate the SQL statements needed to restore a relation's statistics.
  */
@@ -10528,64 +10843,21 @@ static char *
 printRelationStats(Archive *fout, const void *userArg)
 {
 	const RelStatsInfo *rsinfo = (RelStatsInfo *) userArg;
-	const DumpableObject *dobj = &rsinfo->dobj;
+	const DumpableObject *dobj;
+	const char *relschema;
+	const char *relname;
+
+	ArchiveHandle *AH = (ArchiveHandle *) fout;
 
-	PQExpBufferData query;
 	PQExpBufferData out;
 
-	PGresult   *res;
-
-	static bool first_query = true;
-	static int	i_attname;
-	static int	i_inherited;
-	static int	i_null_frac;
-	static int	i_avg_width;
-	static int	i_n_distinct;
-	static int	i_most_common_vals;
-	static int	i_most_common_freqs;
-	static int	i_histogram_bounds;
-	static int	i_correlation;
-	static int	i_most_common_elems;
-	static int	i_most_common_elem_freqs;
-	static int	i_elem_count_histogram;
-	static int	i_range_length_histogram;
-	static int	i_range_empty_frac;
-	static int	i_range_bounds_histogram;
-
-	initPQExpBuffer(&query);
-
-	if (first_query)
-	{
-		appendPQExpBufferStr(&query,
-							 "PREPARE getAttributeStats(pg_catalog.text, pg_catalog.text) AS\n"
-							 "SELECT s.attname, s.inherited, "
-							 "s.null_frac, s.avg_width, s.n_distinct, "
-							 "s.most_common_vals, s.most_common_freqs, "
-							 "s.histogram_bounds, s.correlation, "
-							 "s.most_common_elems, s.most_common_elem_freqs, "
-							 "s.elem_count_histogram, ");
-
-		if (fout->remoteVersion >= 170000)
-			appendPQExpBufferStr(&query,
-								 "s.range_length_histogram, "
-								 "s.range_empty_frac, "
-								 "s.range_bounds_histogram ");
-		else
-			appendPQExpBufferStr(&query,
-								 "NULL AS range_length_histogram,"
-								 "NULL AS range_empty_frac,"
-								 "NULL AS range_bounds_histogram ");
-
-		appendPQExpBufferStr(&query,
-							 "FROM pg_catalog.pg_stats s "
-							 "WHERE s.schemaname = $1 "
-							 "AND s.tablename = $2 "
-							 "ORDER BY s.attname, s.inherited");
-
-		ExecuteSqlStatement(fout, query.data);
-
-		resetPQExpBuffer(&query);
-	}
+	Assert(rsinfo != NULL);
+	dobj = &rsinfo->dobj;
+	Assert(dobj != NULL);
+	relschema = dobj->namespace->dobj.name;
+	Assert(relschema != NULL);
+	relname = dobj->name;
+	Assert(relname != NULL);
 
 	initPQExpBuffer(&out);
 
@@ -10604,132 +10876,72 @@ printRelationStats(Archive *fout, const void *userArg)
 	appendPQExpBuffer(&out, "\t'relallvisible', '%d'::integer\n);\n",
 					  rsinfo->relallvisible);
 
-	/* fetch attribute stats */
-	appendPQExpBufferStr(&query, "EXECUTE getAttributeStats(");
-	appendStringLiteralAH(&query, dobj->namespace->dobj.name, fout);
-	appendPQExpBufferStr(&query, ", ");
-	appendStringLiteralAH(&query, dobj->name, fout);
-	appendPQExpBufferStr(&query, ")");
+	AH->txnCount++;
 
-	res = ExecuteSqlQuery(fout, query.data, PGRES_TUPLES_OK);
+	if (attrstats.state == STATSBUF_UNINITIALIZED)
+		initAttributeStats(fout);
 
-	if (first_query)
+	/*
+	 * Because the query returns rows in the same order as the relations
+	 * requested, and because every relation gets at least one row in the
+	 * result set, the first row for this relation must correspond either to
+	 * the current row of this result set (if one exists) or the first row of
+	 * the next result set (if this one is already consumed).
+	 */
+	if (attrstats.state != STATSBUF_ACTIVE)
+		pg_fatal("Exhausted getAttributeStats() before processing %s.%s",
+				 rsinfo->dobj.namespace->dobj.name,
+				 rsinfo->dobj.name);
+
+	/*
+	 * If the current result set has been fully consumed, then the row(s) we
+	 * need (if any) would be found in the next one. This will update
+	 * attrstats.res and attrstats.idx.
+	 */
+	if (PQntuples(attrstats.res) <= attrstats.idx)
+		fetchNextAttributeStats(fout);
+
+	while (true)
 	{
-		i_attname = PQfnumber(res, "attname");
-		i_inherited = PQfnumber(res, "inherited");
-		i_null_frac = PQfnumber(res, "null_frac");
-		i_avg_width = PQfnumber(res, "avg_width");
-		i_n_distinct = PQfnumber(res, "n_distinct");
-		i_most_common_vals = PQfnumber(res, "most_common_vals");
-		i_most_common_freqs = PQfnumber(res, "most_common_freqs");
-		i_histogram_bounds = PQfnumber(res, "histogram_bounds");
-		i_correlation = PQfnumber(res, "correlation");
-		i_most_common_elems = PQfnumber(res, "most_common_elems");
-		i_most_common_elem_freqs = PQfnumber(res, "most_common_elem_freqs");
-		i_elem_count_histogram = PQfnumber(res, "elem_count_histogram");
-		i_range_length_histogram = PQfnumber(res, "range_length_histogram");
-		i_range_empty_frac = PQfnumber(res, "range_empty_frac");
-		i_range_bounds_histogram = PQfnumber(res, "range_bounds_histogram");
-		first_query = false;
-	}
-
-	/* restore attribute stats */
-	for (int rownum = 0; rownum < PQntuples(res); rownum++)
-	{
-		const char *attname;
-
-		appendPQExpBufferStr(&out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
-		appendPQExpBuffer(&out, "\t'version', '%u'::integer,\n",
-						  fout->remoteVersion);
-		appendPQExpBufferStr(&out, "\t'schemaname', ");
-		appendStringLiteralAH(&out, rsinfo->dobj.namespace->dobj.name, fout);
-		appendPQExpBufferStr(&out, ",\n\t'relname', ");
-		appendStringLiteralAH(&out, rsinfo->dobj.name, fout);
-
-		if (PQgetisnull(res, rownum, i_attname))
-			pg_fatal("attname cannot be NULL");
-		attname = PQgetvalue(res, rownum, i_attname);
+		int			i_schemaname;
+		int			i_tablename;
+		char	   *schemaname;
+		char	   *tablename;	/* misnomer, following pg_stats naming */
 
 		/*
-		 * Indexes look up attname in indAttNames to derive attnum, all others
-		 * use attname directly.  We must specify attnum for indexes, since
-		 * their attnames are not necessarily stable across dump/reload.
+		 * If we hit the end of the result set, then there are no more records
+		 * for this relation, so we should stop, but first get the next result
+		 * set for the next batch of relations.
 		 */
-		if (rsinfo->nindAttNames == 0)
+		if (PQntuples(attrstats.res) <= attrstats.idx)
 		{
-			appendPQExpBuffer(&out, ",\n\t'attname', ");
-			appendStringLiteralAH(&out, attname, fout);
-		}
-		else
-		{
-			bool		found = false;
-
-			for (int i = 0; i < rsinfo->nindAttNames; i++)
-			{
-				if (strcmp(attname, rsinfo->indAttNames[i]) == 0)
-				{
-					appendPQExpBuffer(&out, ",\n\t'attnum', '%d'::smallint",
-									  i + 1);
-					found = true;
-					break;
-				}
-			}
-
-			if (!found)
-				pg_fatal("could not find index attname \"%s\"", attname);
+			fetchNextAttributeStats(fout);
+			break;
 		}
 
-		if (!PQgetisnull(res, rownum, i_inherited))
-			appendNamedArgument(&out, fout, "inherited", "boolean",
-								PQgetvalue(res, rownum, i_inherited));
-		if (!PQgetisnull(res, rownum, i_null_frac))
-			appendNamedArgument(&out, fout, "null_frac", "real",
-								PQgetvalue(res, rownum, i_null_frac));
-		if (!PQgetisnull(res, rownum, i_avg_width))
-			appendNamedArgument(&out, fout, "avg_width", "integer",
-								PQgetvalue(res, rownum, i_avg_width));
-		if (!PQgetisnull(res, rownum, i_n_distinct))
-			appendNamedArgument(&out, fout, "n_distinct", "real",
-								PQgetvalue(res, rownum, i_n_distinct));
-		if (!PQgetisnull(res, rownum, i_most_common_vals))
-			appendNamedArgument(&out, fout, "most_common_vals", "text",
-								PQgetvalue(res, rownum, i_most_common_vals));
-		if (!PQgetisnull(res, rownum, i_most_common_freqs))
-			appendNamedArgument(&out, fout, "most_common_freqs", "real[]",
-								PQgetvalue(res, rownum, i_most_common_freqs));
-		if (!PQgetisnull(res, rownum, i_histogram_bounds))
-			appendNamedArgument(&out, fout, "histogram_bounds", "text",
-								PQgetvalue(res, rownum, i_histogram_bounds));
-		if (!PQgetisnull(res, rownum, i_correlation))
-			appendNamedArgument(&out, fout, "correlation", "real",
-								PQgetvalue(res, rownum, i_correlation));
-		if (!PQgetisnull(res, rownum, i_most_common_elems))
-			appendNamedArgument(&out, fout, "most_common_elems", "text",
-								PQgetvalue(res, rownum, i_most_common_elems));
-		if (!PQgetisnull(res, rownum, i_most_common_elem_freqs))
-			appendNamedArgument(&out, fout, "most_common_elem_freqs", "real[]",
-								PQgetvalue(res, rownum, i_most_common_elem_freqs));
-		if (!PQgetisnull(res, rownum, i_elem_count_histogram))
-			appendNamedArgument(&out, fout, "elem_count_histogram", "real[]",
-								PQgetvalue(res, rownum, i_elem_count_histogram));
-		if (fout->remoteVersion >= 170000)
-		{
-			if (!PQgetisnull(res, rownum, i_range_length_histogram))
-				appendNamedArgument(&out, fout, "range_length_histogram", "text",
-									PQgetvalue(res, rownum, i_range_length_histogram));
-			if (!PQgetisnull(res, rownum, i_range_empty_frac))
-				appendNamedArgument(&out, fout, "range_empty_frac", "real",
-									PQgetvalue(res, rownum, i_range_empty_frac));
-			if (!PQgetisnull(res, rownum, i_range_bounds_histogram))
-				appendNamedArgument(&out, fout, "range_bounds_histogram", "text",
-									PQgetvalue(res, rownum, i_range_bounds_histogram));
-		}
-		appendPQExpBufferStr(&out, "\n);\n");
+		i_schemaname = PQfnumber(attrstats.res, "schemaname");
+		Assert(i_schemaname >= 0);
+		i_tablename = PQfnumber(attrstats.res, "tablename");
+		Assert(i_tablename >= 0);
+
+		if (PQgetisnull(attrstats.res, attrstats.idx, i_schemaname))
+			pg_fatal("getAttributeStats() schemaname cannot be NULL");
+
+		if (PQgetisnull(attrstats.res, attrstats.idx, i_tablename))
+			pg_fatal("getAttributeStats() tablename cannot be NULL");
+
+		schemaname = PQgetvalue(attrstats.res, attrstats.idx, i_schemaname);
+		tablename = PQgetvalue(attrstats.res, attrstats.idx, i_tablename);
+
+		/* stop if current stat row isn't for this relation */
+		if (strcmp(relname, tablename) != 0 || strcmp(relschema, schemaname) != 0)
+			break;
+
+		appendAttributeStats(fout, &out, rsinfo);
+		AH->txnCount++;
+		attrstats.idx++;
 	}
 
-	PQclear(res);
-
-	termPQExpBuffer(&query);
 	return out.data;
 }
 
-- 
2.49.0

v10-0004-Downgrade-many-pg_restore_-_stats-errors-to-warn.patchtext/x-patch; charset=US-ASCII; name=v10-0004-Downgrade-many-pg_restore_-_stats-errors-to-warn.patchDownload
From 651e70ae705d5a4f081509e66a743422d2e86ae4 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 8 Mar 2025 00:52:41 -0500
Subject: [PATCH v10 4/4] Downgrade many pg_restore_*_stats errors to warnings.

We want to avoid errors that can potentially stop an otherwise
successful pg_upgrade or pg_restore operation. With that in mind, change
as many ERROR reports to WARNING + early termination with no data
updated.
---
 src/include/statistics/stat_utils.h        |   4 +-
 src/backend/statistics/attribute_stats.c   | 120 ++++++++++----
 src/backend/statistics/relation_stats.c    |  12 +-
 src/backend/statistics/stat_utils.c        |  65 ++++++--
 src/test/regress/expected/stats_import.out | 184 ++++++++++++++++-----
 src/test/regress/sql/stats_import.sql      |  36 ++--
 6 files changed, 309 insertions(+), 112 deletions(-)

diff --git a/src/include/statistics/stat_utils.h b/src/include/statistics/stat_utils.h
index 512eb776e0e..809c8263a41 100644
--- a/src/include/statistics/stat_utils.h
+++ b/src/include/statistics/stat_utils.h
@@ -21,7 +21,7 @@ struct StatsArgInfo
 	Oid			argtype;
 };
 
-extern void stats_check_required_arg(FunctionCallInfo fcinfo,
+extern bool stats_check_required_arg(FunctionCallInfo fcinfo,
 									 struct StatsArgInfo *arginfo,
 									 int argnum);
 extern bool stats_check_arg_array(FunctionCallInfo fcinfo,
@@ -30,7 +30,7 @@ extern bool stats_check_arg_pair(FunctionCallInfo fcinfo,
 								 struct StatsArgInfo *arginfo,
 								 int argnum1, int argnum2);
 
-extern void stats_lock_check_privileges(Oid reloid);
+extern bool stats_lock_check_privileges(Oid reloid);
 
 extern Oid	stats_lookup_relid(const char *nspname, const char *relname);
 
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index f5eb17ba42d..b7ba1622391 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -100,7 +100,7 @@ static struct StatsArgInfo cleararginfo[] =
 
 static bool attribute_statistics_update(FunctionCallInfo fcinfo);
 static Node *get_attr_expr(Relation rel, int attnum);
-static void get_attr_stat_type(Oid reloid, AttrNumber attnum,
+static bool get_attr_stat_type(Oid reloid, AttrNumber attnum,
 							   Oid *atttypid, int32 *atttypmod,
 							   char *atttyptype, Oid *atttypcoll,
 							   Oid *eq_opr, Oid *lt_opr);
@@ -129,10 +129,12 @@ static void init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
  * stored as an anyarray, and the representation of the array needs to store
  * the correct element type, which must be derived from the attribute.
  *
- * Major errors, such as the table not existing, the attribute not existing,
- * or a permissions failure are always reported at ERROR. Other errors, such
- * as a conversion failure on one statistic kind, are reported as a WARNING
- * and other statistic kinds may still be updated.
+ * This function is called during database upgrades and restorations, therefore
+ * it is imperative to avoid ERRORs that could potentially end the upgrade or
+ * restore unless. Major errors, such as the table not existing, the attribute
+ * not existing, or permissions failure are reported as WARNINGs with an end to
+ * the function, thus allowing the upgrade/restore to continue, but without the
+ * stats that can be regenereated once the database is online again.
  */
 static bool
 attribute_statistics_update(FunctionCallInfo fcinfo)
@@ -148,8 +150,8 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	HeapTuple	statup;
 
 	Oid			atttypid = InvalidOid;
-	int32		atttypmod;
-	char		atttyptype;
+	int32		atttypmod = -1;
+	char		atttyptype = TYPTYPE_PSEUDO; /* Not a great default, but there is no TYPTYPE_INVALID */
 	Oid			atttypcoll = InvalidOid;
 	Oid			eq_opr = InvalidOid;
 	Oid			lt_opr = InvalidOid;
@@ -176,38 +178,52 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 	bool		result = true;
 
-	stats_check_required_arg(fcinfo, attarginfo, ATTRELSCHEMA_ARG);
-	stats_check_required_arg(fcinfo, attarginfo, ATTRELNAME_ARG);
+	if (!stats_check_required_arg(fcinfo, attarginfo, ATTRELSCHEMA_ARG))
+		return false;
+	if (!stats_check_required_arg(fcinfo, attarginfo, ATTRELNAME_ARG))
+		return false;
 
 	nspname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELSCHEMA_ARG));
 	relname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELNAME_ARG));
 
 	reloid = stats_lookup_relid(nspname, relname);
+	if (!OidIsValid(reloid))
+		return false;
 
 	if (RecoveryInProgress())
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("recovery is in progress"),
 				 errhint("Statistics cannot be modified during recovery.")));
+		return false;
+	}
 
 	/* lock before looking up attribute */
-	stats_lock_check_privileges(reloid);
+	if (!stats_lock_check_privileges(reloid))
+		return false;
 
 	/* user can specify either attname or attnum, but not both */
 	if (!PG_ARGISNULL(ATTNAME_ARG))
 	{
 		if (!PG_ARGISNULL(ATTNUM_ARG))
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("cannot specify both attname and attnum")));
+			return false;
+		}
 		attname = TextDatumGetCString(PG_GETARG_DATUM(ATTNAME_ARG));
 		attnum = get_attnum(reloid, attname);
 		/* note that this test covers attisdropped cases too: */
 		if (attnum == InvalidAttrNumber)
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 					(errcode(ERRCODE_UNDEFINED_COLUMN),
 					 errmsg("column \"%s\" of relation \"%s\" does not exist",
 							attname, relname)));
+			return false;
+		}
 	}
 	else if (!PG_ARGISNULL(ATTNUM_ARG))
 	{
@@ -216,27 +232,33 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 		/* annoyingly, get_attname doesn't check attisdropped */
 		if (attname == NULL ||
 			!SearchSysCacheExistsAttName(reloid, attname))
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 					(errcode(ERRCODE_UNDEFINED_COLUMN),
 					 errmsg("column %d of relation \"%s\" does not exist",
 							attnum, relname)));
+			return false;
+		}
 	}
 	else
 	{
-		ereport(ERROR,
+		ereport(WARNING,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("must specify either attname or attnum")));
-		attname = NULL;			/* keep compiler quiet */
-		attnum = 0;
+		return false;
 	}
 
 	if (attnum < 0)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot modify statistics on system column \"%s\"",
 						attname)));
+		return false;
+	}
 
-	stats_check_required_arg(fcinfo, attarginfo, INHERITED_ARG);
+	if (!stats_check_required_arg(fcinfo, attarginfo, INHERITED_ARG))
+		return false;
 	inherited = PG_GETARG_BOOL(INHERITED_ARG);
 
 	/*
@@ -285,10 +307,11 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	}
 
 	/* derive information from attribute */
-	get_attr_stat_type(reloid, attnum,
-					   &atttypid, &atttypmod,
-					   &atttyptype, &atttypcoll,
-					   &eq_opr, &lt_opr);
+	if (!get_attr_stat_type(reloid, attnum,
+							&atttypid, &atttypmod,
+							&atttyptype, &atttypcoll,
+							&eq_opr, &lt_opr))
+		result = false;
 
 	/* if needed, derive element type */
 	if (do_mcelem || do_dechist)
@@ -568,7 +591,7 @@ get_attr_expr(Relation rel, int attnum)
 /*
  * Derive type information from the attribute.
  */
-static void
+static bool
 get_attr_stat_type(Oid reloid, AttrNumber attnum,
 				   Oid *atttypid, int32 *atttypmod,
 				   char *atttyptype, Oid *atttypcoll,
@@ -585,18 +608,26 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum,
 
 	/* Attribute not found */
 	if (!HeapTupleIsValid(atup))
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 errmsg("attribute %d of relation \"%s\" does not exist",
 						attnum, RelationGetRelationName(rel))));
+		relation_close(rel, NoLock);
+		return false;
+	}
 
 	attr = (Form_pg_attribute) GETSTRUCT(atup);
 
 	if (attr->attisdropped)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 errmsg("attribute %d of relation \"%s\" does not exist",
 						attnum, RelationGetRelationName(rel))));
+		relation_close(rel, NoLock);
+		return false;
+	}
 
 	expr = get_attr_expr(rel, attr->attnum);
 
@@ -645,6 +676,7 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum,
 		*atttypcoll = DEFAULT_COLLATION_OID;
 
 	relation_close(rel, NoLock);
+	return true;
 }
 
 /*
@@ -770,6 +802,10 @@ set_stats_slot(Datum *values, bool *nulls, bool *replaces,
 	if (slotidx >= STATISTIC_NUM_SLOTS && first_empty >= 0)
 		slotidx = first_empty;
 
+	/*
+	 * Currently there is no datatype that can have more than STATISTIC_NUM_SLOTS
+	 * statistic kinds, so this can safely remain an ERROR for now.
+	 */
 	if (slotidx >= STATISTIC_NUM_SLOTS)
 		ereport(ERROR,
 				(errmsg("maximum number of statistics slots exceeded: %d",
@@ -915,38 +951,54 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 	AttrNumber	attnum;
 	bool		inherited;
 
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELSCHEMA_ARG);
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELNAME_ARG);
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTNAME_ARG);
-	stats_check_required_arg(fcinfo, cleararginfo, C_INHERITED_ARG);
+	if (!stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELSCHEMA_ARG))
+		PG_RETURN_VOID();
+	if (!stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELNAME_ARG))
+		PG_RETURN_VOID();
+	if (!stats_check_required_arg(fcinfo, cleararginfo, C_ATTNAME_ARG))
+		PG_RETURN_VOID();
+	if (!stats_check_required_arg(fcinfo, cleararginfo, C_INHERITED_ARG))
+		PG_RETURN_VOID();
 
 	nspname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELSCHEMA_ARG));
 	relname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELNAME_ARG));
 
 	reloid = stats_lookup_relid(nspname, relname);
+	if (!OidIsValid(reloid))
+		PG_RETURN_VOID();
 
 	if (RecoveryInProgress())
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("recovery is in progress"),
 				 errhint("Statistics cannot be modified during recovery.")));
+		PG_RETURN_VOID();
+	}
 
-	stats_lock_check_privileges(reloid);
+	if (!stats_lock_check_privileges(reloid))
+		PG_RETURN_VOID();
 
 	attname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTNAME_ARG));
 	attnum = get_attnum(reloid, attname);
 
 	if (attnum < 0)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot clear statistics on system column \"%s\"",
 						attname)));
+		PG_RETURN_VOID();
+	}
 
 	if (attnum == InvalidAttrNumber)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 errmsg("column \"%s\" of relation \"%s\" does not exist",
 						attname, get_rel_name(reloid))));
+		PG_RETURN_VOID();
+	}
 
 	inherited = PG_GETARG_BOOL(C_INHERITED_ARG);
 
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index cd3a75b621a..7c47af15c9f 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -83,13 +83,18 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 	bool		nulls[4] = {0};
 	int			nreplaces = 0;
 
-	stats_check_required_arg(fcinfo, relarginfo, RELSCHEMA_ARG);
-	stats_check_required_arg(fcinfo, relarginfo, RELNAME_ARG);
+	if (!stats_check_required_arg(fcinfo, relarginfo, RELSCHEMA_ARG))
+		return false;
+
+	if (!stats_check_required_arg(fcinfo, relarginfo, RELNAME_ARG))
+		return false;
 
 	nspname = TextDatumGetCString(PG_GETARG_DATUM(RELSCHEMA_ARG));
 	relname = TextDatumGetCString(PG_GETARG_DATUM(RELNAME_ARG));
 
 	reloid = stats_lookup_relid(nspname, relname);
+	if (!OidIsValid(reloid))
+		return false;
 
 	if (RecoveryInProgress())
 		ereport(ERROR,
@@ -97,7 +102,8 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 				 errmsg("recovery is in progress"),
 				 errhint("Statistics cannot be modified during recovery.")));
 
-	stats_lock_check_privileges(reloid);
+	if (!stats_lock_check_privileges(reloid))
+		return false;
 
 	if (!PG_ARGISNULL(RELPAGES_ARG))
 	{
diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c
index a9a3224efe6..d587e875457 100644
--- a/src/backend/statistics/stat_utils.c
+++ b/src/backend/statistics/stat_utils.c
@@ -33,16 +33,20 @@
 /*
  * Ensure that a given argument is not null.
  */
-void
+bool
 stats_check_required_arg(FunctionCallInfo fcinfo,
 						 struct StatsArgInfo *arginfo,
 						 int argnum)
 {
 	if (PG_ARGISNULL(argnum))
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("\"%s\" cannot be NULL",
 						arginfo[argnum].argname)));
+		return false;
+	}
+	return true;
 }
 
 /*
@@ -127,13 +131,14 @@ stats_check_arg_pair(FunctionCallInfo fcinfo,
  *   - the role owns the current database and the relation is not shared
  *   - the role has the MAINTAIN privilege on the relation
  */
-void
+bool
 stats_lock_check_privileges(Oid reloid)
 {
 	Relation	table;
 	Oid			table_oid = reloid;
 	Oid			index_oid = InvalidOid;
 	LOCKMODE	index_lockmode = NoLock;
+	bool		ok = true;
 
 	/*
 	 * For indexes, we follow the locking behavior in do_analyze_rel() and
@@ -173,14 +178,15 @@ stats_lock_check_privileges(Oid reloid)
 		case RELKIND_PARTITIONED_TABLE:
 			break;
 		default:
-			ereport(ERROR,
+			ereport(WARNING,
 					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 					 errmsg("cannot modify statistics for relation \"%s\"",
 							RelationGetRelationName(table)),
 					 errdetail_relkind_not_supported(table->rd_rel->relkind)));
+		ok = false;
 	}
 
-	if (OidIsValid(index_oid))
+	if (ok && (OidIsValid(index_oid)))
 	{
 		Relation	index;
 
@@ -193,25 +199,33 @@ stats_lock_check_privileges(Oid reloid)
 		relation_close(index, NoLock);
 	}
 
-	if (table->rd_rel->relisshared)
-		ereport(ERROR,
+	if (ok && (table->rd_rel->relisshared))
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot modify statistics for shared relation")));
+		ok = false;
+	}
 
-	if (!object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId()))
+	if (ok && (!object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId())))
 	{
 		AclResult	aclresult = pg_class_aclcheck(RelationGetRelid(table),
 												  GetUserId(),
 												  ACL_MAINTAIN);
 
 		if (aclresult != ACLCHECK_OK)
-			aclcheck_error(aclresult,
-						   get_relkind_objtype(table->rd_rel->relkind),
-						   NameStr(table->rd_rel->relname));
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+						errmsg("permission denied for relation %s",
+							   NameStr(table->rd_rel->relname))));
+			ok = false;
+		}
 	}
 
 	/* retain lock on table */
 	relation_close(table, NoLock);
+	return ok;
 }
 
 /*
@@ -223,10 +237,20 @@ stats_lookup_relid(const char *nspname, const char *relname)
 	Oid			nspoid;
 	Oid			reloid;
 
-	nspoid = LookupExplicitNamespace(nspname, false);
+	nspoid = LookupExplicitNamespace(nspname, true);
+	if (!OidIsValid(nspoid))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_UNDEFINED_TABLE),
+				 errmsg("relation \"%s.%s\" does not exist",
+						nspname, relname)));
+
+		return InvalidOid;
+	}
+
 	reloid = get_relname_relid(relname, nspoid);
 	if (!OidIsValid(reloid))
-		ereport(ERROR,
+		ereport(WARNING,
 				(errcode(ERRCODE_UNDEFINED_TABLE),
 				 errmsg("relation \"%s.%s\" does not exist",
 						nspname, relname)));
@@ -303,9 +327,12 @@ stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
 								  &args, &types, &argnulls);
 
 	if (nargs % 2 != 0)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				errmsg("variadic arguments must be name/value pairs"),
 				errhint("Provide an even number of variadic arguments that can be divided into pairs."));
+		return false;
+	}
 
 	/*
 	 * For each argument name/value pair, find corresponding positional
@@ -318,14 +345,20 @@ stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
 		char	   *argname;
 
 		if (argnulls[i])
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 					(errmsg("name at variadic position %d is NULL", i + 1)));
+			return false;
+		}
 
 		if (types[i] != TEXTOID)
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 					(errmsg("name at variadic position %d has type \"%s\", expected type \"%s\"",
 							i + 1, format_type_be(types[i]),
 							format_type_be(TEXTOID))));
+			return false;
+		}
 
 		if (argnulls[i + 1])
 			continue;
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index 48d6392b4ad..161cf67b711 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -46,49 +46,85 @@ SELECT pg_clear_relation_stats('stats_import', 'test');
 --
 -- relstats tests
 --
--- error: schemaname missing
+-- warning: schemaname missing, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'relname', 'test',
         'relpages', 17::integer);
-ERROR:  "schemaname" cannot be NULL
--- error: relname missing
+WARNING:  "schemaname" cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+-- warning: relname missing, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relpages', 17::integer);
-ERROR:  "relname" cannot be NULL
---- error: schemaname is wrong type
+WARNING:  "relname" cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+--- warning: schemaname is wrong type, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 3.6::float,
         'relname', 'test',
         'relpages', 17::integer);
 WARNING:  argument "schemaname" has type "double precision", expected type "text"
-ERROR:  "schemaname" cannot be NULL
---- error: relname is wrong type
+WARNING:  "schemaname" cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+--- warning: relname is wrong type, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 0::oid,
         'relpages', 17::integer);
 WARNING:  argument "relname" has type "oid", expected type "text"
-ERROR:  "relname" cannot be NULL
--- error: relation not found
+WARNING:  "relname" cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+-- warning: relation not found, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'nope',
         'relpages', 17::integer);
-ERROR:  relation "stats_import.nope" does not exist
--- error: odd number of variadic arguments cannot be pairs
+WARNING:  relation "stats_import.nope" does not exist
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+-- warning: odd number of variadic arguments cannot be pairs, nothing updated
 SELECT pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'test',
         'relallvisible');
-ERROR:  variadic arguments must be name/value pairs
+WARNING:  variadic arguments must be name/value pairs
 HINT:  Provide an even number of variadic arguments that can be divided into pairs.
--- error: argument name is NULL
+WARNING:  "schemaname" cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+-- warning: argument name is NULL, nothing updated
 SELECT pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'test',
         NULL, '17'::integer);
-ERROR:  name at variadic position 5 is NULL
+WARNING:  name at variadic position 5 is NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
 -- starting stats
 SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
@@ -340,65 +376,110 @@ CREATE SEQUENCE stats_import.testseq;
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'testseq');
-ERROR:  cannot modify statistics for relation "testseq"
+WARNING:  cannot modify statistics for relation "testseq"
 DETAIL:  This operation is not supported for sequences.
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
 SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testseq');
-ERROR:  cannot modify statistics for relation "testseq"
+WARNING:  cannot modify statistics for relation "testseq"
 DETAIL:  This operation is not supported for sequences.
+ pg_clear_relation_stats 
+-------------------------
+ 
+(1 row)
+
 CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
 SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testview');
-ERROR:  cannot modify statistics for relation "testview"
+WARNING:  cannot modify statistics for relation "testview"
 DETAIL:  This operation is not supported for views.
+ pg_clear_relation_stats 
+-------------------------
+ 
+(1 row)
+
 --
 -- attribute stats
 --
--- error: schemaname missing
+-- warning: schemaname missing, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relname', 'test',
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  "schemaname" cannot be NULL
--- error: schema does not exist
+WARNING:  "schemaname" cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: schema does not exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'nope',
     'relname', 'test',
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  schema "nope" does not exist
--- error: relname missing
+WARNING:  relation "nope.test" does not exist
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: relname missing, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  "relname" cannot be NULL
--- error: relname does not exist
+WARNING:  "relname" cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: relname does not exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'nope',
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  relation "stats_import.nope" does not exist
--- error: relname null
+WARNING:  relation "stats_import.nope" does not exist
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: relname null, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', NULL,
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  "relname" cannot be NULL
--- error: NULL attname
+WARNING:  "relname" cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: NULL attname, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
     'attname', NULL,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  must specify either attname or attnum
--- error: attname doesn't exist
+WARNING:  must specify either attname or attnum
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: attname doesn't exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -407,8 +488,13 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'null_frac', 0.1::real,
     'avg_width', 2::integer,
     'n_distinct', 0.3::real);
-ERROR:  column "nope" of relation "test" does not exist
--- error: both attname and attnum
+WARNING:  column "nope" of relation "test" does not exist
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: both attname and attnum, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -416,30 +502,50 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'attnum', 1::smallint,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  cannot specify both attname and attnum
--- error: neither attname nor attnum
+WARNING:  cannot specify both attname and attnum
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: neither attname nor attnum, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  must specify either attname or attnum
--- error: attribute is system column
+WARNING:  must specify either attname or attnum
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: attribute is system column, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
     'attname', 'xmin',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  cannot modify statistics on system column "xmin"
--- error: inherited null
+WARNING:  cannot modify statistics on system column "xmin"
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: inherited null, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
     'attname', 'id',
     'inherited', NULL::boolean,
     'null_frac', 0.1::real);
-ERROR:  "inherited" cannot be NULL
+WARNING:  "inherited" cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
 -- ok: just the fixed values, with version, no stakinds
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index d140733a750..be8045ceea5 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -39,41 +39,41 @@ SELECT pg_clear_relation_stats('stats_import', 'test');
 -- relstats tests
 --
 
--- error: schemaname missing
+-- warning: schemaname missing, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'relname', 'test',
         'relpages', 17::integer);
 
--- error: relname missing
+-- warning: relname missing, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relpages', 17::integer);
 
---- error: schemaname is wrong type
+--- warning: schemaname is wrong type, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 3.6::float,
         'relname', 'test',
         'relpages', 17::integer);
 
---- error: relname is wrong type
+--- warning: relname is wrong type, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 0::oid,
         'relpages', 17::integer);
 
--- error: relation not found
+-- warning: relation not found, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'nope',
         'relpages', 17::integer);
 
--- error: odd number of variadic arguments cannot be pairs
+-- warning: odd number of variadic arguments cannot be pairs, nothing updated
 SELECT pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'test',
         'relallvisible');
 
--- error: argument name is NULL
+-- warning: argument name is NULL, nothing updated
 SELECT pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'test',
@@ -246,14 +246,14 @@ SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname
 -- attribute stats
 --
 
--- error: schemaname missing
+-- warning: schemaname missing, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relname', 'test',
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: schema does not exist
+-- warning: schema does not exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'nope',
     'relname', 'test',
@@ -261,14 +261,14 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: relname missing
+-- warning: relname missing, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: relname does not exist
+-- warning: relname does not exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'nope',
@@ -276,7 +276,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: relname null
+-- warning: relname null, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', NULL,
@@ -284,7 +284,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: NULL attname
+-- warning: NULL attname, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -292,7 +292,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: attname doesn't exist
+-- warning: attname doesn't exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -302,7 +302,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'avg_width', 2::integer,
     'n_distinct', 0.3::real);
 
--- error: both attname and attnum
+-- warning: both attname and attnum, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -311,14 +311,14 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: neither attname nor attnum
+-- warning: neither attname nor attnum, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: attribute is system column
+-- warning: attribute is system column, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -326,7 +326,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: inherited null
+-- warning: inherited null, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
-- 
2.49.0

v10-0001-Introduce-CreateStmtPtr.patchtext/x-patch; charset=US-ASCII; name=v10-0001-Introduce-CreateStmtPtr.patchDownload
From 8611beb5a7906d0f7e93fb68aa41dd58bc7ab80f Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Fri, 14 Mar 2025 01:06:19 -0400
Subject: [PATCH v10 1/4] Introduce CreateStmtPtr.

CreateStmtPtr is a function pointer that can replace the createStmt/defn
parameter. This is useful in situations where the amount of text
generated for a definition is so large that it is undesirable to hold
many such objects in memory at the same time.

Using functions of this type, the text created is then immediately
written out to the appropriate file for the given dump format.
---
 src/bin/pg_dump/pg_backup.h          |   2 +
 src/bin/pg_dump/pg_backup_archiver.c |  22 ++-
 src/bin/pg_dump/pg_backup_archiver.h |   7 +
 src/bin/pg_dump/pg_dump.c            | 230 +++++++++++++++------------
 4 files changed, 156 insertions(+), 105 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 658986de6f8..fdcccd64a70 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -289,6 +289,8 @@ typedef int (*DataDumperPtr) (Archive *AH, const void *userArg);
 
 typedef void (*SetupWorkerPtrType) (Archive *AH);
 
+typedef char *(*CreateStmtPtr) (Archive *AH, const void *userArg);
+
 /*
  * Main archiver interface.
  */
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 82d51c89ac6..e512201ed58 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -1264,6 +1264,9 @@ ArchiveEntry(Archive *AHX, CatalogId catalogId, DumpId dumpId,
 	newToc->dataDumper = opts->dumpFn;
 	newToc->dataDumperArg = opts->dumpArg;
 	newToc->hadDumper = opts->dumpFn ? true : false;
+	newToc->createDumper = opts->createFn;
+	newToc->createDumperArg = opts->createArg;
+	newToc->hadCreateDumper = opts->createFn ? true : false;
 
 	newToc->formatData = NULL;
 	newToc->dataLength = 0;
@@ -2620,7 +2623,17 @@ WriteToc(ArchiveHandle *AH)
 		WriteStr(AH, te->tag);
 		WriteStr(AH, te->desc);
 		WriteInt(AH, te->section);
-		WriteStr(AH, te->defn);
+
+		if (te->hadCreateDumper)
+		{
+			char	   *defn = te->createDumper((Archive *) AH, te->createDumperArg);
+
+			WriteStr(AH, defn);
+			pg_free(defn);
+		}
+		else
+			WriteStr(AH, te->defn);
+
 		WriteStr(AH, te->dropStmt);
 		WriteStr(AH, te->copyStmt);
 		WriteStr(AH, te->namespace);
@@ -3856,6 +3869,13 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 	{
 		IssueACLPerBlob(AH, te);
 	}
+	else if (te->hadCreateDumper)
+	{
+		char	   *ptr = te->createDumper((Archive *) AH, te->createDumperArg);
+
+		ahwrite(ptr, 1, strlen(ptr), AH);
+		pg_free(ptr);
+	}
 	else if (te->defn && strlen(te->defn) > 0)
 	{
 		ahprintf(AH, "%s\n\n", te->defn);
diff --git a/src/bin/pg_dump/pg_backup_archiver.h b/src/bin/pg_dump/pg_backup_archiver.h
index a2064f471ed..e68db633995 100644
--- a/src/bin/pg_dump/pg_backup_archiver.h
+++ b/src/bin/pg_dump/pg_backup_archiver.h
@@ -368,6 +368,11 @@ struct _tocEntry
 	const void *dataDumperArg;	/* Arg for above routine */
 	void	   *formatData;		/* TOC Entry data specific to file format */
 
+	CreateStmtPtr createDumper; /* Routine for create statement creation */
+	const void *createDumperArg;	/* arg for the above routine */
+	bool		hadCreateDumper;	/* Archiver was passed a create statement
+									 * routine */
+
 	/* working state while dumping/restoring */
 	pgoff_t		dataLength;		/* item's data size; 0 if none or unknown */
 	int			reqs;			/* do we need schema and/or data of object
@@ -407,6 +412,8 @@ typedef struct _archiveOpts
 	int			nDeps;
 	DataDumperPtr dumpFn;
 	const void *dumpArg;
+	CreateStmtPtr createFn;
+	const void *createArg;
 } ArchiveOpts;
 #define ARCHIVE_OPTS(...) &(ArchiveOpts){__VA_ARGS__}
 /* Called to add a TOC entry */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index e41e645f649..224dc8c9330 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -10520,51 +10520,44 @@ statisticsDumpSection(const RelStatsInfo *rsinfo)
 }
 
 /*
- * dumpRelationStats --
+ * printDumpRelationStats --
  *
- * Dump command to import stats into the relation on the new database.
+ * Generate the SQL statements needed to restore a relation's statistics.
  */
-static void
-dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+static char *
+printRelationStats(Archive *fout, const void *userArg)
 {
+	const RelStatsInfo *rsinfo = (RelStatsInfo *) userArg;
 	const DumpableObject *dobj = &rsinfo->dobj;
+
+	PQExpBufferData query;
+	PQExpBufferData out;
+
 	PGresult   *res;
-	PQExpBuffer query;
-	PQExpBuffer out;
-	DumpId	   *deps = NULL;
-	int			ndeps = 0;
-	int			i_attname;
-	int			i_inherited;
-	int			i_null_frac;
-	int			i_avg_width;
-	int			i_n_distinct;
-	int			i_most_common_vals;
-	int			i_most_common_freqs;
-	int			i_histogram_bounds;
-	int			i_correlation;
-	int			i_most_common_elems;
-	int			i_most_common_elem_freqs;
-	int			i_elem_count_histogram;
-	int			i_range_length_histogram;
-	int			i_range_empty_frac;
-	int			i_range_bounds_histogram;
 
-	/* nothing to do if we are not dumping statistics */
-	if (!fout->dopt->dumpStatistics)
-		return;
+	static bool first_query = true;
+	static int	i_attname;
+	static int	i_inherited;
+	static int	i_null_frac;
+	static int	i_avg_width;
+	static int	i_n_distinct;
+	static int	i_most_common_vals;
+	static int	i_most_common_freqs;
+	static int	i_histogram_bounds;
+	static int	i_correlation;
+	static int	i_most_common_elems;
+	static int	i_most_common_elem_freqs;
+	static int	i_elem_count_histogram;
+	static int	i_range_length_histogram;
+	static int	i_range_empty_frac;
+	static int	i_range_bounds_histogram;
 
-	/* dependent on the relation definition, if doing schema */
-	if (fout->dopt->dumpSchema)
+	initPQExpBuffer(&query);
+
+	if (first_query)
 	{
-		deps = dobj->dependencies;
-		ndeps = dobj->nDeps;
-	}
-
-	query = createPQExpBuffer();
-	if (!fout->is_prepared[PREPQUERY_GETATTRIBUTESTATS])
-	{
-		appendPQExpBufferStr(query,
-							 "PREPARE getAttributeStats(pg_catalog.name, pg_catalog.name) AS\n"
+		appendPQExpBufferStr(&query,
+							 "PREPARE getAttributeStats(pg_catalog.text, pg_catalog.text) AS\n"
 							 "SELECT s.attname, s.inherited, "
 							 "s.null_frac, s.avg_width, s.n_distinct, "
 							 "s.most_common_vals, s.most_common_freqs, "
@@ -10573,82 +10566,85 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 							 "s.elem_count_histogram, ");
 
 		if (fout->remoteVersion >= 170000)
-			appendPQExpBufferStr(query,
+			appendPQExpBufferStr(&query,
 								 "s.range_length_histogram, "
 								 "s.range_empty_frac, "
 								 "s.range_bounds_histogram ");
 		else
-			appendPQExpBufferStr(query,
+			appendPQExpBufferStr(&query,
 								 "NULL AS range_length_histogram,"
 								 "NULL AS range_empty_frac,"
 								 "NULL AS range_bounds_histogram ");
 
-		appendPQExpBufferStr(query,
+		appendPQExpBufferStr(&query,
 							 "FROM pg_catalog.pg_stats s "
 							 "WHERE s.schemaname = $1 "
 							 "AND s.tablename = $2 "
 							 "ORDER BY s.attname, s.inherited");
 
-		ExecuteSqlStatement(fout, query->data);
+		ExecuteSqlStatement(fout, query.data);
 
-		fout->is_prepared[PREPQUERY_GETATTRIBUTESTATS] = true;
-		resetPQExpBuffer(query);
+		resetPQExpBuffer(&query);
 	}
 
-	out = createPQExpBuffer();
+	initPQExpBuffer(&out);
 
 	/* restore relation stats */
-	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
-	appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
+	appendPQExpBufferStr(&out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
+	appendPQExpBuffer(&out, "\t'version', '%u'::integer,\n",
 					  fout->remoteVersion);
-	appendPQExpBufferStr(out, "\t'schemaname', ");
-	appendStringLiteralAH(out, rsinfo->dobj.namespace->dobj.name, fout);
-	appendPQExpBufferStr(out, ",\n");
-	appendPQExpBufferStr(out, "\t'relname', ");
-	appendStringLiteralAH(out, rsinfo->dobj.name, fout);
-	appendPQExpBufferStr(out, ",\n");
-	appendPQExpBuffer(out, "\t'relpages', '%d'::integer,\n", rsinfo->relpages);
-	appendPQExpBuffer(out, "\t'reltuples', '%s'::real,\n", rsinfo->reltuples);
-	appendPQExpBuffer(out, "\t'relallvisible', '%d'::integer\n);\n",
+	appendPQExpBufferStr(&out, "\t'schemaname', ");
+	appendStringLiteralAH(&out, rsinfo->dobj.namespace->dobj.name, fout);
+	appendPQExpBufferStr(&out, ",\n");
+	appendPQExpBufferStr(&out, "\t'relname', ");
+	appendStringLiteralAH(&out, rsinfo->dobj.name, fout);
+	appendPQExpBufferStr(&out, ",\n");
+	appendPQExpBuffer(&out, "\t'relpages', '%d'::integer,\n", rsinfo->relpages);
+	appendPQExpBuffer(&out, "\t'reltuples', '%s'::real,\n", rsinfo->reltuples);
+	appendPQExpBuffer(&out, "\t'relallvisible', '%d'::integer\n);\n",
 					  rsinfo->relallvisible);
 
 	/* fetch attribute stats */
-	appendPQExpBufferStr(query, "EXECUTE getAttributeStats(");
-	appendStringLiteralAH(query, dobj->namespace->dobj.name, fout);
-	appendPQExpBufferStr(query, ", ");
-	appendStringLiteralAH(query, dobj->name, fout);
-	appendPQExpBufferStr(query, ");");
+	appendPQExpBufferStr(&query, "EXECUTE getAttributeStats(");
+	appendStringLiteralAH(&query, dobj->namespace->dobj.name, fout);
+	appendPQExpBufferStr(&query, ", ");
+	appendStringLiteralAH(&query, dobj->name, fout);
+	appendPQExpBufferStr(&query, ")");
 
-	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	res = ExecuteSqlQuery(fout, query.data, PGRES_TUPLES_OK);
 
-	i_attname = PQfnumber(res, "attname");
-	i_inherited = PQfnumber(res, "inherited");
-	i_null_frac = PQfnumber(res, "null_frac");
-	i_avg_width = PQfnumber(res, "avg_width");
-	i_n_distinct = PQfnumber(res, "n_distinct");
-	i_most_common_vals = PQfnumber(res, "most_common_vals");
-	i_most_common_freqs = PQfnumber(res, "most_common_freqs");
-	i_histogram_bounds = PQfnumber(res, "histogram_bounds");
-	i_correlation = PQfnumber(res, "correlation");
-	i_most_common_elems = PQfnumber(res, "most_common_elems");
-	i_most_common_elem_freqs = PQfnumber(res, "most_common_elem_freqs");
-	i_elem_count_histogram = PQfnumber(res, "elem_count_histogram");
-	i_range_length_histogram = PQfnumber(res, "range_length_histogram");
-	i_range_empty_frac = PQfnumber(res, "range_empty_frac");
-	i_range_bounds_histogram = PQfnumber(res, "range_bounds_histogram");
+	if (first_query)
+	{
+		i_attname = PQfnumber(res, "attname");
+		i_inherited = PQfnumber(res, "inherited");
+		i_null_frac = PQfnumber(res, "null_frac");
+		i_avg_width = PQfnumber(res, "avg_width");
+		i_n_distinct = PQfnumber(res, "n_distinct");
+		i_most_common_vals = PQfnumber(res, "most_common_vals");
+		i_most_common_freqs = PQfnumber(res, "most_common_freqs");
+		i_histogram_bounds = PQfnumber(res, "histogram_bounds");
+		i_correlation = PQfnumber(res, "correlation");
+		i_most_common_elems = PQfnumber(res, "most_common_elems");
+		i_most_common_elem_freqs = PQfnumber(res, "most_common_elem_freqs");
+		i_elem_count_histogram = PQfnumber(res, "elem_count_histogram");
+		i_range_length_histogram = PQfnumber(res, "range_length_histogram");
+		i_range_empty_frac = PQfnumber(res, "range_empty_frac");
+		i_range_bounds_histogram = PQfnumber(res, "range_bounds_histogram");
+		first_query = false;
+	}
 
 	/* restore attribute stats */
 	for (int rownum = 0; rownum < PQntuples(res); rownum++)
 	{
 		const char *attname;
 
-		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
-		appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
+		appendPQExpBufferStr(&out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+		appendPQExpBuffer(&out, "\t'version', '%u'::integer,\n",
 						  fout->remoteVersion);
-		appendPQExpBufferStr(out, "\t'schemaname', ");
-		appendStringLiteralAH(out, rsinfo->dobj.namespace->dobj.name, fout);
-		appendPQExpBufferStr(out, ",\n\t'relname', ");
-		appendStringLiteralAH(out, rsinfo->dobj.name, fout);
+		appendPQExpBufferStr(&out, "\t'schemaname', ");
+		appendStringLiteralAH(&out, rsinfo->dobj.namespace->dobj.name, fout);
+		appendPQExpBufferStr(&out, ",\n\t'relname', ");
+		appendStringLiteralAH(&out, rsinfo->dobj.name, fout);
 
 		if (PQgetisnull(res, rownum, i_attname))
 			pg_fatal("attname cannot be NULL");
@@ -10661,8 +10657,8 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 		 */
 		if (rsinfo->nindAttNames == 0)
 		{
-			appendPQExpBuffer(out, ",\n\t'attname', ");
-			appendStringLiteralAH(out, attname, fout);
+			appendPQExpBuffer(&out, ",\n\t'attname', ");
+			appendStringLiteralAH(&out, attname, fout);
 		}
 		else
 		{
@@ -10672,7 +10668,7 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 			{
 				if (strcmp(attname, rsinfo->indAttNames[i]) == 0)
 				{
-					appendPQExpBuffer(out, ",\n\t'attnum', '%d'::smallint",
+					appendPQExpBuffer(&out, ",\n\t'attnum', '%d'::smallint",
 									  i + 1);
 					found = true;
 					break;
@@ -10684,67 +10680,93 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 		}
 
 		if (!PQgetisnull(res, rownum, i_inherited))
-			appendNamedArgument(out, fout, "inherited", "boolean",
+			appendNamedArgument(&out, fout, "inherited", "boolean",
 								PQgetvalue(res, rownum, i_inherited));
 		if (!PQgetisnull(res, rownum, i_null_frac))
-			appendNamedArgument(out, fout, "null_frac", "real",
+			appendNamedArgument(&out, fout, "null_frac", "real",
 								PQgetvalue(res, rownum, i_null_frac));
 		if (!PQgetisnull(res, rownum, i_avg_width))
-			appendNamedArgument(out, fout, "avg_width", "integer",
+			appendNamedArgument(&out, fout, "avg_width", "integer",
 								PQgetvalue(res, rownum, i_avg_width));
 		if (!PQgetisnull(res, rownum, i_n_distinct))
-			appendNamedArgument(out, fout, "n_distinct", "real",
+			appendNamedArgument(&out, fout, "n_distinct", "real",
 								PQgetvalue(res, rownum, i_n_distinct));
 		if (!PQgetisnull(res, rownum, i_most_common_vals))
-			appendNamedArgument(out, fout, "most_common_vals", "text",
+			appendNamedArgument(&out, fout, "most_common_vals", "text",
 								PQgetvalue(res, rownum, i_most_common_vals));
 		if (!PQgetisnull(res, rownum, i_most_common_freqs))
-			appendNamedArgument(out, fout, "most_common_freqs", "real[]",
+			appendNamedArgument(&out, fout, "most_common_freqs", "real[]",
 								PQgetvalue(res, rownum, i_most_common_freqs));
 		if (!PQgetisnull(res, rownum, i_histogram_bounds))
-			appendNamedArgument(out, fout, "histogram_bounds", "text",
+			appendNamedArgument(&out, fout, "histogram_bounds", "text",
 								PQgetvalue(res, rownum, i_histogram_bounds));
 		if (!PQgetisnull(res, rownum, i_correlation))
-			appendNamedArgument(out, fout, "correlation", "real",
+			appendNamedArgument(&out, fout, "correlation", "real",
 								PQgetvalue(res, rownum, i_correlation));
 		if (!PQgetisnull(res, rownum, i_most_common_elems))
-			appendNamedArgument(out, fout, "most_common_elems", "text",
+			appendNamedArgument(&out, fout, "most_common_elems", "text",
 								PQgetvalue(res, rownum, i_most_common_elems));
 		if (!PQgetisnull(res, rownum, i_most_common_elem_freqs))
-			appendNamedArgument(out, fout, "most_common_elem_freqs", "real[]",
+			appendNamedArgument(&out, fout, "most_common_elem_freqs", "real[]",
 								PQgetvalue(res, rownum, i_most_common_elem_freqs));
 		if (!PQgetisnull(res, rownum, i_elem_count_histogram))
-			appendNamedArgument(out, fout, "elem_count_histogram", "real[]",
+			appendNamedArgument(&out, fout, "elem_count_histogram", "real[]",
 								PQgetvalue(res, rownum, i_elem_count_histogram));
 		if (fout->remoteVersion >= 170000)
 		{
 			if (!PQgetisnull(res, rownum, i_range_length_histogram))
-				appendNamedArgument(out, fout, "range_length_histogram", "text",
+				appendNamedArgument(&out, fout, "range_length_histogram", "text",
 									PQgetvalue(res, rownum, i_range_length_histogram));
 			if (!PQgetisnull(res, rownum, i_range_empty_frac))
-				appendNamedArgument(out, fout, "range_empty_frac", "real",
+				appendNamedArgument(&out, fout, "range_empty_frac", "real",
 									PQgetvalue(res, rownum, i_range_empty_frac));
 			if (!PQgetisnull(res, rownum, i_range_bounds_histogram))
-				appendNamedArgument(out, fout, "range_bounds_histogram", "text",
+				appendNamedArgument(&out, fout, "range_bounds_histogram", "text",
 									PQgetvalue(res, rownum, i_range_bounds_histogram));
 		}
-		appendPQExpBufferStr(out, "\n);\n");
+		appendPQExpBufferStr(&out, "\n);\n");
 	}
 
 	PQclear(res);
 
+	termPQExpBuffer(&query);
+	return out.data;
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	const DumpableObject *dobj = &rsinfo->dobj;
+
+	DumpId	   *deps = NULL;
+	int			ndeps = 0;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
+	/* dependent on the relation definition, if doing schema */
+	if (fout->dopt->dumpSchema)
+	{
+		deps = dobj->dependencies;
+		ndeps = dobj->nDeps;
+	}
+
 	ArchiveEntry(fout, nilCatalogId, createDumpId(),
 				 ARCHIVE_OPTS(.tag = dobj->name,
 							  .namespace = dobj->namespace->dobj.name,
 							  .description = "STATISTICS DATA",
 							  .section = rsinfo->postponed_def ?
 							  SECTION_POST_DATA : statisticsDumpSection(rsinfo),
-							  .createStmt = out->data,
+							  .createFn = printRelationStats,
+							  .createArg = rsinfo,
 							  .deps = deps,
 							  .nDeps = ndeps));
-
-	destroyPQExpBuffer(out);
-	destroyPQExpBuffer(query);
 }
 
 /*

base-commit: bde2fb797aaebcbe06bf60f330ba5a068f17dda7
-- 
2.49.0

v10-0003-Add-relallfrozen-to-pg_dump-statistics.patchtext/x-patch; charset=US-ASCII; name=v10-0003-Add-relallfrozen-to-pg_dump-statistics.patchDownload
From 87da7d4c517c3b2e63666892e64b9de2a8dbbe44 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 15 Mar 2025 17:34:30 -0400
Subject: [PATCH v10 3/4] Add relallfrozen to pg_dump statistics.

The column relallfrozen was recently added to pg_class and it also
represent statistics, so we should add it to the dump/restore/upgrade
operations.

Dumps of databases prior to v18 will not attempt to restore any value to
relallfrozen, allowing pg_restore_relation_stats() to set the default it
deems appropriate.
---
 src/bin/pg_dump/pg_dump.c        | 52 ++++++++++++++++++++++----------
 src/bin/pg_dump/pg_dump.h        |  1 +
 src/bin/pg_dump/t/002_pg_dump.pl |  3 +-
 3 files changed, 39 insertions(+), 17 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index e3f2dac33ec..6c366fd55d3 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -6897,7 +6897,8 @@ getFuncs(Archive *fout)
  */
 static RelStatsInfo *
 getRelationStatistics(Archive *fout, DumpableObject *rel, int32 relpages,
-					  char *reltuples, int32 relallvisible, char relkind,
+					  char *reltuples, int32 relallvisible,
+					  int32 relallfrozen, char relkind,
 					  char **indAttNames, int nindAttNames)
 {
 	if (!fout->dopt->dumpStatistics)
@@ -6926,6 +6927,7 @@ getRelationStatistics(Archive *fout, DumpableObject *rel, int32 relpages,
 		info->relpages = relpages;
 		info->reltuples = pstrdup(reltuples);
 		info->relallvisible = relallvisible;
+		info->relallfrozen = relallfrozen;
 		info->relkind = relkind;
 		info->indAttNames = indAttNames;
 		info->nindAttNames = nindAttNames;
@@ -6965,6 +6967,7 @@ getTables(Archive *fout, int *numTables)
 	int			i_relpages;
 	int			i_reltuples;
 	int			i_relallvisible;
+	int			i_relallfrozen;
 	int			i_toastpages;
 	int			i_owning_tab;
 	int			i_owning_col;
@@ -7015,8 +7018,13 @@ getTables(Archive *fout, int *numTables)
 						 "c.relowner, "
 						 "c.relchecks, "
 						 "c.relhasindex, c.relhasrules, c.relpages, "
-						 "c.reltuples, c.relallvisible, c.relhastriggers, "
-						 "c.relpersistence, "
+						 "c.reltuples, c.relallvisible, ");
+
+	if (fout->remoteVersion >= 180000)
+		appendPQExpBufferStr(query, "c.relallfrozen, ");
+
+	appendPQExpBufferStr(query,
+						 "c.relhastriggers, c.relpersistence, "
 						 "c.reloftype, "
 						 "c.relacl, "
 						 "acldefault(CASE WHEN c.relkind = " CppAsString2(RELKIND_SEQUENCE)
@@ -7181,6 +7189,7 @@ getTables(Archive *fout, int *numTables)
 	i_relpages = PQfnumber(res, "relpages");
 	i_reltuples = PQfnumber(res, "reltuples");
 	i_relallvisible = PQfnumber(res, "relallvisible");
+	i_relallfrozen = PQfnumber(res, "relallfrozen");
 	i_toastpages = PQfnumber(res, "toastpages");
 	i_owning_tab = PQfnumber(res, "owning_tab");
 	i_owning_col = PQfnumber(res, "owning_col");
@@ -7228,6 +7237,7 @@ getTables(Archive *fout, int *numTables)
 	for (i = 0; i < ntups; i++)
 	{
 		int32		relallvisible = atoi(PQgetvalue(res, i, i_relallvisible));
+		int32		relallfrozen = atoi(PQgetvalue(res, i, i_relallfrozen));
 
 		tblinfo[i].dobj.objType = DO_TABLE;
 		tblinfo[i].dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_reltableoid));
@@ -7330,7 +7340,7 @@ getTables(Archive *fout, int *numTables)
 		if (tblinfo[i].interesting)
 			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relpages,
 								  PQgetvalue(res, i, i_reltuples),
-								  relallvisible, tblinfo[i].relkind, NULL, 0);
+								  relallvisible, relallfrozen, tblinfo[i].relkind, NULL, 0);
 
 		/*
 		 * Read-lock target tables to make sure they aren't DROPPED or altered
@@ -7599,6 +7609,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				i_relpages,
 				i_reltuples,
 				i_relallvisible,
+				i_relallfrozen,
 				i_parentidx,
 				i_indexdef,
 				i_indnkeyatts,
@@ -7653,7 +7664,12 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 	appendPQExpBufferStr(query,
 						 "SELECT t.tableoid, t.oid, i.indrelid, "
 						 "t.relname AS indexname, "
-						 "t.relpages, t.reltuples, t.relallvisible, "
+						 "t.relpages, t.reltuples, t.relallvisible, ");
+
+	if (fout->remoteVersion >= 180000)
+		appendPQExpBufferStr(query, "t.relallfrozen, ");
+
+	appendPQExpBufferStr(query,
 						 "pg_catalog.pg_get_indexdef(i.indexrelid) AS indexdef, "
 						 "i.indkey, i.indisclustered, "
 						 "c.contype, c.conname, "
@@ -7769,6 +7785,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_relpages = PQfnumber(res, "relpages");
 	i_reltuples = PQfnumber(res, "reltuples");
 	i_relallvisible = PQfnumber(res, "relallvisible");
+	i_relallfrozen = PQfnumber(res, "relallfrozen");
 	i_parentidx = PQfnumber(res, "parentidx");
 	i_indexdef = PQfnumber(res, "indexdef");
 	i_indnkeyatts = PQfnumber(res, "indnkeyatts");
@@ -7840,6 +7857,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			RelStatsInfo *relstats;
 			int32		relpages = atoi(PQgetvalue(res, j, i_relpages));
 			int32		relallvisible = atoi(PQgetvalue(res, j, i_relallvisible));
+			int32		relallfrozen = atoi(PQgetvalue(res, j, i_relallfrozen));
 
 			indxinfo[j].dobj.objType = DO_INDEX;
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
@@ -7882,7 +7900,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 
 			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, relpages,
 											 PQgetvalue(res, j, i_reltuples),
-											 relallvisible, indexkind,
+											 relallvisible, relallfrozen, indexkind,
 											 indAttNames, nindAttNames);
 
 			contype = *(PQgetvalue(res, j, i_contype));
@@ -10862,19 +10880,21 @@ printRelationStats(Archive *fout, const void *userArg)
 	initPQExpBuffer(&out);
 
 	/* restore relation stats */
-	appendPQExpBufferStr(&out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
-	appendPQExpBuffer(&out, "\t'version', '%u'::integer,\n",
+	appendPQExpBufferStr(&out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(");
+	appendPQExpBuffer(&out, "\n\t'version', '%u'::integer",
 					  fout->remoteVersion);
-	appendPQExpBufferStr(&out, "\t'schemaname', ");
+	appendPQExpBufferStr(&out, ",\n\t'schemaname', ");
 	appendStringLiteralAH(&out, rsinfo->dobj.namespace->dobj.name, fout);
-	appendPQExpBufferStr(&out, ",\n");
-	appendPQExpBufferStr(&out, "\t'relname', ");
+	appendPQExpBufferStr(&out, ",\n\t'relname', ");
 	appendStringLiteralAH(&out, rsinfo->dobj.name, fout);
-	appendPQExpBufferStr(&out, ",\n");
-	appendPQExpBuffer(&out, "\t'relpages', '%d'::integer,\n", rsinfo->relpages);
-	appendPQExpBuffer(&out, "\t'reltuples', '%s'::real,\n", rsinfo->reltuples);
-	appendPQExpBuffer(&out, "\t'relallvisible', '%d'::integer\n);\n",
-					  rsinfo->relallvisible);
+	appendPQExpBuffer(&out, ",\n\t'relpages', '%d'::integer", rsinfo->relpages);
+	appendPQExpBuffer(&out, ",\n\t'reltuples', '%s'::real", rsinfo->reltuples);
+	appendPQExpBuffer(&out, ",\n\t'relallvisible', '%d'::integer", rsinfo->relallvisible);
+
+	if (fout->remoteVersion >= 180000)
+		appendPQExpBuffer(&out, ",\n\t'relallfrozen', '%d'::integer", rsinfo->relallfrozen);
+
+	appendPQExpBufferStr(&out, "\n);\n");
 
 	AH->txnCount++;
 
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index bbdb30b5f54..82f1eb3c4b7 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -441,6 +441,7 @@ typedef struct _relStatsInfo
 	int32		relpages;
 	char	   *reltuples;
 	int32		relallvisible;
+	int32		relallfrozen;
 	char		relkind;		/* 'r', 'm', 'i', etc */
 
 	/*
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 51ebf8ad13c..576326daec7 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -4771,7 +4771,8 @@ my %tests = (
 			'relname',\s'dup_test_post_data_ix',\s+
 			'relpages',\s'\d+'::integer,\s+
 			'reltuples',\s'\d+'::real,\s+
-			'relallvisible',\s'\d+'::integer\s+
+			'relallvisible',\s'\d+'::integer,\s+
+			'relallfrozen',\s'\d+'::integer\s+
 			\);\s+
 			\QSELECT * FROM pg_catalog.pg_restore_attribute_stats(\E\s+
 			'version',\s'\d+'::integer,\s+
-- 
2.49.0

#500Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#499)
4 attachment(s)
Re: Statistics Import and Export

A rebase and a reordering of the commits to put the really-really-must-have
relallfrozen ahead of the really-must-have stats batching and both of them
head of the error->warning step-downs.

Attachments:

v11-0001-Add-relallfrozen-to-pg_dump-statistics.patchtext/x-patch; charset=US-ASCII; name=v11-0001-Add-relallfrozen-to-pg_dump-statistics.patchDownload
From 96b10b1eb955c5619d23cadf7de8b12d2db638a9 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 15 Mar 2025 17:34:30 -0400
Subject: [PATCH v11 1/4] Add relallfrozen to pg_dump statistics.

The column relallfrozen was recently added to pg_class and it also
represent statistics, so we should add it to the dump/restore/upgrade
operations.

Dumps of databases prior to v18 will not attempt to restore any value to
relallfrozen, allowing pg_restore_relation_stats() to set the default it
deems appropriate.
---
 src/bin/pg_dump/pg_dump.c        | 38 ++++++++++++++++++++++++++------
 src/bin/pg_dump/pg_dump.h        |  1 +
 src/bin/pg_dump/t/002_pg_dump.pl |  3 ++-
 3 files changed, 34 insertions(+), 8 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 84a78625820..211cf10dbd6 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -6874,7 +6874,8 @@ getFuncs(Archive *fout)
  */
 static RelStatsInfo *
 getRelationStatistics(Archive *fout, DumpableObject *rel, int32 relpages,
-					  char *reltuples, int32 relallvisible, char relkind,
+					  char *reltuples, int32 relallvisible,
+					  int32 relallfrozen, char relkind,
 					  char **indAttNames, int nindAttNames)
 {
 	if (!fout->dopt->dumpStatistics)
@@ -6903,6 +6904,7 @@ getRelationStatistics(Archive *fout, DumpableObject *rel, int32 relpages,
 		info->relpages = relpages;
 		info->reltuples = pstrdup(reltuples);
 		info->relallvisible = relallvisible;
+		info->relallfrozen = relallfrozen;
 		info->relkind = relkind;
 		info->indAttNames = indAttNames;
 		info->nindAttNames = nindAttNames;
@@ -6967,6 +6969,7 @@ getTables(Archive *fout, int *numTables)
 	int			i_relpages;
 	int			i_reltuples;
 	int			i_relallvisible;
+	int			i_relallfrozen;
 	int			i_toastpages;
 	int			i_owning_tab;
 	int			i_owning_col;
@@ -7017,8 +7020,13 @@ getTables(Archive *fout, int *numTables)
 						 "c.relowner, "
 						 "c.relchecks, "
 						 "c.relhasindex, c.relhasrules, c.relpages, "
-						 "c.reltuples, c.relallvisible, c.relhastriggers, "
-						 "c.relpersistence, "
+						 "c.reltuples, c.relallvisible, ");
+
+	if (fout->remoteVersion >= 180000)
+		appendPQExpBufferStr(query, "c.relallfrozen, ");
+
+	appendPQExpBufferStr(query,
+						 "c.relhastriggers, c.relpersistence, "
 						 "c.reloftype, "
 						 "c.relacl, "
 						 "acldefault(CASE WHEN c.relkind = " CppAsString2(RELKIND_SEQUENCE)
@@ -7183,6 +7191,7 @@ getTables(Archive *fout, int *numTables)
 	i_relpages = PQfnumber(res, "relpages");
 	i_reltuples = PQfnumber(res, "reltuples");
 	i_relallvisible = PQfnumber(res, "relallvisible");
+	i_relallfrozen = PQfnumber(res, "relallfrozen");
 	i_toastpages = PQfnumber(res, "toastpages");
 	i_owning_tab = PQfnumber(res, "owning_tab");
 	i_owning_col = PQfnumber(res, "owning_col");
@@ -7230,6 +7239,7 @@ getTables(Archive *fout, int *numTables)
 	for (i = 0; i < ntups; i++)
 	{
 		int32		relallvisible = atoi(PQgetvalue(res, i, i_relallvisible));
+		int32		relallfrozen = atoi(PQgetvalue(res, i, i_relallfrozen));
 
 		tblinfo[i].dobj.objType = DO_TABLE;
 		tblinfo[i].dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_reltableoid));
@@ -7336,7 +7346,7 @@ getTables(Archive *fout, int *numTables)
 			stats = getRelationStatistics(fout, &tblinfo[i].dobj,
 										  tblinfo[i].relpages,
 										  PQgetvalue(res, i, i_reltuples),
-										  relallvisible,
+										  relallvisible, relallfrozen,
 										  tblinfo[i].relkind, NULL, 0);
 			if (tblinfo[i].relkind == RELKIND_MATVIEW)
 				tblinfo[i].stats = stats;
@@ -7609,6 +7619,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				i_relpages,
 				i_reltuples,
 				i_relallvisible,
+				i_relallfrozen,
 				i_parentidx,
 				i_indexdef,
 				i_indnkeyatts,
@@ -7663,7 +7674,12 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 	appendPQExpBufferStr(query,
 						 "SELECT t.tableoid, t.oid, i.indrelid, "
 						 "t.relname AS indexname, "
-						 "t.relpages, t.reltuples, t.relallvisible, "
+						 "t.relpages, t.reltuples, t.relallvisible, ");
+
+	if (fout->remoteVersion >= 180000)
+		appendPQExpBufferStr(query, "t.relallfrozen, ");
+
+	appendPQExpBufferStr(query,
 						 "pg_catalog.pg_get_indexdef(i.indexrelid) AS indexdef, "
 						 "i.indkey, i.indisclustered, "
 						 "c.contype, c.conname, "
@@ -7779,6 +7795,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_relpages = PQfnumber(res, "relpages");
 	i_reltuples = PQfnumber(res, "reltuples");
 	i_relallvisible = PQfnumber(res, "relallvisible");
+	i_relallfrozen = PQfnumber(res, "relallfrozen");
 	i_parentidx = PQfnumber(res, "parentidx");
 	i_indexdef = PQfnumber(res, "indexdef");
 	i_indnkeyatts = PQfnumber(res, "indnkeyatts");
@@ -7850,6 +7867,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			RelStatsInfo *relstats;
 			int32		relpages = atoi(PQgetvalue(res, j, i_relpages));
 			int32		relallvisible = atoi(PQgetvalue(res, j, i_relallvisible));
+			int32		relallfrozen = atoi(PQgetvalue(res, j, i_relallfrozen));
 
 			indxinfo[j].dobj.objType = DO_INDEX;
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
@@ -7892,7 +7910,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 
 			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, relpages,
 											 PQgetvalue(res, j, i_reltuples),
-											 relallvisible, indexkind,
+											 relallvisible, relallfrozen, indexkind,
 											 indAttNames, nindAttNames);
 
 			contype = *(PQgetvalue(res, j, i_contype));
@@ -10618,9 +10636,15 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 	appendPQExpBufferStr(out, ",\n");
 	appendPQExpBuffer(out, "\t'relpages', '%d'::integer,\n", rsinfo->relpages);
 	appendPQExpBuffer(out, "\t'reltuples', '%s'::real,\n", rsinfo->reltuples);
-	appendPQExpBuffer(out, "\t'relallvisible', '%d'::integer\n);\n",
+	appendPQExpBuffer(out, "\t'relallvisible', '%d'::integer",
 					  rsinfo->relallvisible);
 
+	if (fout->remoteVersion >= 180000)
+		appendPQExpBuffer(out, ",\n\t'relallfrozen', '%d'::integer", rsinfo->relallfrozen);
+
+	appendPQExpBufferStr(out, "\n);\n");
+
+
 	/* fetch attribute stats */
 	appendPQExpBufferStr(query, "EXECUTE getAttributeStats(");
 	appendStringLiteralAH(query, dobj->namespace->dobj.name, fout);
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 70f7a369e4a..e6f0f86a459 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -442,6 +442,7 @@ typedef struct _relStatsInfo
 	int32		relpages;
 	char	   *reltuples;
 	int32		relallvisible;
+	int32		relallfrozen;
 	char		relkind;		/* 'r', 'm', 'i', etc */
 
 	/*
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 51ebf8ad13c..576326daec7 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -4771,7 +4771,8 @@ my %tests = (
 			'relname',\s'dup_test_post_data_ix',\s+
 			'relpages',\s'\d+'::integer,\s+
 			'reltuples',\s'\d+'::real,\s+
-			'relallvisible',\s'\d+'::integer\s+
+			'relallvisible',\s'\d+'::integer,\s+
+			'relallfrozen',\s'\d+'::integer\s+
 			\);\s+
 			\QSELECT * FROM pg_catalog.pg_restore_attribute_stats(\E\s+
 			'version',\s'\d+'::integer,\s+

base-commit: a0a4601765b896079eb82a9d5cfa1f41154fcfdb
-- 
2.49.0

v11-0004-Downgrade-many-pg_restore_-_stats-errors-to-warn.patchtext/x-patch; charset=US-ASCII; name=v11-0004-Downgrade-many-pg_restore_-_stats-errors-to-warn.patchDownload
From fd6cd3691e21b807a299749631dc0bdddc886853 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 8 Mar 2025 00:52:41 -0500
Subject: [PATCH v11 4/4] Downgrade many pg_restore_*_stats errors to warnings.

We want to avoid errors that can potentially stop an otherwise
successful pg_upgrade or pg_restore operation. With that in mind, change
as many ERROR reports to WARNING + early termination with no data
updated.
---
 src/include/statistics/stat_utils.h        |   4 +-
 src/backend/statistics/attribute_stats.c   | 120 ++++++++++----
 src/backend/statistics/relation_stats.c    |  12 +-
 src/backend/statistics/stat_utils.c        |  65 ++++++--
 src/test/regress/expected/stats_import.out | 184 ++++++++++++++++-----
 src/test/regress/sql/stats_import.sql      |  36 ++--
 6 files changed, 309 insertions(+), 112 deletions(-)

diff --git a/src/include/statistics/stat_utils.h b/src/include/statistics/stat_utils.h
index 512eb776e0e..809c8263a41 100644
--- a/src/include/statistics/stat_utils.h
+++ b/src/include/statistics/stat_utils.h
@@ -21,7 +21,7 @@ struct StatsArgInfo
 	Oid			argtype;
 };
 
-extern void stats_check_required_arg(FunctionCallInfo fcinfo,
+extern bool stats_check_required_arg(FunctionCallInfo fcinfo,
 									 struct StatsArgInfo *arginfo,
 									 int argnum);
 extern bool stats_check_arg_array(FunctionCallInfo fcinfo,
@@ -30,7 +30,7 @@ extern bool stats_check_arg_pair(FunctionCallInfo fcinfo,
 								 struct StatsArgInfo *arginfo,
 								 int argnum1, int argnum2);
 
-extern void stats_lock_check_privileges(Oid reloid);
+extern bool stats_lock_check_privileges(Oid reloid);
 
 extern Oid	stats_lookup_relid(const char *nspname, const char *relname);
 
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index f5eb17ba42d..b7ba1622391 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -100,7 +100,7 @@ static struct StatsArgInfo cleararginfo[] =
 
 static bool attribute_statistics_update(FunctionCallInfo fcinfo);
 static Node *get_attr_expr(Relation rel, int attnum);
-static void get_attr_stat_type(Oid reloid, AttrNumber attnum,
+static bool get_attr_stat_type(Oid reloid, AttrNumber attnum,
 							   Oid *atttypid, int32 *atttypmod,
 							   char *atttyptype, Oid *atttypcoll,
 							   Oid *eq_opr, Oid *lt_opr);
@@ -129,10 +129,12 @@ static void init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
  * stored as an anyarray, and the representation of the array needs to store
  * the correct element type, which must be derived from the attribute.
  *
- * Major errors, such as the table not existing, the attribute not existing,
- * or a permissions failure are always reported at ERROR. Other errors, such
- * as a conversion failure on one statistic kind, are reported as a WARNING
- * and other statistic kinds may still be updated.
+ * This function is called during database upgrades and restorations, therefore
+ * it is imperative to avoid ERRORs that could potentially end the upgrade or
+ * restore unless. Major errors, such as the table not existing, the attribute
+ * not existing, or permissions failure are reported as WARNINGs with an end to
+ * the function, thus allowing the upgrade/restore to continue, but without the
+ * stats that can be regenereated once the database is online again.
  */
 static bool
 attribute_statistics_update(FunctionCallInfo fcinfo)
@@ -148,8 +150,8 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	HeapTuple	statup;
 
 	Oid			atttypid = InvalidOid;
-	int32		atttypmod;
-	char		atttyptype;
+	int32		atttypmod = -1;
+	char		atttyptype = TYPTYPE_PSEUDO; /* Not a great default, but there is no TYPTYPE_INVALID */
 	Oid			atttypcoll = InvalidOid;
 	Oid			eq_opr = InvalidOid;
 	Oid			lt_opr = InvalidOid;
@@ -176,38 +178,52 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 	bool		result = true;
 
-	stats_check_required_arg(fcinfo, attarginfo, ATTRELSCHEMA_ARG);
-	stats_check_required_arg(fcinfo, attarginfo, ATTRELNAME_ARG);
+	if (!stats_check_required_arg(fcinfo, attarginfo, ATTRELSCHEMA_ARG))
+		return false;
+	if (!stats_check_required_arg(fcinfo, attarginfo, ATTRELNAME_ARG))
+		return false;
 
 	nspname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELSCHEMA_ARG));
 	relname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELNAME_ARG));
 
 	reloid = stats_lookup_relid(nspname, relname);
+	if (!OidIsValid(reloid))
+		return false;
 
 	if (RecoveryInProgress())
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("recovery is in progress"),
 				 errhint("Statistics cannot be modified during recovery.")));
+		return false;
+	}
 
 	/* lock before looking up attribute */
-	stats_lock_check_privileges(reloid);
+	if (!stats_lock_check_privileges(reloid))
+		return false;
 
 	/* user can specify either attname or attnum, but not both */
 	if (!PG_ARGISNULL(ATTNAME_ARG))
 	{
 		if (!PG_ARGISNULL(ATTNUM_ARG))
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("cannot specify both attname and attnum")));
+			return false;
+		}
 		attname = TextDatumGetCString(PG_GETARG_DATUM(ATTNAME_ARG));
 		attnum = get_attnum(reloid, attname);
 		/* note that this test covers attisdropped cases too: */
 		if (attnum == InvalidAttrNumber)
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 					(errcode(ERRCODE_UNDEFINED_COLUMN),
 					 errmsg("column \"%s\" of relation \"%s\" does not exist",
 							attname, relname)));
+			return false;
+		}
 	}
 	else if (!PG_ARGISNULL(ATTNUM_ARG))
 	{
@@ -216,27 +232,33 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 		/* annoyingly, get_attname doesn't check attisdropped */
 		if (attname == NULL ||
 			!SearchSysCacheExistsAttName(reloid, attname))
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 					(errcode(ERRCODE_UNDEFINED_COLUMN),
 					 errmsg("column %d of relation \"%s\" does not exist",
 							attnum, relname)));
+			return false;
+		}
 	}
 	else
 	{
-		ereport(ERROR,
+		ereport(WARNING,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("must specify either attname or attnum")));
-		attname = NULL;			/* keep compiler quiet */
-		attnum = 0;
+		return false;
 	}
 
 	if (attnum < 0)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot modify statistics on system column \"%s\"",
 						attname)));
+		return false;
+	}
 
-	stats_check_required_arg(fcinfo, attarginfo, INHERITED_ARG);
+	if (!stats_check_required_arg(fcinfo, attarginfo, INHERITED_ARG))
+		return false;
 	inherited = PG_GETARG_BOOL(INHERITED_ARG);
 
 	/*
@@ -285,10 +307,11 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	}
 
 	/* derive information from attribute */
-	get_attr_stat_type(reloid, attnum,
-					   &atttypid, &atttypmod,
-					   &atttyptype, &atttypcoll,
-					   &eq_opr, &lt_opr);
+	if (!get_attr_stat_type(reloid, attnum,
+							&atttypid, &atttypmod,
+							&atttyptype, &atttypcoll,
+							&eq_opr, &lt_opr))
+		result = false;
 
 	/* if needed, derive element type */
 	if (do_mcelem || do_dechist)
@@ -568,7 +591,7 @@ get_attr_expr(Relation rel, int attnum)
 /*
  * Derive type information from the attribute.
  */
-static void
+static bool
 get_attr_stat_type(Oid reloid, AttrNumber attnum,
 				   Oid *atttypid, int32 *atttypmod,
 				   char *atttyptype, Oid *atttypcoll,
@@ -585,18 +608,26 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum,
 
 	/* Attribute not found */
 	if (!HeapTupleIsValid(atup))
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 errmsg("attribute %d of relation \"%s\" does not exist",
 						attnum, RelationGetRelationName(rel))));
+		relation_close(rel, NoLock);
+		return false;
+	}
 
 	attr = (Form_pg_attribute) GETSTRUCT(atup);
 
 	if (attr->attisdropped)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 errmsg("attribute %d of relation \"%s\" does not exist",
 						attnum, RelationGetRelationName(rel))));
+		relation_close(rel, NoLock);
+		return false;
+	}
 
 	expr = get_attr_expr(rel, attr->attnum);
 
@@ -645,6 +676,7 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum,
 		*atttypcoll = DEFAULT_COLLATION_OID;
 
 	relation_close(rel, NoLock);
+	return true;
 }
 
 /*
@@ -770,6 +802,10 @@ set_stats_slot(Datum *values, bool *nulls, bool *replaces,
 	if (slotidx >= STATISTIC_NUM_SLOTS && first_empty >= 0)
 		slotidx = first_empty;
 
+	/*
+	 * Currently there is no datatype that can have more than STATISTIC_NUM_SLOTS
+	 * statistic kinds, so this can safely remain an ERROR for now.
+	 */
 	if (slotidx >= STATISTIC_NUM_SLOTS)
 		ereport(ERROR,
 				(errmsg("maximum number of statistics slots exceeded: %d",
@@ -915,38 +951,54 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 	AttrNumber	attnum;
 	bool		inherited;
 
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELSCHEMA_ARG);
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELNAME_ARG);
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTNAME_ARG);
-	stats_check_required_arg(fcinfo, cleararginfo, C_INHERITED_ARG);
+	if (!stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELSCHEMA_ARG))
+		PG_RETURN_VOID();
+	if (!stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELNAME_ARG))
+		PG_RETURN_VOID();
+	if (!stats_check_required_arg(fcinfo, cleararginfo, C_ATTNAME_ARG))
+		PG_RETURN_VOID();
+	if (!stats_check_required_arg(fcinfo, cleararginfo, C_INHERITED_ARG))
+		PG_RETURN_VOID();
 
 	nspname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELSCHEMA_ARG));
 	relname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELNAME_ARG));
 
 	reloid = stats_lookup_relid(nspname, relname);
+	if (!OidIsValid(reloid))
+		PG_RETURN_VOID();
 
 	if (RecoveryInProgress())
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("recovery is in progress"),
 				 errhint("Statistics cannot be modified during recovery.")));
+		PG_RETURN_VOID();
+	}
 
-	stats_lock_check_privileges(reloid);
+	if (!stats_lock_check_privileges(reloid))
+		PG_RETURN_VOID();
 
 	attname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTNAME_ARG));
 	attnum = get_attnum(reloid, attname);
 
 	if (attnum < 0)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot clear statistics on system column \"%s\"",
 						attname)));
+		PG_RETURN_VOID();
+	}
 
 	if (attnum == InvalidAttrNumber)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 errmsg("column \"%s\" of relation \"%s\" does not exist",
 						attname, get_rel_name(reloid))));
+		PG_RETURN_VOID();
+	}
 
 	inherited = PG_GETARG_BOOL(C_INHERITED_ARG);
 
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index cd3a75b621a..7c47af15c9f 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -83,13 +83,18 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 	bool		nulls[4] = {0};
 	int			nreplaces = 0;
 
-	stats_check_required_arg(fcinfo, relarginfo, RELSCHEMA_ARG);
-	stats_check_required_arg(fcinfo, relarginfo, RELNAME_ARG);
+	if (!stats_check_required_arg(fcinfo, relarginfo, RELSCHEMA_ARG))
+		return false;
+
+	if (!stats_check_required_arg(fcinfo, relarginfo, RELNAME_ARG))
+		return false;
 
 	nspname = TextDatumGetCString(PG_GETARG_DATUM(RELSCHEMA_ARG));
 	relname = TextDatumGetCString(PG_GETARG_DATUM(RELNAME_ARG));
 
 	reloid = stats_lookup_relid(nspname, relname);
+	if (!OidIsValid(reloid))
+		return false;
 
 	if (RecoveryInProgress())
 		ereport(ERROR,
@@ -97,7 +102,8 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 				 errmsg("recovery is in progress"),
 				 errhint("Statistics cannot be modified during recovery.")));
 
-	stats_lock_check_privileges(reloid);
+	if (!stats_lock_check_privileges(reloid))
+		return false;
 
 	if (!PG_ARGISNULL(RELPAGES_ARG))
 	{
diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c
index a9a3224efe6..d587e875457 100644
--- a/src/backend/statistics/stat_utils.c
+++ b/src/backend/statistics/stat_utils.c
@@ -33,16 +33,20 @@
 /*
  * Ensure that a given argument is not null.
  */
-void
+bool
 stats_check_required_arg(FunctionCallInfo fcinfo,
 						 struct StatsArgInfo *arginfo,
 						 int argnum)
 {
 	if (PG_ARGISNULL(argnum))
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("\"%s\" cannot be NULL",
 						arginfo[argnum].argname)));
+		return false;
+	}
+	return true;
 }
 
 /*
@@ -127,13 +131,14 @@ stats_check_arg_pair(FunctionCallInfo fcinfo,
  *   - the role owns the current database and the relation is not shared
  *   - the role has the MAINTAIN privilege on the relation
  */
-void
+bool
 stats_lock_check_privileges(Oid reloid)
 {
 	Relation	table;
 	Oid			table_oid = reloid;
 	Oid			index_oid = InvalidOid;
 	LOCKMODE	index_lockmode = NoLock;
+	bool		ok = true;
 
 	/*
 	 * For indexes, we follow the locking behavior in do_analyze_rel() and
@@ -173,14 +178,15 @@ stats_lock_check_privileges(Oid reloid)
 		case RELKIND_PARTITIONED_TABLE:
 			break;
 		default:
-			ereport(ERROR,
+			ereport(WARNING,
 					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 					 errmsg("cannot modify statistics for relation \"%s\"",
 							RelationGetRelationName(table)),
 					 errdetail_relkind_not_supported(table->rd_rel->relkind)));
+		ok = false;
 	}
 
-	if (OidIsValid(index_oid))
+	if (ok && (OidIsValid(index_oid)))
 	{
 		Relation	index;
 
@@ -193,25 +199,33 @@ stats_lock_check_privileges(Oid reloid)
 		relation_close(index, NoLock);
 	}
 
-	if (table->rd_rel->relisshared)
-		ereport(ERROR,
+	if (ok && (table->rd_rel->relisshared))
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot modify statistics for shared relation")));
+		ok = false;
+	}
 
-	if (!object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId()))
+	if (ok && (!object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId())))
 	{
 		AclResult	aclresult = pg_class_aclcheck(RelationGetRelid(table),
 												  GetUserId(),
 												  ACL_MAINTAIN);
 
 		if (aclresult != ACLCHECK_OK)
-			aclcheck_error(aclresult,
-						   get_relkind_objtype(table->rd_rel->relkind),
-						   NameStr(table->rd_rel->relname));
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+						errmsg("permission denied for relation %s",
+							   NameStr(table->rd_rel->relname))));
+			ok = false;
+		}
 	}
 
 	/* retain lock on table */
 	relation_close(table, NoLock);
+	return ok;
 }
 
 /*
@@ -223,10 +237,20 @@ stats_lookup_relid(const char *nspname, const char *relname)
 	Oid			nspoid;
 	Oid			reloid;
 
-	nspoid = LookupExplicitNamespace(nspname, false);
+	nspoid = LookupExplicitNamespace(nspname, true);
+	if (!OidIsValid(nspoid))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_UNDEFINED_TABLE),
+				 errmsg("relation \"%s.%s\" does not exist",
+						nspname, relname)));
+
+		return InvalidOid;
+	}
+
 	reloid = get_relname_relid(relname, nspoid);
 	if (!OidIsValid(reloid))
-		ereport(ERROR,
+		ereport(WARNING,
 				(errcode(ERRCODE_UNDEFINED_TABLE),
 				 errmsg("relation \"%s.%s\" does not exist",
 						nspname, relname)));
@@ -303,9 +327,12 @@ stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
 								  &args, &types, &argnulls);
 
 	if (nargs % 2 != 0)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				errmsg("variadic arguments must be name/value pairs"),
 				errhint("Provide an even number of variadic arguments that can be divided into pairs."));
+		return false;
+	}
 
 	/*
 	 * For each argument name/value pair, find corresponding positional
@@ -318,14 +345,20 @@ stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
 		char	   *argname;
 
 		if (argnulls[i])
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 					(errmsg("name at variadic position %d is NULL", i + 1)));
+			return false;
+		}
 
 		if (types[i] != TEXTOID)
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 					(errmsg("name at variadic position %d has type \"%s\", expected type \"%s\"",
 							i + 1, format_type_be(types[i]),
 							format_type_be(TEXTOID))));
+			return false;
+		}
 
 		if (argnulls[i + 1])
 			continue;
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index 48d6392b4ad..161cf67b711 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -46,49 +46,85 @@ SELECT pg_clear_relation_stats('stats_import', 'test');
 --
 -- relstats tests
 --
--- error: schemaname missing
+-- warning: schemaname missing, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'relname', 'test',
         'relpages', 17::integer);
-ERROR:  "schemaname" cannot be NULL
--- error: relname missing
+WARNING:  "schemaname" cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+-- warning: relname missing, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relpages', 17::integer);
-ERROR:  "relname" cannot be NULL
---- error: schemaname is wrong type
+WARNING:  "relname" cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+--- warning: schemaname is wrong type, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 3.6::float,
         'relname', 'test',
         'relpages', 17::integer);
 WARNING:  argument "schemaname" has type "double precision", expected type "text"
-ERROR:  "schemaname" cannot be NULL
---- error: relname is wrong type
+WARNING:  "schemaname" cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+--- warning: relname is wrong type, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 0::oid,
         'relpages', 17::integer);
 WARNING:  argument "relname" has type "oid", expected type "text"
-ERROR:  "relname" cannot be NULL
--- error: relation not found
+WARNING:  "relname" cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+-- warning: relation not found, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'nope',
         'relpages', 17::integer);
-ERROR:  relation "stats_import.nope" does not exist
--- error: odd number of variadic arguments cannot be pairs
+WARNING:  relation "stats_import.nope" does not exist
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+-- warning: odd number of variadic arguments cannot be pairs, nothing updated
 SELECT pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'test',
         'relallvisible');
-ERROR:  variadic arguments must be name/value pairs
+WARNING:  variadic arguments must be name/value pairs
 HINT:  Provide an even number of variadic arguments that can be divided into pairs.
--- error: argument name is NULL
+WARNING:  "schemaname" cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+-- warning: argument name is NULL, nothing updated
 SELECT pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'test',
         NULL, '17'::integer);
-ERROR:  name at variadic position 5 is NULL
+WARNING:  name at variadic position 5 is NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
 -- starting stats
 SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
@@ -340,65 +376,110 @@ CREATE SEQUENCE stats_import.testseq;
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'testseq');
-ERROR:  cannot modify statistics for relation "testseq"
+WARNING:  cannot modify statistics for relation "testseq"
 DETAIL:  This operation is not supported for sequences.
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
 SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testseq');
-ERROR:  cannot modify statistics for relation "testseq"
+WARNING:  cannot modify statistics for relation "testseq"
 DETAIL:  This operation is not supported for sequences.
+ pg_clear_relation_stats 
+-------------------------
+ 
+(1 row)
+
 CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
 SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testview');
-ERROR:  cannot modify statistics for relation "testview"
+WARNING:  cannot modify statistics for relation "testview"
 DETAIL:  This operation is not supported for views.
+ pg_clear_relation_stats 
+-------------------------
+ 
+(1 row)
+
 --
 -- attribute stats
 --
--- error: schemaname missing
+-- warning: schemaname missing, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relname', 'test',
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  "schemaname" cannot be NULL
--- error: schema does not exist
+WARNING:  "schemaname" cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: schema does not exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'nope',
     'relname', 'test',
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  schema "nope" does not exist
--- error: relname missing
+WARNING:  relation "nope.test" does not exist
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: relname missing, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  "relname" cannot be NULL
--- error: relname does not exist
+WARNING:  "relname" cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: relname does not exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'nope',
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  relation "stats_import.nope" does not exist
--- error: relname null
+WARNING:  relation "stats_import.nope" does not exist
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: relname null, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', NULL,
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  "relname" cannot be NULL
--- error: NULL attname
+WARNING:  "relname" cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: NULL attname, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
     'attname', NULL,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  must specify either attname or attnum
--- error: attname doesn't exist
+WARNING:  must specify either attname or attnum
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: attname doesn't exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -407,8 +488,13 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'null_frac', 0.1::real,
     'avg_width', 2::integer,
     'n_distinct', 0.3::real);
-ERROR:  column "nope" of relation "test" does not exist
--- error: both attname and attnum
+WARNING:  column "nope" of relation "test" does not exist
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: both attname and attnum, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -416,30 +502,50 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'attnum', 1::smallint,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  cannot specify both attname and attnum
--- error: neither attname nor attnum
+WARNING:  cannot specify both attname and attnum
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: neither attname nor attnum, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  must specify either attname or attnum
--- error: attribute is system column
+WARNING:  must specify either attname or attnum
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: attribute is system column, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
     'attname', 'xmin',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  cannot modify statistics on system column "xmin"
--- error: inherited null
+WARNING:  cannot modify statistics on system column "xmin"
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: inherited null, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
     'attname', 'id',
     'inherited', NULL::boolean,
     'null_frac', 0.1::real);
-ERROR:  "inherited" cannot be NULL
+WARNING:  "inherited" cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
 -- ok: just the fixed values, with version, no stakinds
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index d140733a750..be8045ceea5 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -39,41 +39,41 @@ SELECT pg_clear_relation_stats('stats_import', 'test');
 -- relstats tests
 --
 
--- error: schemaname missing
+-- warning: schemaname missing, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'relname', 'test',
         'relpages', 17::integer);
 
--- error: relname missing
+-- warning: relname missing, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relpages', 17::integer);
 
---- error: schemaname is wrong type
+--- warning: schemaname is wrong type, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 3.6::float,
         'relname', 'test',
         'relpages', 17::integer);
 
---- error: relname is wrong type
+--- warning: relname is wrong type, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 0::oid,
         'relpages', 17::integer);
 
--- error: relation not found
+-- warning: relation not found, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'nope',
         'relpages', 17::integer);
 
--- error: odd number of variadic arguments cannot be pairs
+-- warning: odd number of variadic arguments cannot be pairs, nothing updated
 SELECT pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'test',
         'relallvisible');
 
--- error: argument name is NULL
+-- warning: argument name is NULL, nothing updated
 SELECT pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'test',
@@ -246,14 +246,14 @@ SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname
 -- attribute stats
 --
 
--- error: schemaname missing
+-- warning: schemaname missing, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relname', 'test',
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: schema does not exist
+-- warning: schema does not exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'nope',
     'relname', 'test',
@@ -261,14 +261,14 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: relname missing
+-- warning: relname missing, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: relname does not exist
+-- warning: relname does not exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'nope',
@@ -276,7 +276,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: relname null
+-- warning: relname null, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', NULL,
@@ -284,7 +284,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: NULL attname
+-- warning: NULL attname, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -292,7 +292,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: attname doesn't exist
+-- warning: attname doesn't exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -302,7 +302,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'avg_width', 2::integer,
     'n_distinct', 0.3::real);
 
--- error: both attname and attnum
+-- warning: both attname and attnum, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -311,14 +311,14 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: neither attname nor attnum
+-- warning: neither attname nor attnum, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: attribute is system column
+-- warning: attribute is system column, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -326,7 +326,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: inherited null
+-- warning: inherited null, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
-- 
2.49.0

v11-0003-Batching-getAttributeStats.patchtext/x-patch; charset=US-ASCII; name=v11-0003-Batching-getAttributeStats.patchDownload
From 7b226732d1e68b5899c3dc8fbc6eb940c0f884ad Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Fri, 14 Mar 2025 03:54:26 -0400
Subject: [PATCH v11 3/4] Batching getAttributeStats().

The prepared statement getAttributeStats() is fairly heavyweight and
could greatly increase pg_dump/pg_upgrade runtime. To alleviate this,
create a result set buffer of all of the attribute stats fetched for a
batch of 100 relations that could potentially have stats.

The query ensures that the order of results exactly matches the needs of
the code walking the TOC to print the stats calls.
---
 src/bin/pg_dump/pg_dump.c | 554 ++++++++++++++++++++++++++------------
 1 file changed, 383 insertions(+), 171 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index b7571ea15eb..bcf9dd1eb47 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -143,6 +143,25 @@ typedef enum OidOptions
 	zeroAsNone = 4,
 } OidOptions;
 
+typedef enum StatsBufferState
+{
+	STATSBUF_UNINITIALIZED = 0,
+	STATSBUF_ACTIVE,
+	STATSBUF_EXHAUSTED
+}			StatsBufferState;
+
+typedef struct
+{
+	PGresult   *res;			/* results from most recent
+								 * getAttributeStats() */
+	int			idx;			/* first un-consumed row of results */
+	TocEntry   *te;				/* next TOC entry to search for statsitics
+								 * data */
+
+	StatsBufferState state;		/* current state of the buffer */
+}			AttributeStatsBuffer;
+
+
 /* global decls */
 static bool dosync = true;		/* Issue fsync() to make dump durable on disk. */
 
@@ -209,6 +228,18 @@ static int	nbinaryUpgradeClassOids = 0;
 static SequenceItem *sequences = NULL;
 static int	nsequences = 0;
 
+static AttributeStatsBuffer attrstats =
+{
+	NULL, 0, NULL, STATSBUF_UNINITIALIZED
+};
+
+/*
+ * The maximum number of relations that should be fetched in any one
+ * getAttributeStats() call.
+ */
+
+#define MAX_ATTR_STATS_RELS 100
+
 /*
  * The default number of rows per INSERT when
  * --inserts is specified without --rows-per-insert
@@ -222,6 +253,8 @@ static int	nsequences = 0;
  */
 #define MAX_BLOBS_PER_ARCHIVE_ENTRY 1000
 
+
+
 /*
  * Macro for producing quoted, schema-qualified name of a dumpable object.
  */
@@ -399,6 +432,9 @@ static void setupDumpWorker(Archive *AH);
 static TableInfo *getRootTableInfo(const TableInfo *tbinfo);
 static bool forcePartitionRootLoad(const TableInfo *tbinfo);
 static void read_dump_filters(const char *filename, DumpOptions *dopt);
+static void appendNamedArgument(PQExpBuffer out, Archive *fout,
+								const char *argname, const char *argtype,
+								const char *argval);
 
 
 int
@@ -10556,7 +10592,286 @@ appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
 }
 
 /*
- * printDumpRelationStats --
+ * Fetch next batch of rows from getAttributeStats()
+ */
+static void
+fetchNextAttributeStats(Archive *fout)
+{
+	ArchiveHandle *AH = (ArchiveHandle *) fout;
+	PQExpBufferData schemas;
+	PQExpBufferData relations;
+	int			numoids = 0;
+
+	Assert(AH != NULL);
+
+	/* free last result set, if any */
+	if (attrstats.state == STATSBUF_ACTIVE)
+		PQclear(attrstats.res);
+
+	/* If we have looped around to the start of the TOC, restart */
+	if (attrstats.te == AH->toc)
+		attrstats.te = AH->toc->next;
+
+	initPQExpBuffer(&schemas);
+	initPQExpBuffer(&relations);
+
+	/*
+	 * Walk ahead looking for relstats entries that are active in this
+	 * section, adding the names to the schemas and relations lists.
+	 */
+	while ((attrstats.te != AH->toc) && (numoids < MAX_ATTR_STATS_RELS))
+	{
+		if (attrstats.te->reqs != 0 &&
+			strcmp(attrstats.te->desc, "STATISTICS DATA") == 0)
+		{
+			RelStatsInfo *rsinfo = (RelStatsInfo *) attrstats.te->createDumperArg;
+
+			Assert(rsinfo != NULL);
+
+			if (numoids > 0)
+			{
+				appendPQExpBufferStr(&schemas, ",");
+				appendPQExpBufferStr(&relations, ",");
+			}
+			appendPQExpBufferStr(&schemas, fmtId(rsinfo->dobj.namespace->dobj.name));
+			appendPQExpBufferStr(&relations, fmtId(rsinfo->dobj.name));
+			numoids++;
+		}
+
+		attrstats.te = attrstats.te->next;
+	}
+
+	if (numoids > 0)
+	{
+		PQExpBufferData query;
+
+		initPQExpBuffer(&query);
+		appendPQExpBuffer(&query,
+						  "EXECUTE getAttributeStats('{%s}'::pg_catalog.text[],'{%s}'::pg_catalog.text[])",
+						  schemas.data, relations.data);
+		attrstats.res = ExecuteSqlQuery(fout, query.data, PGRES_TUPLES_OK);
+		attrstats.idx = 0;
+	}
+	else
+	{
+		attrstats.state = STATSBUF_EXHAUSTED;
+		attrstats.res = NULL;
+		attrstats.idx = -1;
+	}
+
+	termPQExpBuffer(&schemas);
+	termPQExpBuffer(&relations);
+}
+
+/*
+ * Prepare the getAttributeStats() statement
+ *
+ * This is done automatically if the user specified dumpStatistics.
+ */
+static void
+initAttributeStats(Archive *fout)
+{
+	ArchiveHandle *AH = (ArchiveHandle *) fout;
+	PQExpBufferData query;
+
+	Assert(AH != NULL);
+	initPQExpBuffer(&query);
+
+	appendPQExpBufferStr(&query,
+						 "PREPARE getAttributeStats(pg_catalog.text[], pg_catalog.text[]) AS\n"
+						 "SELECT s.schemaname, s.tablename, s.attname, s.inherited, "
+						 "s.null_frac, s.avg_width, s.n_distinct, s.most_common_vals, "
+						 "s.most_common_freqs, s.histogram_bounds, s.correlation, "
+						 "s.most_common_elems, s.most_common_elem_freqs, "
+						 "s.elem_count_histogram, ");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(&query,
+							 "s.range_length_histogram, "
+							 "s.range_empty_frac, "
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(&query,
+							 "NULL AS range_length_histogram, "
+							 "NULL AS range_empty_frac, "
+							 " NULL AS range_bounds_histogram ");
+
+	/*
+	 * The results must be in the order of relations supplied in the
+	 * parameters to ensure that they are in sync with a walk of the TOC.
+	 *
+	 * The redundant (and incomplete) filter clause on s.tablename = ANY(...)
+	 * is a way to lead the query into using the index
+	 * pg_class_relname_nsp_index which in turn allows the planner to avoid an
+	 * expensive full scan of pg_stats.
+	 *
+	 * We may need to adjust this query for versions that are not so easily
+	 * led.
+	 */
+	appendPQExpBufferStr(&query,
+						 "FROM pg_catalog.pg_stats AS s "
+						 "JOIN unnest($1, $2) WITH ORDINALITY AS u(schemaname, tablename, ord) "
+						 "ON s.schemaname = u.schemaname "
+						 "AND s.tablename = u.tablename "
+						 "WHERE s.tablename = ANY($2) "
+						 "ORDER BY u.ord, s.attname, s.inherited");
+
+	ExecuteSqlStatement(fout, query.data);
+
+	termPQExpBuffer(&query);
+
+	attrstats.te = AH->toc->next;
+
+	fetchNextAttributeStats(fout);
+
+	attrstats.state = STATSBUF_ACTIVE;
+}
+
+
+/*
+ * append a single attribute stat to the buffer for this relation.
+ */
+static void
+appendAttributeStats(Archive *fout, PQExpBuffer out,
+					 const RelStatsInfo *rsinfo)
+{
+	PGresult   *res = attrstats.res;
+	int			tup_num = attrstats.idx;
+
+	const char *attname;
+
+	static bool indexes_set = false;
+	static int	i_attname,
+				i_inherited,
+				i_null_frac,
+				i_avg_width,
+				i_n_distinct,
+				i_most_common_vals,
+				i_most_common_freqs,
+				i_histogram_bounds,
+				i_correlation,
+				i_most_common_elems,
+				i_most_common_elem_freqs,
+				i_elem_count_histogram,
+				i_range_length_histogram,
+				i_range_empty_frac,
+				i_range_bounds_histogram;
+
+	if (!indexes_set)
+	{
+		/*
+		 * It's a prepared statement, so the indexes will be the same for all
+		 * result sets, so we only need to set them once.
+		 */
+		i_attname = PQfnumber(res, "attname");
+		i_inherited = PQfnumber(res, "inherited");
+		i_null_frac = PQfnumber(res, "null_frac");
+		i_avg_width = PQfnumber(res, "avg_width");
+		i_n_distinct = PQfnumber(res, "n_distinct");
+		i_most_common_vals = PQfnumber(res, "most_common_vals");
+		i_most_common_freqs = PQfnumber(res, "most_common_freqs");
+		i_histogram_bounds = PQfnumber(res, "histogram_bounds");
+		i_correlation = PQfnumber(res, "correlation");
+		i_most_common_elems = PQfnumber(res, "most_common_elems");
+		i_most_common_elem_freqs = PQfnumber(res, "most_common_elem_freqs");
+		i_elem_count_histogram = PQfnumber(res, "elem_count_histogram");
+		i_range_length_histogram = PQfnumber(res, "range_length_histogram");
+		i_range_empty_frac = PQfnumber(res, "range_empty_frac");
+		i_range_bounds_histogram = PQfnumber(res, "range_bounds_histogram");
+		indexes_set = true;
+	}
+
+	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+	appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
+					  fout->remoteVersion);
+	appendPQExpBufferStr(out, "\t'schemaname', ");
+	appendStringLiteralAH(out, rsinfo->dobj.namespace->dobj.name, fout);
+	appendPQExpBufferStr(out, ",\n\t'relname', ");
+	appendStringLiteralAH(out, rsinfo->dobj.name, fout);
+
+	if (PQgetisnull(res, tup_num, i_attname))
+		pg_fatal("attname cannot be NULL");
+	attname = PQgetvalue(res, tup_num, i_attname);
+
+	/*
+	 * Indexes look up attname in indAttNames to derive attnum, all others use
+	 * attname directly.  We must specify attnum for indexes, since their
+	 * attnames are not necessarily stable across dump/reload.
+	 */
+	if (rsinfo->nindAttNames == 0)
+	{
+		appendPQExpBuffer(out, ",\n\t'attname', ");
+		appendStringLiteralAH(out, attname, fout);
+	}
+	else
+	{
+		bool		found = false;
+
+		for (int i = 0; i < rsinfo->nindAttNames; i++)
+			if (strcmp(attname, rsinfo->indAttNames[i]) == 0)
+			{
+				appendPQExpBuffer(out, ",\n\t'attnum', '%d'::smallint",
+								  i + 1);
+				found = true;
+				break;
+			}
+
+		if (!found)
+			pg_fatal("could not find index attname \"%s\"", attname);
+	}
+
+	if (!PQgetisnull(res, tup_num, i_inherited))
+		appendNamedArgument(out, fout, "inherited", "boolean",
+							PQgetvalue(res, tup_num, i_inherited));
+	if (!PQgetisnull(res, tup_num, i_null_frac))
+		appendNamedArgument(out, fout, "null_frac", "real",
+							PQgetvalue(res, tup_num, i_null_frac));
+	if (!PQgetisnull(res, tup_num, i_avg_width))
+		appendNamedArgument(out, fout, "avg_width", "integer",
+							PQgetvalue(res, tup_num, i_avg_width));
+	if (!PQgetisnull(res, tup_num, i_n_distinct))
+		appendNamedArgument(out, fout, "n_distinct", "real",
+							PQgetvalue(res, tup_num, i_n_distinct));
+	if (!PQgetisnull(res, tup_num, i_most_common_vals))
+		appendNamedArgument(out, fout, "most_common_vals", "text",
+							PQgetvalue(res, tup_num, i_most_common_vals));
+	if (!PQgetisnull(res, tup_num, i_most_common_freqs))
+		appendNamedArgument(out, fout, "most_common_freqs", "real[]",
+							PQgetvalue(res, tup_num, i_most_common_freqs));
+	if (!PQgetisnull(res, tup_num, i_histogram_bounds))
+		appendNamedArgument(out, fout, "histogram_bounds", "text",
+							PQgetvalue(res, tup_num, i_histogram_bounds));
+	if (!PQgetisnull(res, tup_num, i_correlation))
+		appendNamedArgument(out, fout, "correlation", "real",
+							PQgetvalue(res, tup_num, i_correlation));
+	if (!PQgetisnull(res, tup_num, i_most_common_elems))
+		appendNamedArgument(out, fout, "most_common_elems", "text",
+							PQgetvalue(res, tup_num, i_most_common_elems));
+	if (!PQgetisnull(res, tup_num, i_most_common_elem_freqs))
+		appendNamedArgument(out, fout, "most_common_elem_freqs", "real[]",
+							PQgetvalue(res, tup_num, i_most_common_elem_freqs));
+	if (!PQgetisnull(res, tup_num, i_elem_count_histogram))
+		appendNamedArgument(out, fout, "elem_count_histogram", "real[]",
+							PQgetvalue(res, tup_num, i_elem_count_histogram));
+	if (fout->remoteVersion >= 170000)
+	{
+		if (!PQgetisnull(res, tup_num, i_range_length_histogram))
+			appendNamedArgument(out, fout, "range_length_histogram", "text",
+								PQgetvalue(res, tup_num, i_range_length_histogram));
+		if (!PQgetisnull(res, tup_num, i_range_empty_frac))
+			appendNamedArgument(out, fout, "range_empty_frac", "real",
+								PQgetvalue(res, tup_num, i_range_empty_frac));
+		if (!PQgetisnull(res, tup_num, i_range_bounds_histogram))
+			appendNamedArgument(out, fout, "range_bounds_histogram", "text",
+								PQgetvalue(res, tup_num, i_range_bounds_histogram));
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+
+
+/*
+ * printRelationStats --
  *
  * Generate the SQL statements needed to restore a relation's statistics.
  */
@@ -10564,64 +10879,21 @@ static char *
 printRelationStats(Archive *fout, const void *userArg)
 {
 	const RelStatsInfo *rsinfo = (RelStatsInfo *) userArg;
-	const DumpableObject *dobj = &rsinfo->dobj;
+	const DumpableObject *dobj;
+	const char *relschema;
+	const char *relname;
+
+	ArchiveHandle *AH = (ArchiveHandle *) fout;
 
-	PQExpBufferData query;
 	PQExpBufferData out;
 
-	PGresult   *res;
-
-	static bool first_query = true;
-	static int	i_attname;
-	static int	i_inherited;
-	static int	i_null_frac;
-	static int	i_avg_width;
-	static int	i_n_distinct;
-	static int	i_most_common_vals;
-	static int	i_most_common_freqs;
-	static int	i_histogram_bounds;
-	static int	i_correlation;
-	static int	i_most_common_elems;
-	static int	i_most_common_elem_freqs;
-	static int	i_elem_count_histogram;
-	static int	i_range_length_histogram;
-	static int	i_range_empty_frac;
-	static int	i_range_bounds_histogram;
-
-	initPQExpBuffer(&query);
-
-	if (first_query)
-	{
-		appendPQExpBufferStr(&query,
-							 "PREPARE getAttributeStats(pg_catalog.text, pg_catalog.text) AS\n"
-							 "SELECT s.attname, s.inherited, "
-							 "s.null_frac, s.avg_width, s.n_distinct, "
-							 "s.most_common_vals, s.most_common_freqs, "
-							 "s.histogram_bounds, s.correlation, "
-							 "s.most_common_elems, s.most_common_elem_freqs, "
-							 "s.elem_count_histogram, ");
-
-		if (fout->remoteVersion >= 170000)
-			appendPQExpBufferStr(&query,
-								 "s.range_length_histogram, "
-								 "s.range_empty_frac, "
-								 "s.range_bounds_histogram ");
-		else
-			appendPQExpBufferStr(&query,
-								 "NULL AS range_length_histogram,"
-								 "NULL AS range_empty_frac,"
-								 "NULL AS range_bounds_histogram ");
-
-		appendPQExpBufferStr(&query,
-							 "FROM pg_catalog.pg_stats s "
-							 "WHERE s.schemaname = $1 "
-							 "AND s.tablename = $2 "
-							 "ORDER BY s.attname, s.inherited");
-
-		ExecuteSqlStatement(fout, query.data);
-
-		resetPQExpBuffer(&query);
-	}
+	Assert(rsinfo != NULL);
+	dobj = &rsinfo->dobj;
+	Assert(dobj != NULL);
+	relschema = dobj->namespace->dobj.name;
+	Assert(relschema != NULL);
+	relname = dobj->name;
+	Assert(relname != NULL);
 
 	initPQExpBuffer(&out);
 
@@ -10642,132 +10914,72 @@ printRelationStats(Archive *fout, const void *userArg)
 	appendPQExpBufferStr(&out, "\n);\n");
 
 
-	/* fetch attribute stats */
-	appendPQExpBufferStr(&query, "EXECUTE getAttributeStats(");
-	appendStringLiteralAH(&query, dobj->namespace->dobj.name, fout);
-	appendPQExpBufferStr(&query, ", ");
-	appendStringLiteralAH(&query, dobj->name, fout);
-	appendPQExpBufferStr(&query, ")");
+	AH->txnCount++;
 
-	res = ExecuteSqlQuery(fout, query.data, PGRES_TUPLES_OK);
+	if (attrstats.state == STATSBUF_UNINITIALIZED)
+		initAttributeStats(fout);
 
-	if (first_query)
+	/*
+	 * Because the query returns rows in the same order as the relations
+	 * requested, and because every relation gets at least one row in the
+	 * result set, the first row for this relation must correspond either to
+	 * the current row of this result set (if one exists) or the first row of
+	 * the next result set (if this one is already consumed).
+	 */
+	if (attrstats.state != STATSBUF_ACTIVE)
+		pg_fatal("Exhausted getAttributeStats() before processing %s.%s",
+				 rsinfo->dobj.namespace->dobj.name,
+				 rsinfo->dobj.name);
+
+	/*
+	 * If the current result set has been fully consumed, then the row(s) we
+	 * need (if any) would be found in the next one. This will update
+	 * attrstats.res and attrstats.idx.
+	 */
+	if (PQntuples(attrstats.res) <= attrstats.idx)
+		fetchNextAttributeStats(fout);
+
+	while (true)
 	{
-		i_attname = PQfnumber(res, "attname");
-		i_inherited = PQfnumber(res, "inherited");
-		i_null_frac = PQfnumber(res, "null_frac");
-		i_avg_width = PQfnumber(res, "avg_width");
-		i_n_distinct = PQfnumber(res, "n_distinct");
-		i_most_common_vals = PQfnumber(res, "most_common_vals");
-		i_most_common_freqs = PQfnumber(res, "most_common_freqs");
-		i_histogram_bounds = PQfnumber(res, "histogram_bounds");
-		i_correlation = PQfnumber(res, "correlation");
-		i_most_common_elems = PQfnumber(res, "most_common_elems");
-		i_most_common_elem_freqs = PQfnumber(res, "most_common_elem_freqs");
-		i_elem_count_histogram = PQfnumber(res, "elem_count_histogram");
-		i_range_length_histogram = PQfnumber(res, "range_length_histogram");
-		i_range_empty_frac = PQfnumber(res, "range_empty_frac");
-		i_range_bounds_histogram = PQfnumber(res, "range_bounds_histogram");
-		first_query = false;
-	}
-
-	/* restore attribute stats */
-	for (int rownum = 0; rownum < PQntuples(res); rownum++)
-	{
-		const char *attname;
-
-		appendPQExpBufferStr(&out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
-		appendPQExpBuffer(&out, "\t'version', '%u'::integer,\n",
-						  fout->remoteVersion);
-		appendPQExpBufferStr(&out, "\t'schemaname', ");
-		appendStringLiteralAH(&out, rsinfo->dobj.namespace->dobj.name, fout);
-		appendPQExpBufferStr(&out, ",\n\t'relname', ");
-		appendStringLiteralAH(&out, rsinfo->dobj.name, fout);
-
-		if (PQgetisnull(res, rownum, i_attname))
-			pg_fatal("attname cannot be NULL");
-		attname = PQgetvalue(res, rownum, i_attname);
+		int			i_schemaname;
+		int			i_tablename;
+		char	   *schemaname;
+		char	   *tablename;	/* misnomer, following pg_stats naming */
 
 		/*
-		 * Indexes look up attname in indAttNames to derive attnum, all others
-		 * use attname directly.  We must specify attnum for indexes, since
-		 * their attnames are not necessarily stable across dump/reload.
+		 * If we hit the end of the result set, then there are no more records
+		 * for this relation, so we should stop, but first get the next result
+		 * set for the next batch of relations.
 		 */
-		if (rsinfo->nindAttNames == 0)
+		if (PQntuples(attrstats.res) <= attrstats.idx)
 		{
-			appendPQExpBuffer(&out, ",\n\t'attname', ");
-			appendStringLiteralAH(&out, attname, fout);
-		}
-		else
-		{
-			bool		found = false;
-
-			for (int i = 0; i < rsinfo->nindAttNames; i++)
-			{
-				if (strcmp(attname, rsinfo->indAttNames[i]) == 0)
-				{
-					appendPQExpBuffer(&out, ",\n\t'attnum', '%d'::smallint",
-									  i + 1);
-					found = true;
-					break;
-				}
-			}
-
-			if (!found)
-				pg_fatal("could not find index attname \"%s\"", attname);
+			fetchNextAttributeStats(fout);
+			break;
 		}
 
-		if (!PQgetisnull(res, rownum, i_inherited))
-			appendNamedArgument(&out, fout, "inherited", "boolean",
-								PQgetvalue(res, rownum, i_inherited));
-		if (!PQgetisnull(res, rownum, i_null_frac))
-			appendNamedArgument(&out, fout, "null_frac", "real",
-								PQgetvalue(res, rownum, i_null_frac));
-		if (!PQgetisnull(res, rownum, i_avg_width))
-			appendNamedArgument(&out, fout, "avg_width", "integer",
-								PQgetvalue(res, rownum, i_avg_width));
-		if (!PQgetisnull(res, rownum, i_n_distinct))
-			appendNamedArgument(&out, fout, "n_distinct", "real",
-								PQgetvalue(res, rownum, i_n_distinct));
-		if (!PQgetisnull(res, rownum, i_most_common_vals))
-			appendNamedArgument(&out, fout, "most_common_vals", "text",
-								PQgetvalue(res, rownum, i_most_common_vals));
-		if (!PQgetisnull(res, rownum, i_most_common_freqs))
-			appendNamedArgument(&out, fout, "most_common_freqs", "real[]",
-								PQgetvalue(res, rownum, i_most_common_freqs));
-		if (!PQgetisnull(res, rownum, i_histogram_bounds))
-			appendNamedArgument(&out, fout, "histogram_bounds", "text",
-								PQgetvalue(res, rownum, i_histogram_bounds));
-		if (!PQgetisnull(res, rownum, i_correlation))
-			appendNamedArgument(&out, fout, "correlation", "real",
-								PQgetvalue(res, rownum, i_correlation));
-		if (!PQgetisnull(res, rownum, i_most_common_elems))
-			appendNamedArgument(&out, fout, "most_common_elems", "text",
-								PQgetvalue(res, rownum, i_most_common_elems));
-		if (!PQgetisnull(res, rownum, i_most_common_elem_freqs))
-			appendNamedArgument(&out, fout, "most_common_elem_freqs", "real[]",
-								PQgetvalue(res, rownum, i_most_common_elem_freqs));
-		if (!PQgetisnull(res, rownum, i_elem_count_histogram))
-			appendNamedArgument(&out, fout, "elem_count_histogram", "real[]",
-								PQgetvalue(res, rownum, i_elem_count_histogram));
-		if (fout->remoteVersion >= 170000)
-		{
-			if (!PQgetisnull(res, rownum, i_range_length_histogram))
-				appendNamedArgument(&out, fout, "range_length_histogram", "text",
-									PQgetvalue(res, rownum, i_range_length_histogram));
-			if (!PQgetisnull(res, rownum, i_range_empty_frac))
-				appendNamedArgument(&out, fout, "range_empty_frac", "real",
-									PQgetvalue(res, rownum, i_range_empty_frac));
-			if (!PQgetisnull(res, rownum, i_range_bounds_histogram))
-				appendNamedArgument(&out, fout, "range_bounds_histogram", "text",
-									PQgetvalue(res, rownum, i_range_bounds_histogram));
-		}
-		appendPQExpBufferStr(&out, "\n);\n");
+		i_schemaname = PQfnumber(attrstats.res, "schemaname");
+		Assert(i_schemaname >= 0);
+		i_tablename = PQfnumber(attrstats.res, "tablename");
+		Assert(i_tablename >= 0);
+
+		if (PQgetisnull(attrstats.res, attrstats.idx, i_schemaname))
+			pg_fatal("getAttributeStats() schemaname cannot be NULL");
+
+		if (PQgetisnull(attrstats.res, attrstats.idx, i_tablename))
+			pg_fatal("getAttributeStats() tablename cannot be NULL");
+
+		schemaname = PQgetvalue(attrstats.res, attrstats.idx, i_schemaname);
+		tablename = PQgetvalue(attrstats.res, attrstats.idx, i_tablename);
+
+		/* stop if current stat row isn't for this relation */
+		if (strcmp(relname, tablename) != 0 || strcmp(relschema, schemaname) != 0)
+			break;
+
+		appendAttributeStats(fout, &out, rsinfo);
+		AH->txnCount++;
+		attrstats.idx++;
 	}
 
-	PQclear(res);
-
-	termPQExpBuffer(&query);
 	return out.data;
 }
 
-- 
2.49.0

v11-0002-Introduce-CreateStmtPtr.patchtext/x-patch; charset=US-ASCII; name=v11-0002-Introduce-CreateStmtPtr.patchDownload
From c71426ee2068d37237b79a65d1a0764b7cd3d60f Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Fri, 14 Mar 2025 01:06:19 -0400
Subject: [PATCH v11 2/4] Introduce CreateStmtPtr.

CreateStmtPtr is a function pointer that can replace the createStmt/defn
parameter. This is useful in situations where the amount of text
generated for a definition is so large that it is undesirable to hold
many such objects in memory at the same time.

Using functions of this type, the text created is then immediately
written out to the appropriate file for the given dump format.
---
 src/bin/pg_dump/pg_backup.h          |   2 +
 src/bin/pg_dump/pg_backup_archiver.c |  22 ++-
 src/bin/pg_dump/pg_backup_archiver.h |   7 +
 src/bin/pg_dump/pg_dump.c            | 229 +++++++++++++++------------
 4 files changed, 158 insertions(+), 102 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 658986de6f8..fdcccd64a70 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -289,6 +289,8 @@ typedef int (*DataDumperPtr) (Archive *AH, const void *userArg);
 
 typedef void (*SetupWorkerPtrType) (Archive *AH);
 
+typedef char *(*CreateStmtPtr) (Archive *AH, const void *userArg);
+
 /*
  * Main archiver interface.
  */
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 1d131e5a57d..1b4c62fd7d7 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -1265,6 +1265,9 @@ ArchiveEntry(Archive *AHX, CatalogId catalogId, DumpId dumpId,
 	newToc->dataDumper = opts->dumpFn;
 	newToc->dataDumperArg = opts->dumpArg;
 	newToc->hadDumper = opts->dumpFn ? true : false;
+	newToc->createDumper = opts->createFn;
+	newToc->createDumperArg = opts->createArg;
+	newToc->hadCreateDumper = opts->createFn ? true : false;
 
 	newToc->formatData = NULL;
 	newToc->dataLength = 0;
@@ -2621,7 +2624,17 @@ WriteToc(ArchiveHandle *AH)
 		WriteStr(AH, te->tag);
 		WriteStr(AH, te->desc);
 		WriteInt(AH, te->section);
-		WriteStr(AH, te->defn);
+
+		if (te->hadCreateDumper)
+		{
+			char	   *defn = te->createDumper((Archive *) AH, te->createDumperArg);
+
+			WriteStr(AH, defn);
+			pg_free(defn);
+		}
+		else
+			WriteStr(AH, te->defn);
+
 		WriteStr(AH, te->dropStmt);
 		WriteStr(AH, te->copyStmt);
 		WriteStr(AH, te->namespace);
@@ -3877,6 +3890,13 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 	{
 		IssueACLPerBlob(AH, te);
 	}
+	else if (te->hadCreateDumper)
+	{
+		char	   *ptr = te->createDumper((Archive *) AH, te->createDumperArg);
+
+		ahwrite(ptr, 1, strlen(ptr), AH);
+		pg_free(ptr);
+	}
 	else if (te->defn && strlen(te->defn) > 0)
 	{
 		ahprintf(AH, "%s\n\n", te->defn);
diff --git a/src/bin/pg_dump/pg_backup_archiver.h b/src/bin/pg_dump/pg_backup_archiver.h
index a2064f471ed..e68db633995 100644
--- a/src/bin/pg_dump/pg_backup_archiver.h
+++ b/src/bin/pg_dump/pg_backup_archiver.h
@@ -368,6 +368,11 @@ struct _tocEntry
 	const void *dataDumperArg;	/* Arg for above routine */
 	void	   *formatData;		/* TOC Entry data specific to file format */
 
+	CreateStmtPtr createDumper; /* Routine for create statement creation */
+	const void *createDumperArg;	/* arg for the above routine */
+	bool		hadCreateDumper;	/* Archiver was passed a create statement
+									 * routine */
+
 	/* working state while dumping/restoring */
 	pgoff_t		dataLength;		/* item's data size; 0 if none or unknown */
 	int			reqs;			/* do we need schema and/or data of object
@@ -407,6 +412,8 @@ typedef struct _archiveOpts
 	int			nDeps;
 	DataDumperPtr dumpFn;
 	const void *dumpArg;
+	CreateStmtPtr createFn;
+	const void *createArg;
 } ArchiveOpts;
 #define ARCHIVE_OPTS(...) &(ArchiveOpts){__VA_ARGS__}
 /* Called to add a TOC entry */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 211cf10dbd6..b7571ea15eb 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -10556,42 +10556,44 @@ appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
 }
 
 /*
- * dumpRelationStats --
+ * printDumpRelationStats --
  *
- * Dump command to import stats into the relation on the new database.
+ * Generate the SQL statements needed to restore a relation's statistics.
  */
-static void
-dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+static char *
+printRelationStats(Archive *fout, const void *userArg)
 {
+	const RelStatsInfo *rsinfo = (RelStatsInfo *) userArg;
 	const DumpableObject *dobj = &rsinfo->dobj;
+
+	PQExpBufferData query;
+	PQExpBufferData out;
+
 	PGresult   *res;
-	PQExpBuffer query;
-	PQExpBuffer out;
-	int			i_attname;
-	int			i_inherited;
-	int			i_null_frac;
-	int			i_avg_width;
-	int			i_n_distinct;
-	int			i_most_common_vals;
-	int			i_most_common_freqs;
-	int			i_histogram_bounds;
-	int			i_correlation;
-	int			i_most_common_elems;
-	int			i_most_common_elem_freqs;
-	int			i_elem_count_histogram;
-	int			i_range_length_histogram;
-	int			i_range_empty_frac;
-	int			i_range_bounds_histogram;
 
-	/* nothing to do if we are not dumping statistics */
-	if (!fout->dopt->dumpStatistics)
-		return;
+	static bool first_query = true;
+	static int	i_attname;
+	static int	i_inherited;
+	static int	i_null_frac;
+	static int	i_avg_width;
+	static int	i_n_distinct;
+	static int	i_most_common_vals;
+	static int	i_most_common_freqs;
+	static int	i_histogram_bounds;
+	static int	i_correlation;
+	static int	i_most_common_elems;
+	static int	i_most_common_elem_freqs;
+	static int	i_elem_count_histogram;
+	static int	i_range_length_histogram;
+	static int	i_range_empty_frac;
+	static int	i_range_bounds_histogram;
 
-	query = createPQExpBuffer();
-	if (!fout->is_prepared[PREPQUERY_GETATTRIBUTESTATS])
+	initPQExpBuffer(&query);
+
+	if (first_query)
 	{
-		appendPQExpBufferStr(query,
-							 "PREPARE getAttributeStats(pg_catalog.name, pg_catalog.name) AS\n"
+		appendPQExpBufferStr(&query,
+							 "PREPARE getAttributeStats(pg_catalog.text, pg_catalog.text) AS\n"
 							 "SELECT s.attname, s.inherited, "
 							 "s.null_frac, s.avg_width, s.n_distinct, "
 							 "s.most_common_vals, s.most_common_freqs, "
@@ -10600,88 +10602,87 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 							 "s.elem_count_histogram, ");
 
 		if (fout->remoteVersion >= 170000)
-			appendPQExpBufferStr(query,
+			appendPQExpBufferStr(&query,
 								 "s.range_length_histogram, "
 								 "s.range_empty_frac, "
 								 "s.range_bounds_histogram ");
 		else
-			appendPQExpBufferStr(query,
+			appendPQExpBufferStr(&query,
 								 "NULL AS range_length_histogram,"
 								 "NULL AS range_empty_frac,"
 								 "NULL AS range_bounds_histogram ");
 
-		appendPQExpBufferStr(query,
+		appendPQExpBufferStr(&query,
 							 "FROM pg_catalog.pg_stats s "
 							 "WHERE s.schemaname = $1 "
 							 "AND s.tablename = $2 "
 							 "ORDER BY s.attname, s.inherited");
 
-		ExecuteSqlStatement(fout, query->data);
+		ExecuteSqlStatement(fout, query.data);
 
-		fout->is_prepared[PREPQUERY_GETATTRIBUTESTATS] = true;
-		resetPQExpBuffer(query);
+		resetPQExpBuffer(&query);
 	}
 
-	out = createPQExpBuffer();
+	initPQExpBuffer(&out);
 
 	/* restore relation stats */
-	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
-	appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
-					  fout->remoteVersion);
-	appendPQExpBufferStr(out, "\t'schemaname', ");
-	appendStringLiteralAH(out, rsinfo->dobj.namespace->dobj.name, fout);
-	appendPQExpBufferStr(out, ",\n");
-	appendPQExpBufferStr(out, "\t'relname', ");
-	appendStringLiteralAH(out, rsinfo->dobj.name, fout);
-	appendPQExpBufferStr(out, ",\n");
-	appendPQExpBuffer(out, "\t'relpages', '%d'::integer,\n", rsinfo->relpages);
-	appendPQExpBuffer(out, "\t'reltuples', '%s'::real,\n", rsinfo->reltuples);
-	appendPQExpBuffer(out, "\t'relallvisible', '%d'::integer",
-					  rsinfo->relallvisible);
+	appendPQExpBufferStr(&out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(");
+	appendPQExpBuffer(&out, "\n\t'version', '%u'::integer", fout->remoteVersion);
+	appendPQExpBufferStr(&out, ",\n\t'schemaname', ");
+	appendStringLiteralAH(&out, rsinfo->dobj.namespace->dobj.name, fout);
+	appendPQExpBufferStr(&out, ",\n\t'relname', ");
+	appendStringLiteralAH(&out, rsinfo->dobj.name, fout);
+	appendPQExpBuffer(&out, ",\n\t'relpages', '%d'::integer", rsinfo->relpages);
+	appendPQExpBuffer(&out, ",\n\t'reltuples', '%s'::real", rsinfo->reltuples);
+	appendPQExpBuffer(&out, ",\n\t'relallvisible', '%d'::integer", rsinfo->relallvisible);
 
 	if (fout->remoteVersion >= 180000)
-		appendPQExpBuffer(out, ",\n\t'relallfrozen', '%d'::integer", rsinfo->relallfrozen);
+		appendPQExpBuffer(&out, ",\n\t'relallfrozen', '%d'::integer", rsinfo->relallfrozen);
 
-	appendPQExpBufferStr(out, "\n);\n");
+	appendPQExpBufferStr(&out, "\n);\n");
 
 
 	/* fetch attribute stats */
-	appendPQExpBufferStr(query, "EXECUTE getAttributeStats(");
-	appendStringLiteralAH(query, dobj->namespace->dobj.name, fout);
-	appendPQExpBufferStr(query, ", ");
-	appendStringLiteralAH(query, dobj->name, fout);
-	appendPQExpBufferStr(query, ");");
+	appendPQExpBufferStr(&query, "EXECUTE getAttributeStats(");
+	appendStringLiteralAH(&query, dobj->namespace->dobj.name, fout);
+	appendPQExpBufferStr(&query, ", ");
+	appendStringLiteralAH(&query, dobj->name, fout);
+	appendPQExpBufferStr(&query, ")");
 
-	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	res = ExecuteSqlQuery(fout, query.data, PGRES_TUPLES_OK);
 
-	i_attname = PQfnumber(res, "attname");
-	i_inherited = PQfnumber(res, "inherited");
-	i_null_frac = PQfnumber(res, "null_frac");
-	i_avg_width = PQfnumber(res, "avg_width");
-	i_n_distinct = PQfnumber(res, "n_distinct");
-	i_most_common_vals = PQfnumber(res, "most_common_vals");
-	i_most_common_freqs = PQfnumber(res, "most_common_freqs");
-	i_histogram_bounds = PQfnumber(res, "histogram_bounds");
-	i_correlation = PQfnumber(res, "correlation");
-	i_most_common_elems = PQfnumber(res, "most_common_elems");
-	i_most_common_elem_freqs = PQfnumber(res, "most_common_elem_freqs");
-	i_elem_count_histogram = PQfnumber(res, "elem_count_histogram");
-	i_range_length_histogram = PQfnumber(res, "range_length_histogram");
-	i_range_empty_frac = PQfnumber(res, "range_empty_frac");
-	i_range_bounds_histogram = PQfnumber(res, "range_bounds_histogram");
+	if (first_query)
+	{
+		i_attname = PQfnumber(res, "attname");
+		i_inherited = PQfnumber(res, "inherited");
+		i_null_frac = PQfnumber(res, "null_frac");
+		i_avg_width = PQfnumber(res, "avg_width");
+		i_n_distinct = PQfnumber(res, "n_distinct");
+		i_most_common_vals = PQfnumber(res, "most_common_vals");
+		i_most_common_freqs = PQfnumber(res, "most_common_freqs");
+		i_histogram_bounds = PQfnumber(res, "histogram_bounds");
+		i_correlation = PQfnumber(res, "correlation");
+		i_most_common_elems = PQfnumber(res, "most_common_elems");
+		i_most_common_elem_freqs = PQfnumber(res, "most_common_elem_freqs");
+		i_elem_count_histogram = PQfnumber(res, "elem_count_histogram");
+		i_range_length_histogram = PQfnumber(res, "range_length_histogram");
+		i_range_empty_frac = PQfnumber(res, "range_empty_frac");
+		i_range_bounds_histogram = PQfnumber(res, "range_bounds_histogram");
+		first_query = false;
+	}
 
 	/* restore attribute stats */
 	for (int rownum = 0; rownum < PQntuples(res); rownum++)
 	{
 		const char *attname;
 
-		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
-		appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
+		appendPQExpBufferStr(&out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+		appendPQExpBuffer(&out, "\t'version', '%u'::integer,\n",
 						  fout->remoteVersion);
-		appendPQExpBufferStr(out, "\t'schemaname', ");
-		appendStringLiteralAH(out, rsinfo->dobj.namespace->dobj.name, fout);
-		appendPQExpBufferStr(out, ",\n\t'relname', ");
-		appendStringLiteralAH(out, rsinfo->dobj.name, fout);
+		appendPQExpBufferStr(&out, "\t'schemaname', ");
+		appendStringLiteralAH(&out, rsinfo->dobj.namespace->dobj.name, fout);
+		appendPQExpBufferStr(&out, ",\n\t'relname', ");
+		appendStringLiteralAH(&out, rsinfo->dobj.name, fout);
 
 		if (PQgetisnull(res, rownum, i_attname))
 			pg_fatal("attname cannot be NULL");
@@ -10694,8 +10695,8 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 		 */
 		if (rsinfo->nindAttNames == 0)
 		{
-			appendPQExpBuffer(out, ",\n\t'attname', ");
-			appendStringLiteralAH(out, attname, fout);
+			appendPQExpBuffer(&out, ",\n\t'attname', ");
+			appendStringLiteralAH(&out, attname, fout);
 		}
 		else
 		{
@@ -10705,7 +10706,7 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 			{
 				if (strcmp(attname, rsinfo->indAttNames[i]) == 0)
 				{
-					appendPQExpBuffer(out, ",\n\t'attnum', '%d'::smallint",
+					appendPQExpBuffer(&out, ",\n\t'attnum', '%d'::smallint",
 									  i + 1);
 					found = true;
 					break;
@@ -10717,66 +10718,92 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 		}
 
 		if (!PQgetisnull(res, rownum, i_inherited))
-			appendNamedArgument(out, fout, "inherited", "boolean",
+			appendNamedArgument(&out, fout, "inherited", "boolean",
 								PQgetvalue(res, rownum, i_inherited));
 		if (!PQgetisnull(res, rownum, i_null_frac))
-			appendNamedArgument(out, fout, "null_frac", "real",
+			appendNamedArgument(&out, fout, "null_frac", "real",
 								PQgetvalue(res, rownum, i_null_frac));
 		if (!PQgetisnull(res, rownum, i_avg_width))
-			appendNamedArgument(out, fout, "avg_width", "integer",
+			appendNamedArgument(&out, fout, "avg_width", "integer",
 								PQgetvalue(res, rownum, i_avg_width));
 		if (!PQgetisnull(res, rownum, i_n_distinct))
-			appendNamedArgument(out, fout, "n_distinct", "real",
+			appendNamedArgument(&out, fout, "n_distinct", "real",
 								PQgetvalue(res, rownum, i_n_distinct));
 		if (!PQgetisnull(res, rownum, i_most_common_vals))
-			appendNamedArgument(out, fout, "most_common_vals", "text",
+			appendNamedArgument(&out, fout, "most_common_vals", "text",
 								PQgetvalue(res, rownum, i_most_common_vals));
 		if (!PQgetisnull(res, rownum, i_most_common_freqs))
-			appendNamedArgument(out, fout, "most_common_freqs", "real[]",
+			appendNamedArgument(&out, fout, "most_common_freqs", "real[]",
 								PQgetvalue(res, rownum, i_most_common_freqs));
 		if (!PQgetisnull(res, rownum, i_histogram_bounds))
-			appendNamedArgument(out, fout, "histogram_bounds", "text",
+			appendNamedArgument(&out, fout, "histogram_bounds", "text",
 								PQgetvalue(res, rownum, i_histogram_bounds));
 		if (!PQgetisnull(res, rownum, i_correlation))
-			appendNamedArgument(out, fout, "correlation", "real",
+			appendNamedArgument(&out, fout, "correlation", "real",
 								PQgetvalue(res, rownum, i_correlation));
 		if (!PQgetisnull(res, rownum, i_most_common_elems))
-			appendNamedArgument(out, fout, "most_common_elems", "text",
+			appendNamedArgument(&out, fout, "most_common_elems", "text",
 								PQgetvalue(res, rownum, i_most_common_elems));
 		if (!PQgetisnull(res, rownum, i_most_common_elem_freqs))
-			appendNamedArgument(out, fout, "most_common_elem_freqs", "real[]",
+			appendNamedArgument(&out, fout, "most_common_elem_freqs", "real[]",
 								PQgetvalue(res, rownum, i_most_common_elem_freqs));
 		if (!PQgetisnull(res, rownum, i_elem_count_histogram))
-			appendNamedArgument(out, fout, "elem_count_histogram", "real[]",
+			appendNamedArgument(&out, fout, "elem_count_histogram", "real[]",
 								PQgetvalue(res, rownum, i_elem_count_histogram));
 		if (fout->remoteVersion >= 170000)
 		{
 			if (!PQgetisnull(res, rownum, i_range_length_histogram))
-				appendNamedArgument(out, fout, "range_length_histogram", "text",
+				appendNamedArgument(&out, fout, "range_length_histogram", "text",
 									PQgetvalue(res, rownum, i_range_length_histogram));
 			if (!PQgetisnull(res, rownum, i_range_empty_frac))
-				appendNamedArgument(out, fout, "range_empty_frac", "real",
+				appendNamedArgument(&out, fout, "range_empty_frac", "real",
 									PQgetvalue(res, rownum, i_range_empty_frac));
 			if (!PQgetisnull(res, rownum, i_range_bounds_histogram))
-				appendNamedArgument(out, fout, "range_bounds_histogram", "text",
+				appendNamedArgument(&out, fout, "range_bounds_histogram", "text",
 									PQgetvalue(res, rownum, i_range_bounds_histogram));
 		}
-		appendPQExpBufferStr(out, "\n);\n");
+		appendPQExpBufferStr(&out, "\n);\n");
 	}
 
 	PQclear(res);
 
+	termPQExpBuffer(&query);
+	return out.data;
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	const DumpableObject *dobj = &rsinfo->dobj;
+
+	DumpId	   *deps = NULL;
+	int			ndeps = 0;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
+	/* dependent on the relation definition, if doing schema */
+	if (fout->dopt->dumpSchema)
+	{
+		deps = dobj->dependencies;
+		ndeps = dobj->nDeps;
+	}
+
 	ArchiveEntry(fout, nilCatalogId, createDumpId(),
 				 ARCHIVE_OPTS(.tag = dobj->name,
 							  .namespace = dobj->namespace->dobj.name,
 							  .description = "STATISTICS DATA",
 							  .section = rsinfo->section,
-							  .createStmt = out->data,
-							  .deps = dobj->dependencies,
-							  .nDeps = dobj->nDeps));
-
-	destroyPQExpBuffer(out);
-	destroyPQExpBuffer(query);
+							  .createFn = printRelationStats,
+							  .createArg = rsinfo,
+							  .deps = deps,
+							  .nDeps = ndeps));
 }
 
 /*
-- 
2.49.0

#501Jeff Davis
pgsql@j-davis.com
In reply to: Corey Huinker (#500)
1 attachment(s)
Re: Statistics Import and Export

On Fri, 2025-03-28 at 21:11 -0400, Corey Huinker wrote:

A rebase and a reordering of the commits to put the really-really-
must-have relallfrozen ahead of the really-must-have stats batching
and both of them head of the error->warning step-downs.

v11-0001 has a couple issues:

The first is that i_relallfrozen is undefined in versions earlier than
18. That's trivial to fix, we just add "0 AS relallfrozen," in the
earlier versions, but still refrain from outputting it.

The second is that the pg_upgrade test (when run with
olddump/oldinstall) compares the before and after dumps, and if the
"before" version is 17, then it will not have the relallfrozen argument
to pg_restore_relation_stats. We might need a filtering step in
adjust_new_dumpfile?

Attached new v11j-0001

Regards,
Jeff Davis

Attachments:

v11j-0001-Add-relallfrozen-to-pg_dump-statistics.patchtext/x-patch; charset=UTF-8; name=v11j-0001-Add-relallfrozen-to-pg_dump-statistics.patchDownload
From 154b8b5c10ec330c26ccd9006c434a7db1feef04 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 15 Mar 2025 17:34:30 -0400
Subject: [PATCH v11j] Add relallfrozen to pg_dump statistics.

Author: Corey Huinker <corey.huinker@gmail.com>
Discussion: https://postgr.es/m/CADkLM=desCuf3dVHasADvdUVRmb-5gO0mhMO5u9nzgv6i7U86Q@mail.gmail.com
---
 src/bin/pg_dump/pg_dump.c                     | 42 +++++++++++++++----
 src/bin/pg_dump/pg_dump.h                     |  1 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  3 +-
 .../perl/PostgreSQL/Test/AdjustUpgrade.pm     |  5 +++
 4 files changed, 43 insertions(+), 8 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 84a78625820..4ca34be230c 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -6874,7 +6874,8 @@ getFuncs(Archive *fout)
  */
 static RelStatsInfo *
 getRelationStatistics(Archive *fout, DumpableObject *rel, int32 relpages,
-					  char *reltuples, int32 relallvisible, char relkind,
+					  char *reltuples, int32 relallvisible,
+					  int32 relallfrozen, char relkind,
 					  char **indAttNames, int nindAttNames)
 {
 	if (!fout->dopt->dumpStatistics)
@@ -6903,6 +6904,7 @@ getRelationStatistics(Archive *fout, DumpableObject *rel, int32 relpages,
 		info->relpages = relpages;
 		info->reltuples = pstrdup(reltuples);
 		info->relallvisible = relallvisible;
+		info->relallfrozen = relallfrozen;
 		info->relkind = relkind;
 		info->indAttNames = indAttNames;
 		info->nindAttNames = nindAttNames;
@@ -6967,6 +6969,7 @@ getTables(Archive *fout, int *numTables)
 	int			i_relpages;
 	int			i_reltuples;
 	int			i_relallvisible;
+	int			i_relallfrozen;
 	int			i_toastpages;
 	int			i_owning_tab;
 	int			i_owning_col;
@@ -7017,8 +7020,15 @@ getTables(Archive *fout, int *numTables)
 						 "c.relowner, "
 						 "c.relchecks, "
 						 "c.relhasindex, c.relhasrules, c.relpages, "
-						 "c.reltuples, c.relallvisible, c.relhastriggers, "
-						 "c.relpersistence, "
+						 "c.reltuples, c.relallvisible, ");
+
+	if (fout->remoteVersion >= 180000)
+		appendPQExpBufferStr(query, "c.relallfrozen, ");
+	else
+		appendPQExpBufferStr(query, "0 AS relallfrozen, ");
+
+	appendPQExpBufferStr(query,
+						 "c.relhastriggers, c.relpersistence, "
 						 "c.reloftype, "
 						 "c.relacl, "
 						 "acldefault(CASE WHEN c.relkind = " CppAsString2(RELKIND_SEQUENCE)
@@ -7183,6 +7193,7 @@ getTables(Archive *fout, int *numTables)
 	i_relpages = PQfnumber(res, "relpages");
 	i_reltuples = PQfnumber(res, "reltuples");
 	i_relallvisible = PQfnumber(res, "relallvisible");
+	i_relallfrozen = PQfnumber(res, "relallfrozen");
 	i_toastpages = PQfnumber(res, "toastpages");
 	i_owning_tab = PQfnumber(res, "owning_tab");
 	i_owning_col = PQfnumber(res, "owning_col");
@@ -7230,6 +7241,7 @@ getTables(Archive *fout, int *numTables)
 	for (i = 0; i < ntups; i++)
 	{
 		int32		relallvisible = atoi(PQgetvalue(res, i, i_relallvisible));
+		int32		relallfrozen = atoi(PQgetvalue(res, i, i_relallfrozen));
 
 		tblinfo[i].dobj.objType = DO_TABLE;
 		tblinfo[i].dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_reltableoid));
@@ -7336,7 +7348,7 @@ getTables(Archive *fout, int *numTables)
 			stats = getRelationStatistics(fout, &tblinfo[i].dobj,
 										  tblinfo[i].relpages,
 										  PQgetvalue(res, i, i_reltuples),
-										  relallvisible,
+										  relallvisible, relallfrozen,
 										  tblinfo[i].relkind, NULL, 0);
 			if (tblinfo[i].relkind == RELKIND_MATVIEW)
 				tblinfo[i].stats = stats;
@@ -7609,6 +7621,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				i_relpages,
 				i_reltuples,
 				i_relallvisible,
+				i_relallfrozen,
 				i_parentidx,
 				i_indexdef,
 				i_indnkeyatts,
@@ -7663,7 +7676,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 	appendPQExpBufferStr(query,
 						 "SELECT t.tableoid, t.oid, i.indrelid, "
 						 "t.relname AS indexname, "
-						 "t.relpages, t.reltuples, t.relallvisible, "
+						 "t.relpages, t.reltuples, t.relallvisible, ");
+
+	if (fout->remoteVersion >= 180000)
+		appendPQExpBufferStr(query, "t.relallfrozen, ");
+	else
+		appendPQExpBufferStr(query, "0 AS relallfrozen, ");
+
+	appendPQExpBufferStr(query,
 						 "pg_catalog.pg_get_indexdef(i.indexrelid) AS indexdef, "
 						 "i.indkey, i.indisclustered, "
 						 "c.contype, c.conname, "
@@ -7779,6 +7799,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_relpages = PQfnumber(res, "relpages");
 	i_reltuples = PQfnumber(res, "reltuples");
 	i_relallvisible = PQfnumber(res, "relallvisible");
+	i_relallfrozen = PQfnumber(res, "relallfrozen");
 	i_parentidx = PQfnumber(res, "parentidx");
 	i_indexdef = PQfnumber(res, "indexdef");
 	i_indnkeyatts = PQfnumber(res, "indnkeyatts");
@@ -7850,6 +7871,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			RelStatsInfo *relstats;
 			int32		relpages = atoi(PQgetvalue(res, j, i_relpages));
 			int32		relallvisible = atoi(PQgetvalue(res, j, i_relallvisible));
+			int32		relallfrozen = atoi(PQgetvalue(res, j, i_relallfrozen));
 
 			indxinfo[j].dobj.objType = DO_INDEX;
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
@@ -7892,7 +7914,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 
 			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, relpages,
 											 PQgetvalue(res, j, i_reltuples),
-											 relallvisible, indexkind,
+											 relallvisible, relallfrozen, indexkind,
 											 indAttNames, nindAttNames);
 
 			contype = *(PQgetvalue(res, j, i_contype));
@@ -10618,9 +10640,15 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 	appendPQExpBufferStr(out, ",\n");
 	appendPQExpBuffer(out, "\t'relpages', '%d'::integer,\n", rsinfo->relpages);
 	appendPQExpBuffer(out, "\t'reltuples', '%s'::real,\n", rsinfo->reltuples);
-	appendPQExpBuffer(out, "\t'relallvisible', '%d'::integer\n);\n",
+	appendPQExpBuffer(out, "\t'relallvisible', '%d'::integer",
 					  rsinfo->relallvisible);
 
+	if (fout->remoteVersion >= 180000)
+		appendPQExpBuffer(out, ",\n\t'relallfrozen', '%d'::integer", rsinfo->relallfrozen);
+
+	appendPQExpBufferStr(out, "\n);\n");
+
+
 	/* fetch attribute stats */
 	appendPQExpBufferStr(query, "EXECUTE getAttributeStats(");
 	appendStringLiteralAH(query, dobj->namespace->dobj.name, fout);
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 70f7a369e4a..e6f0f86a459 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -442,6 +442,7 @@ typedef struct _relStatsInfo
 	int32		relpages;
 	char	   *reltuples;
 	int32		relallvisible;
+	int32		relallfrozen;
 	char		relkind;		/* 'r', 'm', 'i', etc */
 
 	/*
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 51ebf8ad13c..576326daec7 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -4771,7 +4771,8 @@ my %tests = (
 			'relname',\s'dup_test_post_data_ix',\s+
 			'relpages',\s'\d+'::integer,\s+
 			'reltuples',\s'\d+'::real,\s+
-			'relallvisible',\s'\d+'::integer\s+
+			'relallvisible',\s'\d+'::integer,\s+
+			'relallfrozen',\s'\d+'::integer\s+
 			\);\s+
 			\QSELECT * FROM pg_catalog.pg_restore_attribute_stats(\E\s+
 			'version',\s'\d+'::integer,\s+
diff --git a/src/test/perl/PostgreSQL/Test/AdjustUpgrade.pm b/src/test/perl/PostgreSQL/Test/AdjustUpgrade.pm
index 81a8f44aa9f..07550295a82 100644
--- a/src/test/perl/PostgreSQL/Test/AdjustUpgrade.pm
+++ b/src/test/perl/PostgreSQL/Test/AdjustUpgrade.pm
@@ -648,6 +648,11 @@ sub adjust_new_dumpfile
 	$dump =~ s {\n(\s+'version',) '\d+'::integer,$}
 		{$1 '000000'::integer,}mg;
 
+	if ($old_version < 18)
+	{
+		$dump =~ s {,\n(\s+'relallfrozen',) '\d+'::integer$}{}mg;
+	}
+
 	# pre-v16 dumps do not know about XMLSERIALIZE(NO INDENT).
 	if ($old_version < 16)
 	{
-- 
2.34.1

#502Corey Huinker
corey.huinker@gmail.com
In reply to: Jeff Davis (#501)
Re: Statistics Import and Export

The first is that i_relallfrozen is undefined in versions earlier than
18. That's trivial to fix, we just add "0 AS relallfrozen," in the
earlier versions, but still refrain from outputting it.

Ok, so long as we refrain from outputting it, I'm cool with whatever we
store internally.

The second is that the pg_upgrade test (when run with
olddump/oldinstall) compares the before and after dumps, and if the
"before" version is 17, then it will not have the relallfrozen argument
to pg_restore_relation_stats. We might need a filtering step in
adjust_new_dumpfile?

That sounds trickier. Do we already have filtering steps that are sensitive
to the "before" version dump?

#503Corey Huinker
corey.huinker@gmail.com
In reply to: Corey Huinker (#502)
3 attachment(s)
Re: Statistics Import and Export

The second is that the pg_upgrade test (when run with

olddump/oldinstall) compares the before and after dumps, and if the
"before" version is 17, then it will not have the relallfrozen argument
to pg_restore_relation_stats. We might need a filtering step in
adjust_new_dumpfile?

That sounds trickier.

Narrator: It was not trickier.

In light of v11-0001 being committed as 4694aedf63bf, I've rebased the
remaining patches.

Attachments:

v12-0001-Introduce-CreateStmtPtr.patchtext/x-patch; charset=US-ASCII; name=v12-0001-Introduce-CreateStmtPtr.patchDownload
From 607984bdcc91fa31fb7a12e9b24fb8704aa14975 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Fri, 14 Mar 2025 01:06:19 -0400
Subject: [PATCH v12 1/3] Introduce CreateStmtPtr.

CreateStmtPtr is a function pointer that can replace the createStmt/defn
parameter. This is useful in situations where the amount of text
generated for a definition is so large that it is undesirable to hold
many such objects in memory at the same time.

Using functions of this type, the text created is then immediately
written out to the appropriate file for the given dump format.
---
 src/bin/pg_dump/pg_backup.h          |   2 +
 src/bin/pg_dump/pg_backup_archiver.c |  22 ++-
 src/bin/pg_dump/pg_backup_archiver.h |   7 +
 src/bin/pg_dump/pg_dump.c            | 229 +++++++++++++++------------
 4 files changed, 158 insertions(+), 102 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 658986de6f8..fdcccd64a70 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -289,6 +289,8 @@ typedef int (*DataDumperPtr) (Archive *AH, const void *userArg);
 
 typedef void (*SetupWorkerPtrType) (Archive *AH);
 
+typedef char *(*CreateStmtPtr) (Archive *AH, const void *userArg);
+
 /*
  * Main archiver interface.
  */
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 1d131e5a57d..1b4c62fd7d7 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -1265,6 +1265,9 @@ ArchiveEntry(Archive *AHX, CatalogId catalogId, DumpId dumpId,
 	newToc->dataDumper = opts->dumpFn;
 	newToc->dataDumperArg = opts->dumpArg;
 	newToc->hadDumper = opts->dumpFn ? true : false;
+	newToc->createDumper = opts->createFn;
+	newToc->createDumperArg = opts->createArg;
+	newToc->hadCreateDumper = opts->createFn ? true : false;
 
 	newToc->formatData = NULL;
 	newToc->dataLength = 0;
@@ -2621,7 +2624,17 @@ WriteToc(ArchiveHandle *AH)
 		WriteStr(AH, te->tag);
 		WriteStr(AH, te->desc);
 		WriteInt(AH, te->section);
-		WriteStr(AH, te->defn);
+
+		if (te->hadCreateDumper)
+		{
+			char	   *defn = te->createDumper((Archive *) AH, te->createDumperArg);
+
+			WriteStr(AH, defn);
+			pg_free(defn);
+		}
+		else
+			WriteStr(AH, te->defn);
+
 		WriteStr(AH, te->dropStmt);
 		WriteStr(AH, te->copyStmt);
 		WriteStr(AH, te->namespace);
@@ -3877,6 +3890,13 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 	{
 		IssueACLPerBlob(AH, te);
 	}
+	else if (te->hadCreateDumper)
+	{
+		char	   *ptr = te->createDumper((Archive *) AH, te->createDumperArg);
+
+		ahwrite(ptr, 1, strlen(ptr), AH);
+		pg_free(ptr);
+	}
 	else if (te->defn && strlen(te->defn) > 0)
 	{
 		ahprintf(AH, "%s\n\n", te->defn);
diff --git a/src/bin/pg_dump/pg_backup_archiver.h b/src/bin/pg_dump/pg_backup_archiver.h
index a2064f471ed..e68db633995 100644
--- a/src/bin/pg_dump/pg_backup_archiver.h
+++ b/src/bin/pg_dump/pg_backup_archiver.h
@@ -368,6 +368,11 @@ struct _tocEntry
 	const void *dataDumperArg;	/* Arg for above routine */
 	void	   *formatData;		/* TOC Entry data specific to file format */
 
+	CreateStmtPtr createDumper; /* Routine for create statement creation */
+	const void *createDumperArg;	/* arg for the above routine */
+	bool		hadCreateDumper;	/* Archiver was passed a create statement
+									 * routine */
+
 	/* working state while dumping/restoring */
 	pgoff_t		dataLength;		/* item's data size; 0 if none or unknown */
 	int			reqs;			/* do we need schema and/or data of object
@@ -407,6 +412,8 @@ typedef struct _archiveOpts
 	int			nDeps;
 	DataDumperPtr dumpFn;
 	const void *dumpArg;
+	CreateStmtPtr createFn;
+	const void *createArg;
 } ArchiveOpts;
 #define ARCHIVE_OPTS(...) &(ArchiveOpts){__VA_ARGS__}
 /* Called to add a TOC entry */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 4ca34be230c..cc195d6cd9e 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -10560,42 +10560,44 @@ appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
 }
 
 /*
- * dumpRelationStats --
+ * printDumpRelationStats --
  *
- * Dump command to import stats into the relation on the new database.
+ * Generate the SQL statements needed to restore a relation's statistics.
  */
-static void
-dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+static char *
+printRelationStats(Archive *fout, const void *userArg)
 {
+	const RelStatsInfo *rsinfo = (RelStatsInfo *) userArg;
 	const DumpableObject *dobj = &rsinfo->dobj;
+
+	PQExpBufferData query;
+	PQExpBufferData out;
+
 	PGresult   *res;
-	PQExpBuffer query;
-	PQExpBuffer out;
-	int			i_attname;
-	int			i_inherited;
-	int			i_null_frac;
-	int			i_avg_width;
-	int			i_n_distinct;
-	int			i_most_common_vals;
-	int			i_most_common_freqs;
-	int			i_histogram_bounds;
-	int			i_correlation;
-	int			i_most_common_elems;
-	int			i_most_common_elem_freqs;
-	int			i_elem_count_histogram;
-	int			i_range_length_histogram;
-	int			i_range_empty_frac;
-	int			i_range_bounds_histogram;
 
-	/* nothing to do if we are not dumping statistics */
-	if (!fout->dopt->dumpStatistics)
-		return;
+	static bool first_query = true;
+	static int	i_attname;
+	static int	i_inherited;
+	static int	i_null_frac;
+	static int	i_avg_width;
+	static int	i_n_distinct;
+	static int	i_most_common_vals;
+	static int	i_most_common_freqs;
+	static int	i_histogram_bounds;
+	static int	i_correlation;
+	static int	i_most_common_elems;
+	static int	i_most_common_elem_freqs;
+	static int	i_elem_count_histogram;
+	static int	i_range_length_histogram;
+	static int	i_range_empty_frac;
+	static int	i_range_bounds_histogram;
 
-	query = createPQExpBuffer();
-	if (!fout->is_prepared[PREPQUERY_GETATTRIBUTESTATS])
+	initPQExpBuffer(&query);
+
+	if (first_query)
 	{
-		appendPQExpBufferStr(query,
-							 "PREPARE getAttributeStats(pg_catalog.name, pg_catalog.name) AS\n"
+		appendPQExpBufferStr(&query,
+							 "PREPARE getAttributeStats(pg_catalog.text, pg_catalog.text) AS\n"
 							 "SELECT s.attname, s.inherited, "
 							 "s.null_frac, s.avg_width, s.n_distinct, "
 							 "s.most_common_vals, s.most_common_freqs, "
@@ -10604,88 +10606,87 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 							 "s.elem_count_histogram, ");
 
 		if (fout->remoteVersion >= 170000)
-			appendPQExpBufferStr(query,
+			appendPQExpBufferStr(&query,
 								 "s.range_length_histogram, "
 								 "s.range_empty_frac, "
 								 "s.range_bounds_histogram ");
 		else
-			appendPQExpBufferStr(query,
+			appendPQExpBufferStr(&query,
 								 "NULL AS range_length_histogram,"
 								 "NULL AS range_empty_frac,"
 								 "NULL AS range_bounds_histogram ");
 
-		appendPQExpBufferStr(query,
+		appendPQExpBufferStr(&query,
 							 "FROM pg_catalog.pg_stats s "
 							 "WHERE s.schemaname = $1 "
 							 "AND s.tablename = $2 "
 							 "ORDER BY s.attname, s.inherited");
 
-		ExecuteSqlStatement(fout, query->data);
+		ExecuteSqlStatement(fout, query.data);
 
-		fout->is_prepared[PREPQUERY_GETATTRIBUTESTATS] = true;
-		resetPQExpBuffer(query);
+		resetPQExpBuffer(&query);
 	}
 
-	out = createPQExpBuffer();
+	initPQExpBuffer(&out);
 
 	/* restore relation stats */
-	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
-	appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
-					  fout->remoteVersion);
-	appendPQExpBufferStr(out, "\t'schemaname', ");
-	appendStringLiteralAH(out, rsinfo->dobj.namespace->dobj.name, fout);
-	appendPQExpBufferStr(out, ",\n");
-	appendPQExpBufferStr(out, "\t'relname', ");
-	appendStringLiteralAH(out, rsinfo->dobj.name, fout);
-	appendPQExpBufferStr(out, ",\n");
-	appendPQExpBuffer(out, "\t'relpages', '%d'::integer,\n", rsinfo->relpages);
-	appendPQExpBuffer(out, "\t'reltuples', '%s'::real,\n", rsinfo->reltuples);
-	appendPQExpBuffer(out, "\t'relallvisible', '%d'::integer",
-					  rsinfo->relallvisible);
+	appendPQExpBufferStr(&out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(");
+	appendPQExpBuffer(&out, "\n\t'version', '%u'::integer", fout->remoteVersion);
+	appendPQExpBufferStr(&out, ",\n\t'schemaname', ");
+	appendStringLiteralAH(&out, rsinfo->dobj.namespace->dobj.name, fout);
+	appendPQExpBufferStr(&out, ",\n\t'relname', ");
+	appendStringLiteralAH(&out, rsinfo->dobj.name, fout);
+	appendPQExpBuffer(&out, ",\n\t'relpages', '%d'::integer", rsinfo->relpages);
+	appendPQExpBuffer(&out, ",\n\t'reltuples', '%s'::real", rsinfo->reltuples);
+	appendPQExpBuffer(&out, ",\n\t'relallvisible', '%d'::integer", rsinfo->relallvisible);
 
 	if (fout->remoteVersion >= 180000)
-		appendPQExpBuffer(out, ",\n\t'relallfrozen', '%d'::integer", rsinfo->relallfrozen);
+		appendPQExpBuffer(&out, ",\n\t'relallfrozen', '%d'::integer", rsinfo->relallfrozen);
 
-	appendPQExpBufferStr(out, "\n);\n");
+	appendPQExpBufferStr(&out, "\n);\n");
 
 
 	/* fetch attribute stats */
-	appendPQExpBufferStr(query, "EXECUTE getAttributeStats(");
-	appendStringLiteralAH(query, dobj->namespace->dobj.name, fout);
-	appendPQExpBufferStr(query, ", ");
-	appendStringLiteralAH(query, dobj->name, fout);
-	appendPQExpBufferStr(query, ");");
+	appendPQExpBufferStr(&query, "EXECUTE getAttributeStats(");
+	appendStringLiteralAH(&query, dobj->namespace->dobj.name, fout);
+	appendPQExpBufferStr(&query, ", ");
+	appendStringLiteralAH(&query, dobj->name, fout);
+	appendPQExpBufferStr(&query, ")");
 
-	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	res = ExecuteSqlQuery(fout, query.data, PGRES_TUPLES_OK);
 
-	i_attname = PQfnumber(res, "attname");
-	i_inherited = PQfnumber(res, "inherited");
-	i_null_frac = PQfnumber(res, "null_frac");
-	i_avg_width = PQfnumber(res, "avg_width");
-	i_n_distinct = PQfnumber(res, "n_distinct");
-	i_most_common_vals = PQfnumber(res, "most_common_vals");
-	i_most_common_freqs = PQfnumber(res, "most_common_freqs");
-	i_histogram_bounds = PQfnumber(res, "histogram_bounds");
-	i_correlation = PQfnumber(res, "correlation");
-	i_most_common_elems = PQfnumber(res, "most_common_elems");
-	i_most_common_elem_freqs = PQfnumber(res, "most_common_elem_freqs");
-	i_elem_count_histogram = PQfnumber(res, "elem_count_histogram");
-	i_range_length_histogram = PQfnumber(res, "range_length_histogram");
-	i_range_empty_frac = PQfnumber(res, "range_empty_frac");
-	i_range_bounds_histogram = PQfnumber(res, "range_bounds_histogram");
+	if (first_query)
+	{
+		i_attname = PQfnumber(res, "attname");
+		i_inherited = PQfnumber(res, "inherited");
+		i_null_frac = PQfnumber(res, "null_frac");
+		i_avg_width = PQfnumber(res, "avg_width");
+		i_n_distinct = PQfnumber(res, "n_distinct");
+		i_most_common_vals = PQfnumber(res, "most_common_vals");
+		i_most_common_freqs = PQfnumber(res, "most_common_freqs");
+		i_histogram_bounds = PQfnumber(res, "histogram_bounds");
+		i_correlation = PQfnumber(res, "correlation");
+		i_most_common_elems = PQfnumber(res, "most_common_elems");
+		i_most_common_elem_freqs = PQfnumber(res, "most_common_elem_freqs");
+		i_elem_count_histogram = PQfnumber(res, "elem_count_histogram");
+		i_range_length_histogram = PQfnumber(res, "range_length_histogram");
+		i_range_empty_frac = PQfnumber(res, "range_empty_frac");
+		i_range_bounds_histogram = PQfnumber(res, "range_bounds_histogram");
+		first_query = false;
+	}
 
 	/* restore attribute stats */
 	for (int rownum = 0; rownum < PQntuples(res); rownum++)
 	{
 		const char *attname;
 
-		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
-		appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
+		appendPQExpBufferStr(&out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+		appendPQExpBuffer(&out, "\t'version', '%u'::integer,\n",
 						  fout->remoteVersion);
-		appendPQExpBufferStr(out, "\t'schemaname', ");
-		appendStringLiteralAH(out, rsinfo->dobj.namespace->dobj.name, fout);
-		appendPQExpBufferStr(out, ",\n\t'relname', ");
-		appendStringLiteralAH(out, rsinfo->dobj.name, fout);
+		appendPQExpBufferStr(&out, "\t'schemaname', ");
+		appendStringLiteralAH(&out, rsinfo->dobj.namespace->dobj.name, fout);
+		appendPQExpBufferStr(&out, ",\n\t'relname', ");
+		appendStringLiteralAH(&out, rsinfo->dobj.name, fout);
 
 		if (PQgetisnull(res, rownum, i_attname))
 			pg_fatal("attname cannot be NULL");
@@ -10698,8 +10699,8 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 		 */
 		if (rsinfo->nindAttNames == 0)
 		{
-			appendPQExpBuffer(out, ",\n\t'attname', ");
-			appendStringLiteralAH(out, attname, fout);
+			appendPQExpBuffer(&out, ",\n\t'attname', ");
+			appendStringLiteralAH(&out, attname, fout);
 		}
 		else
 		{
@@ -10709,7 +10710,7 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 			{
 				if (strcmp(attname, rsinfo->indAttNames[i]) == 0)
 				{
-					appendPQExpBuffer(out, ",\n\t'attnum', '%d'::smallint",
+					appendPQExpBuffer(&out, ",\n\t'attnum', '%d'::smallint",
 									  i + 1);
 					found = true;
 					break;
@@ -10721,66 +10722,92 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 		}
 
 		if (!PQgetisnull(res, rownum, i_inherited))
-			appendNamedArgument(out, fout, "inherited", "boolean",
+			appendNamedArgument(&out, fout, "inherited", "boolean",
 								PQgetvalue(res, rownum, i_inherited));
 		if (!PQgetisnull(res, rownum, i_null_frac))
-			appendNamedArgument(out, fout, "null_frac", "real",
+			appendNamedArgument(&out, fout, "null_frac", "real",
 								PQgetvalue(res, rownum, i_null_frac));
 		if (!PQgetisnull(res, rownum, i_avg_width))
-			appendNamedArgument(out, fout, "avg_width", "integer",
+			appendNamedArgument(&out, fout, "avg_width", "integer",
 								PQgetvalue(res, rownum, i_avg_width));
 		if (!PQgetisnull(res, rownum, i_n_distinct))
-			appendNamedArgument(out, fout, "n_distinct", "real",
+			appendNamedArgument(&out, fout, "n_distinct", "real",
 								PQgetvalue(res, rownum, i_n_distinct));
 		if (!PQgetisnull(res, rownum, i_most_common_vals))
-			appendNamedArgument(out, fout, "most_common_vals", "text",
+			appendNamedArgument(&out, fout, "most_common_vals", "text",
 								PQgetvalue(res, rownum, i_most_common_vals));
 		if (!PQgetisnull(res, rownum, i_most_common_freqs))
-			appendNamedArgument(out, fout, "most_common_freqs", "real[]",
+			appendNamedArgument(&out, fout, "most_common_freqs", "real[]",
 								PQgetvalue(res, rownum, i_most_common_freqs));
 		if (!PQgetisnull(res, rownum, i_histogram_bounds))
-			appendNamedArgument(out, fout, "histogram_bounds", "text",
+			appendNamedArgument(&out, fout, "histogram_bounds", "text",
 								PQgetvalue(res, rownum, i_histogram_bounds));
 		if (!PQgetisnull(res, rownum, i_correlation))
-			appendNamedArgument(out, fout, "correlation", "real",
+			appendNamedArgument(&out, fout, "correlation", "real",
 								PQgetvalue(res, rownum, i_correlation));
 		if (!PQgetisnull(res, rownum, i_most_common_elems))
-			appendNamedArgument(out, fout, "most_common_elems", "text",
+			appendNamedArgument(&out, fout, "most_common_elems", "text",
 								PQgetvalue(res, rownum, i_most_common_elems));
 		if (!PQgetisnull(res, rownum, i_most_common_elem_freqs))
-			appendNamedArgument(out, fout, "most_common_elem_freqs", "real[]",
+			appendNamedArgument(&out, fout, "most_common_elem_freqs", "real[]",
 								PQgetvalue(res, rownum, i_most_common_elem_freqs));
 		if (!PQgetisnull(res, rownum, i_elem_count_histogram))
-			appendNamedArgument(out, fout, "elem_count_histogram", "real[]",
+			appendNamedArgument(&out, fout, "elem_count_histogram", "real[]",
 								PQgetvalue(res, rownum, i_elem_count_histogram));
 		if (fout->remoteVersion >= 170000)
 		{
 			if (!PQgetisnull(res, rownum, i_range_length_histogram))
-				appendNamedArgument(out, fout, "range_length_histogram", "text",
+				appendNamedArgument(&out, fout, "range_length_histogram", "text",
 									PQgetvalue(res, rownum, i_range_length_histogram));
 			if (!PQgetisnull(res, rownum, i_range_empty_frac))
-				appendNamedArgument(out, fout, "range_empty_frac", "real",
+				appendNamedArgument(&out, fout, "range_empty_frac", "real",
 									PQgetvalue(res, rownum, i_range_empty_frac));
 			if (!PQgetisnull(res, rownum, i_range_bounds_histogram))
-				appendNamedArgument(out, fout, "range_bounds_histogram", "text",
+				appendNamedArgument(&out, fout, "range_bounds_histogram", "text",
 									PQgetvalue(res, rownum, i_range_bounds_histogram));
 		}
-		appendPQExpBufferStr(out, "\n);\n");
+		appendPQExpBufferStr(&out, "\n);\n");
 	}
 
 	PQclear(res);
 
+	termPQExpBuffer(&query);
+	return out.data;
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	const DumpableObject *dobj = &rsinfo->dobj;
+
+	DumpId	   *deps = NULL;
+	int			ndeps = 0;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
+	/* dependent on the relation definition, if doing schema */
+	if (fout->dopt->dumpSchema)
+	{
+		deps = dobj->dependencies;
+		ndeps = dobj->nDeps;
+	}
+
 	ArchiveEntry(fout, nilCatalogId, createDumpId(),
 				 ARCHIVE_OPTS(.tag = dobj->name,
 							  .namespace = dobj->namespace->dobj.name,
 							  .description = "STATISTICS DATA",
 							  .section = rsinfo->section,
-							  .createStmt = out->data,
-							  .deps = dobj->dependencies,
-							  .nDeps = dobj->nDeps));
-
-	destroyPQExpBuffer(out);
-	destroyPQExpBuffer(query);
+							  .createFn = printRelationStats,
+							  .createArg = rsinfo,
+							  .deps = deps,
+							  .nDeps = ndeps));
 }
 
 /*

base-commit: e2809e3a1015697832ee4d37b75ba1cd0caac0f0
-- 
2.49.0

v12-0002-Batching-getAttributeStats.patchtext/x-patch; charset=US-ASCII; name=v12-0002-Batching-getAttributeStats.patchDownload
From 410171805037718c3adcae778bba56c485038e3f Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Fri, 14 Mar 2025 03:54:26 -0400
Subject: [PATCH v12 2/3] Batching getAttributeStats().

The prepared statement getAttributeStats() is fairly heavyweight and
could greatly increase pg_dump/pg_upgrade runtime. To alleviate this,
create a result set buffer of all of the attribute stats fetched for a
batch of 100 relations that could potentially have stats.

The query ensures that the order of results exactly matches the needs of
the code walking the TOC to print the stats calls.
---
 src/bin/pg_dump/pg_dump.c | 554 ++++++++++++++++++++++++++------------
 1 file changed, 383 insertions(+), 171 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index cc195d6cd9e..26144371b1b 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -143,6 +143,25 @@ typedef enum OidOptions
 	zeroAsNone = 4,
 } OidOptions;
 
+typedef enum StatsBufferState
+{
+	STATSBUF_UNINITIALIZED = 0,
+	STATSBUF_ACTIVE,
+	STATSBUF_EXHAUSTED
+}			StatsBufferState;
+
+typedef struct
+{
+	PGresult   *res;			/* results from most recent
+								 * getAttributeStats() */
+	int			idx;			/* first un-consumed row of results */
+	TocEntry   *te;				/* next TOC entry to search for statsitics
+								 * data */
+
+	StatsBufferState state;		/* current state of the buffer */
+}			AttributeStatsBuffer;
+
+
 /* global decls */
 static bool dosync = true;		/* Issue fsync() to make dump durable on disk. */
 
@@ -209,6 +228,18 @@ static int	nbinaryUpgradeClassOids = 0;
 static SequenceItem *sequences = NULL;
 static int	nsequences = 0;
 
+static AttributeStatsBuffer attrstats =
+{
+	NULL, 0, NULL, STATSBUF_UNINITIALIZED
+};
+
+/*
+ * The maximum number of relations that should be fetched in any one
+ * getAttributeStats() call.
+ */
+
+#define MAX_ATTR_STATS_RELS 100
+
 /*
  * The default number of rows per INSERT when
  * --inserts is specified without --rows-per-insert
@@ -222,6 +253,8 @@ static int	nsequences = 0;
  */
 #define MAX_BLOBS_PER_ARCHIVE_ENTRY 1000
 
+
+
 /*
  * Macro for producing quoted, schema-qualified name of a dumpable object.
  */
@@ -399,6 +432,9 @@ static void setupDumpWorker(Archive *AH);
 static TableInfo *getRootTableInfo(const TableInfo *tbinfo);
 static bool forcePartitionRootLoad(const TableInfo *tbinfo);
 static void read_dump_filters(const char *filename, DumpOptions *dopt);
+static void appendNamedArgument(PQExpBuffer out, Archive *fout,
+								const char *argname, const char *argtype,
+								const char *argval);
 
 
 int
@@ -10560,7 +10596,286 @@ appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
 }
 
 /*
- * printDumpRelationStats --
+ * Fetch next batch of rows from getAttributeStats()
+ */
+static void
+fetchNextAttributeStats(Archive *fout)
+{
+	ArchiveHandle *AH = (ArchiveHandle *) fout;
+	PQExpBufferData schemas;
+	PQExpBufferData relations;
+	int			numoids = 0;
+
+	Assert(AH != NULL);
+
+	/* free last result set, if any */
+	if (attrstats.state == STATSBUF_ACTIVE)
+		PQclear(attrstats.res);
+
+	/* If we have looped around to the start of the TOC, restart */
+	if (attrstats.te == AH->toc)
+		attrstats.te = AH->toc->next;
+
+	initPQExpBuffer(&schemas);
+	initPQExpBuffer(&relations);
+
+	/*
+	 * Walk ahead looking for relstats entries that are active in this
+	 * section, adding the names to the schemas and relations lists.
+	 */
+	while ((attrstats.te != AH->toc) && (numoids < MAX_ATTR_STATS_RELS))
+	{
+		if (attrstats.te->reqs != 0 &&
+			strcmp(attrstats.te->desc, "STATISTICS DATA") == 0)
+		{
+			RelStatsInfo *rsinfo = (RelStatsInfo *) attrstats.te->createDumperArg;
+
+			Assert(rsinfo != NULL);
+
+			if (numoids > 0)
+			{
+				appendPQExpBufferStr(&schemas, ",");
+				appendPQExpBufferStr(&relations, ",");
+			}
+			appendPQExpBufferStr(&schemas, fmtId(rsinfo->dobj.namespace->dobj.name));
+			appendPQExpBufferStr(&relations, fmtId(rsinfo->dobj.name));
+			numoids++;
+		}
+
+		attrstats.te = attrstats.te->next;
+	}
+
+	if (numoids > 0)
+	{
+		PQExpBufferData query;
+
+		initPQExpBuffer(&query);
+		appendPQExpBuffer(&query,
+						  "EXECUTE getAttributeStats('{%s}'::pg_catalog.text[],'{%s}'::pg_catalog.text[])",
+						  schemas.data, relations.data);
+		attrstats.res = ExecuteSqlQuery(fout, query.data, PGRES_TUPLES_OK);
+		attrstats.idx = 0;
+	}
+	else
+	{
+		attrstats.state = STATSBUF_EXHAUSTED;
+		attrstats.res = NULL;
+		attrstats.idx = -1;
+	}
+
+	termPQExpBuffer(&schemas);
+	termPQExpBuffer(&relations);
+}
+
+/*
+ * Prepare the getAttributeStats() statement
+ *
+ * This is done automatically if the user specified dumpStatistics.
+ */
+static void
+initAttributeStats(Archive *fout)
+{
+	ArchiveHandle *AH = (ArchiveHandle *) fout;
+	PQExpBufferData query;
+
+	Assert(AH != NULL);
+	initPQExpBuffer(&query);
+
+	appendPQExpBufferStr(&query,
+						 "PREPARE getAttributeStats(pg_catalog.text[], pg_catalog.text[]) AS\n"
+						 "SELECT s.schemaname, s.tablename, s.attname, s.inherited, "
+						 "s.null_frac, s.avg_width, s.n_distinct, s.most_common_vals, "
+						 "s.most_common_freqs, s.histogram_bounds, s.correlation, "
+						 "s.most_common_elems, s.most_common_elem_freqs, "
+						 "s.elem_count_histogram, ");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(&query,
+							 "s.range_length_histogram, "
+							 "s.range_empty_frac, "
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(&query,
+							 "NULL AS range_length_histogram, "
+							 "NULL AS range_empty_frac, "
+							 " NULL AS range_bounds_histogram ");
+
+	/*
+	 * The results must be in the order of relations supplied in the
+	 * parameters to ensure that they are in sync with a walk of the TOC.
+	 *
+	 * The redundant (and incomplete) filter clause on s.tablename = ANY(...)
+	 * is a way to lead the query into using the index
+	 * pg_class_relname_nsp_index which in turn allows the planner to avoid an
+	 * expensive full scan of pg_stats.
+	 *
+	 * We may need to adjust this query for versions that are not so easily
+	 * led.
+	 */
+	appendPQExpBufferStr(&query,
+						 "FROM pg_catalog.pg_stats AS s "
+						 "JOIN unnest($1, $2) WITH ORDINALITY AS u(schemaname, tablename, ord) "
+						 "ON s.schemaname = u.schemaname "
+						 "AND s.tablename = u.tablename "
+						 "WHERE s.tablename = ANY($2) "
+						 "ORDER BY u.ord, s.attname, s.inherited");
+
+	ExecuteSqlStatement(fout, query.data);
+
+	termPQExpBuffer(&query);
+
+	attrstats.te = AH->toc->next;
+
+	fetchNextAttributeStats(fout);
+
+	attrstats.state = STATSBUF_ACTIVE;
+}
+
+
+/*
+ * append a single attribute stat to the buffer for this relation.
+ */
+static void
+appendAttributeStats(Archive *fout, PQExpBuffer out,
+					 const RelStatsInfo *rsinfo)
+{
+	PGresult   *res = attrstats.res;
+	int			tup_num = attrstats.idx;
+
+	const char *attname;
+
+	static bool indexes_set = false;
+	static int	i_attname,
+				i_inherited,
+				i_null_frac,
+				i_avg_width,
+				i_n_distinct,
+				i_most_common_vals,
+				i_most_common_freqs,
+				i_histogram_bounds,
+				i_correlation,
+				i_most_common_elems,
+				i_most_common_elem_freqs,
+				i_elem_count_histogram,
+				i_range_length_histogram,
+				i_range_empty_frac,
+				i_range_bounds_histogram;
+
+	if (!indexes_set)
+	{
+		/*
+		 * It's a prepared statement, so the indexes will be the same for all
+		 * result sets, so we only need to set them once.
+		 */
+		i_attname = PQfnumber(res, "attname");
+		i_inherited = PQfnumber(res, "inherited");
+		i_null_frac = PQfnumber(res, "null_frac");
+		i_avg_width = PQfnumber(res, "avg_width");
+		i_n_distinct = PQfnumber(res, "n_distinct");
+		i_most_common_vals = PQfnumber(res, "most_common_vals");
+		i_most_common_freqs = PQfnumber(res, "most_common_freqs");
+		i_histogram_bounds = PQfnumber(res, "histogram_bounds");
+		i_correlation = PQfnumber(res, "correlation");
+		i_most_common_elems = PQfnumber(res, "most_common_elems");
+		i_most_common_elem_freqs = PQfnumber(res, "most_common_elem_freqs");
+		i_elem_count_histogram = PQfnumber(res, "elem_count_histogram");
+		i_range_length_histogram = PQfnumber(res, "range_length_histogram");
+		i_range_empty_frac = PQfnumber(res, "range_empty_frac");
+		i_range_bounds_histogram = PQfnumber(res, "range_bounds_histogram");
+		indexes_set = true;
+	}
+
+	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+	appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
+					  fout->remoteVersion);
+	appendPQExpBufferStr(out, "\t'schemaname', ");
+	appendStringLiteralAH(out, rsinfo->dobj.namespace->dobj.name, fout);
+	appendPQExpBufferStr(out, ",\n\t'relname', ");
+	appendStringLiteralAH(out, rsinfo->dobj.name, fout);
+
+	if (PQgetisnull(res, tup_num, i_attname))
+		pg_fatal("attname cannot be NULL");
+	attname = PQgetvalue(res, tup_num, i_attname);
+
+	/*
+	 * Indexes look up attname in indAttNames to derive attnum, all others use
+	 * attname directly.  We must specify attnum for indexes, since their
+	 * attnames are not necessarily stable across dump/reload.
+	 */
+	if (rsinfo->nindAttNames == 0)
+	{
+		appendPQExpBuffer(out, ",\n\t'attname', ");
+		appendStringLiteralAH(out, attname, fout);
+	}
+	else
+	{
+		bool		found = false;
+
+		for (int i = 0; i < rsinfo->nindAttNames; i++)
+			if (strcmp(attname, rsinfo->indAttNames[i]) == 0)
+			{
+				appendPQExpBuffer(out, ",\n\t'attnum', '%d'::smallint",
+								  i + 1);
+				found = true;
+				break;
+			}
+
+		if (!found)
+			pg_fatal("could not find index attname \"%s\"", attname);
+	}
+
+	if (!PQgetisnull(res, tup_num, i_inherited))
+		appendNamedArgument(out, fout, "inherited", "boolean",
+							PQgetvalue(res, tup_num, i_inherited));
+	if (!PQgetisnull(res, tup_num, i_null_frac))
+		appendNamedArgument(out, fout, "null_frac", "real",
+							PQgetvalue(res, tup_num, i_null_frac));
+	if (!PQgetisnull(res, tup_num, i_avg_width))
+		appendNamedArgument(out, fout, "avg_width", "integer",
+							PQgetvalue(res, tup_num, i_avg_width));
+	if (!PQgetisnull(res, tup_num, i_n_distinct))
+		appendNamedArgument(out, fout, "n_distinct", "real",
+							PQgetvalue(res, tup_num, i_n_distinct));
+	if (!PQgetisnull(res, tup_num, i_most_common_vals))
+		appendNamedArgument(out, fout, "most_common_vals", "text",
+							PQgetvalue(res, tup_num, i_most_common_vals));
+	if (!PQgetisnull(res, tup_num, i_most_common_freqs))
+		appendNamedArgument(out, fout, "most_common_freqs", "real[]",
+							PQgetvalue(res, tup_num, i_most_common_freqs));
+	if (!PQgetisnull(res, tup_num, i_histogram_bounds))
+		appendNamedArgument(out, fout, "histogram_bounds", "text",
+							PQgetvalue(res, tup_num, i_histogram_bounds));
+	if (!PQgetisnull(res, tup_num, i_correlation))
+		appendNamedArgument(out, fout, "correlation", "real",
+							PQgetvalue(res, tup_num, i_correlation));
+	if (!PQgetisnull(res, tup_num, i_most_common_elems))
+		appendNamedArgument(out, fout, "most_common_elems", "text",
+							PQgetvalue(res, tup_num, i_most_common_elems));
+	if (!PQgetisnull(res, tup_num, i_most_common_elem_freqs))
+		appendNamedArgument(out, fout, "most_common_elem_freqs", "real[]",
+							PQgetvalue(res, tup_num, i_most_common_elem_freqs));
+	if (!PQgetisnull(res, tup_num, i_elem_count_histogram))
+		appendNamedArgument(out, fout, "elem_count_histogram", "real[]",
+							PQgetvalue(res, tup_num, i_elem_count_histogram));
+	if (fout->remoteVersion >= 170000)
+	{
+		if (!PQgetisnull(res, tup_num, i_range_length_histogram))
+			appendNamedArgument(out, fout, "range_length_histogram", "text",
+								PQgetvalue(res, tup_num, i_range_length_histogram));
+		if (!PQgetisnull(res, tup_num, i_range_empty_frac))
+			appendNamedArgument(out, fout, "range_empty_frac", "real",
+								PQgetvalue(res, tup_num, i_range_empty_frac));
+		if (!PQgetisnull(res, tup_num, i_range_bounds_histogram))
+			appendNamedArgument(out, fout, "range_bounds_histogram", "text",
+								PQgetvalue(res, tup_num, i_range_bounds_histogram));
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+
+
+/*
+ * printRelationStats --
  *
  * Generate the SQL statements needed to restore a relation's statistics.
  */
@@ -10568,64 +10883,21 @@ static char *
 printRelationStats(Archive *fout, const void *userArg)
 {
 	const RelStatsInfo *rsinfo = (RelStatsInfo *) userArg;
-	const DumpableObject *dobj = &rsinfo->dobj;
+	const DumpableObject *dobj;
+	const char *relschema;
+	const char *relname;
+
+	ArchiveHandle *AH = (ArchiveHandle *) fout;
 
-	PQExpBufferData query;
 	PQExpBufferData out;
 
-	PGresult   *res;
-
-	static bool first_query = true;
-	static int	i_attname;
-	static int	i_inherited;
-	static int	i_null_frac;
-	static int	i_avg_width;
-	static int	i_n_distinct;
-	static int	i_most_common_vals;
-	static int	i_most_common_freqs;
-	static int	i_histogram_bounds;
-	static int	i_correlation;
-	static int	i_most_common_elems;
-	static int	i_most_common_elem_freqs;
-	static int	i_elem_count_histogram;
-	static int	i_range_length_histogram;
-	static int	i_range_empty_frac;
-	static int	i_range_bounds_histogram;
-
-	initPQExpBuffer(&query);
-
-	if (first_query)
-	{
-		appendPQExpBufferStr(&query,
-							 "PREPARE getAttributeStats(pg_catalog.text, pg_catalog.text) AS\n"
-							 "SELECT s.attname, s.inherited, "
-							 "s.null_frac, s.avg_width, s.n_distinct, "
-							 "s.most_common_vals, s.most_common_freqs, "
-							 "s.histogram_bounds, s.correlation, "
-							 "s.most_common_elems, s.most_common_elem_freqs, "
-							 "s.elem_count_histogram, ");
-
-		if (fout->remoteVersion >= 170000)
-			appendPQExpBufferStr(&query,
-								 "s.range_length_histogram, "
-								 "s.range_empty_frac, "
-								 "s.range_bounds_histogram ");
-		else
-			appendPQExpBufferStr(&query,
-								 "NULL AS range_length_histogram,"
-								 "NULL AS range_empty_frac,"
-								 "NULL AS range_bounds_histogram ");
-
-		appendPQExpBufferStr(&query,
-							 "FROM pg_catalog.pg_stats s "
-							 "WHERE s.schemaname = $1 "
-							 "AND s.tablename = $2 "
-							 "ORDER BY s.attname, s.inherited");
-
-		ExecuteSqlStatement(fout, query.data);
-
-		resetPQExpBuffer(&query);
-	}
+	Assert(rsinfo != NULL);
+	dobj = &rsinfo->dobj;
+	Assert(dobj != NULL);
+	relschema = dobj->namespace->dobj.name;
+	Assert(relschema != NULL);
+	relname = dobj->name;
+	Assert(relname != NULL);
 
 	initPQExpBuffer(&out);
 
@@ -10646,132 +10918,72 @@ printRelationStats(Archive *fout, const void *userArg)
 	appendPQExpBufferStr(&out, "\n);\n");
 
 
-	/* fetch attribute stats */
-	appendPQExpBufferStr(&query, "EXECUTE getAttributeStats(");
-	appendStringLiteralAH(&query, dobj->namespace->dobj.name, fout);
-	appendPQExpBufferStr(&query, ", ");
-	appendStringLiteralAH(&query, dobj->name, fout);
-	appendPQExpBufferStr(&query, ")");
+	AH->txnCount++;
 
-	res = ExecuteSqlQuery(fout, query.data, PGRES_TUPLES_OK);
+	if (attrstats.state == STATSBUF_UNINITIALIZED)
+		initAttributeStats(fout);
 
-	if (first_query)
+	/*
+	 * Because the query returns rows in the same order as the relations
+	 * requested, and because every relation gets at least one row in the
+	 * result set, the first row for this relation must correspond either to
+	 * the current row of this result set (if one exists) or the first row of
+	 * the next result set (if this one is already consumed).
+	 */
+	if (attrstats.state != STATSBUF_ACTIVE)
+		pg_fatal("Exhausted getAttributeStats() before processing %s.%s",
+				 rsinfo->dobj.namespace->dobj.name,
+				 rsinfo->dobj.name);
+
+	/*
+	 * If the current result set has been fully consumed, then the row(s) we
+	 * need (if any) would be found in the next one. This will update
+	 * attrstats.res and attrstats.idx.
+	 */
+	if (PQntuples(attrstats.res) <= attrstats.idx)
+		fetchNextAttributeStats(fout);
+
+	while (true)
 	{
-		i_attname = PQfnumber(res, "attname");
-		i_inherited = PQfnumber(res, "inherited");
-		i_null_frac = PQfnumber(res, "null_frac");
-		i_avg_width = PQfnumber(res, "avg_width");
-		i_n_distinct = PQfnumber(res, "n_distinct");
-		i_most_common_vals = PQfnumber(res, "most_common_vals");
-		i_most_common_freqs = PQfnumber(res, "most_common_freqs");
-		i_histogram_bounds = PQfnumber(res, "histogram_bounds");
-		i_correlation = PQfnumber(res, "correlation");
-		i_most_common_elems = PQfnumber(res, "most_common_elems");
-		i_most_common_elem_freqs = PQfnumber(res, "most_common_elem_freqs");
-		i_elem_count_histogram = PQfnumber(res, "elem_count_histogram");
-		i_range_length_histogram = PQfnumber(res, "range_length_histogram");
-		i_range_empty_frac = PQfnumber(res, "range_empty_frac");
-		i_range_bounds_histogram = PQfnumber(res, "range_bounds_histogram");
-		first_query = false;
-	}
-
-	/* restore attribute stats */
-	for (int rownum = 0; rownum < PQntuples(res); rownum++)
-	{
-		const char *attname;
-
-		appendPQExpBufferStr(&out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
-		appendPQExpBuffer(&out, "\t'version', '%u'::integer,\n",
-						  fout->remoteVersion);
-		appendPQExpBufferStr(&out, "\t'schemaname', ");
-		appendStringLiteralAH(&out, rsinfo->dobj.namespace->dobj.name, fout);
-		appendPQExpBufferStr(&out, ",\n\t'relname', ");
-		appendStringLiteralAH(&out, rsinfo->dobj.name, fout);
-
-		if (PQgetisnull(res, rownum, i_attname))
-			pg_fatal("attname cannot be NULL");
-		attname = PQgetvalue(res, rownum, i_attname);
+		int			i_schemaname;
+		int			i_tablename;
+		char	   *schemaname;
+		char	   *tablename;	/* misnomer, following pg_stats naming */
 
 		/*
-		 * Indexes look up attname in indAttNames to derive attnum, all others
-		 * use attname directly.  We must specify attnum for indexes, since
-		 * their attnames are not necessarily stable across dump/reload.
+		 * If we hit the end of the result set, then there are no more records
+		 * for this relation, so we should stop, but first get the next result
+		 * set for the next batch of relations.
 		 */
-		if (rsinfo->nindAttNames == 0)
+		if (PQntuples(attrstats.res) <= attrstats.idx)
 		{
-			appendPQExpBuffer(&out, ",\n\t'attname', ");
-			appendStringLiteralAH(&out, attname, fout);
-		}
-		else
-		{
-			bool		found = false;
-
-			for (int i = 0; i < rsinfo->nindAttNames; i++)
-			{
-				if (strcmp(attname, rsinfo->indAttNames[i]) == 0)
-				{
-					appendPQExpBuffer(&out, ",\n\t'attnum', '%d'::smallint",
-									  i + 1);
-					found = true;
-					break;
-				}
-			}
-
-			if (!found)
-				pg_fatal("could not find index attname \"%s\"", attname);
+			fetchNextAttributeStats(fout);
+			break;
 		}
 
-		if (!PQgetisnull(res, rownum, i_inherited))
-			appendNamedArgument(&out, fout, "inherited", "boolean",
-								PQgetvalue(res, rownum, i_inherited));
-		if (!PQgetisnull(res, rownum, i_null_frac))
-			appendNamedArgument(&out, fout, "null_frac", "real",
-								PQgetvalue(res, rownum, i_null_frac));
-		if (!PQgetisnull(res, rownum, i_avg_width))
-			appendNamedArgument(&out, fout, "avg_width", "integer",
-								PQgetvalue(res, rownum, i_avg_width));
-		if (!PQgetisnull(res, rownum, i_n_distinct))
-			appendNamedArgument(&out, fout, "n_distinct", "real",
-								PQgetvalue(res, rownum, i_n_distinct));
-		if (!PQgetisnull(res, rownum, i_most_common_vals))
-			appendNamedArgument(&out, fout, "most_common_vals", "text",
-								PQgetvalue(res, rownum, i_most_common_vals));
-		if (!PQgetisnull(res, rownum, i_most_common_freqs))
-			appendNamedArgument(&out, fout, "most_common_freqs", "real[]",
-								PQgetvalue(res, rownum, i_most_common_freqs));
-		if (!PQgetisnull(res, rownum, i_histogram_bounds))
-			appendNamedArgument(&out, fout, "histogram_bounds", "text",
-								PQgetvalue(res, rownum, i_histogram_bounds));
-		if (!PQgetisnull(res, rownum, i_correlation))
-			appendNamedArgument(&out, fout, "correlation", "real",
-								PQgetvalue(res, rownum, i_correlation));
-		if (!PQgetisnull(res, rownum, i_most_common_elems))
-			appendNamedArgument(&out, fout, "most_common_elems", "text",
-								PQgetvalue(res, rownum, i_most_common_elems));
-		if (!PQgetisnull(res, rownum, i_most_common_elem_freqs))
-			appendNamedArgument(&out, fout, "most_common_elem_freqs", "real[]",
-								PQgetvalue(res, rownum, i_most_common_elem_freqs));
-		if (!PQgetisnull(res, rownum, i_elem_count_histogram))
-			appendNamedArgument(&out, fout, "elem_count_histogram", "real[]",
-								PQgetvalue(res, rownum, i_elem_count_histogram));
-		if (fout->remoteVersion >= 170000)
-		{
-			if (!PQgetisnull(res, rownum, i_range_length_histogram))
-				appendNamedArgument(&out, fout, "range_length_histogram", "text",
-									PQgetvalue(res, rownum, i_range_length_histogram));
-			if (!PQgetisnull(res, rownum, i_range_empty_frac))
-				appendNamedArgument(&out, fout, "range_empty_frac", "real",
-									PQgetvalue(res, rownum, i_range_empty_frac));
-			if (!PQgetisnull(res, rownum, i_range_bounds_histogram))
-				appendNamedArgument(&out, fout, "range_bounds_histogram", "text",
-									PQgetvalue(res, rownum, i_range_bounds_histogram));
-		}
-		appendPQExpBufferStr(&out, "\n);\n");
+		i_schemaname = PQfnumber(attrstats.res, "schemaname");
+		Assert(i_schemaname >= 0);
+		i_tablename = PQfnumber(attrstats.res, "tablename");
+		Assert(i_tablename >= 0);
+
+		if (PQgetisnull(attrstats.res, attrstats.idx, i_schemaname))
+			pg_fatal("getAttributeStats() schemaname cannot be NULL");
+
+		if (PQgetisnull(attrstats.res, attrstats.idx, i_tablename))
+			pg_fatal("getAttributeStats() tablename cannot be NULL");
+
+		schemaname = PQgetvalue(attrstats.res, attrstats.idx, i_schemaname);
+		tablename = PQgetvalue(attrstats.res, attrstats.idx, i_tablename);
+
+		/* stop if current stat row isn't for this relation */
+		if (strcmp(relname, tablename) != 0 || strcmp(relschema, schemaname) != 0)
+			break;
+
+		appendAttributeStats(fout, &out, rsinfo);
+		AH->txnCount++;
+		attrstats.idx++;
 	}
 
-	PQclear(res);
-
-	termPQExpBuffer(&query);
 	return out.data;
 }
 
-- 
2.49.0

v12-0003-Downgrade-many-pg_restore_-_stats-errors-to-warn.patchtext/x-patch; charset=US-ASCII; name=v12-0003-Downgrade-many-pg_restore_-_stats-errors-to-warn.patchDownload
From 16794820dedd79ec58f8692da5b50a4d8976620a Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Sat, 8 Mar 2025 00:52:41 -0500
Subject: [PATCH v12 3/3] Downgrade many pg_restore_*_stats errors to warnings.

We want to avoid errors that can potentially stop an otherwise
successful pg_upgrade or pg_restore operation. With that in mind, change
as many ERROR reports to WARNING + early termination with no data
updated.
---
 src/include/statistics/stat_utils.h        |   4 +-
 src/backend/statistics/attribute_stats.c   | 120 ++++++++++----
 src/backend/statistics/relation_stats.c    |  12 +-
 src/backend/statistics/stat_utils.c        |  65 ++++++--
 src/test/regress/expected/stats_import.out | 184 ++++++++++++++++-----
 src/test/regress/sql/stats_import.sql      |  36 ++--
 6 files changed, 309 insertions(+), 112 deletions(-)

diff --git a/src/include/statistics/stat_utils.h b/src/include/statistics/stat_utils.h
index 512eb776e0e..809c8263a41 100644
--- a/src/include/statistics/stat_utils.h
+++ b/src/include/statistics/stat_utils.h
@@ -21,7 +21,7 @@ struct StatsArgInfo
 	Oid			argtype;
 };
 
-extern void stats_check_required_arg(FunctionCallInfo fcinfo,
+extern bool stats_check_required_arg(FunctionCallInfo fcinfo,
 									 struct StatsArgInfo *arginfo,
 									 int argnum);
 extern bool stats_check_arg_array(FunctionCallInfo fcinfo,
@@ -30,7 +30,7 @@ extern bool stats_check_arg_pair(FunctionCallInfo fcinfo,
 								 struct StatsArgInfo *arginfo,
 								 int argnum1, int argnum2);
 
-extern void stats_lock_check_privileges(Oid reloid);
+extern bool stats_lock_check_privileges(Oid reloid);
 
 extern Oid	stats_lookup_relid(const char *nspname, const char *relname);
 
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index f5eb17ba42d..b7ba1622391 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -100,7 +100,7 @@ static struct StatsArgInfo cleararginfo[] =
 
 static bool attribute_statistics_update(FunctionCallInfo fcinfo);
 static Node *get_attr_expr(Relation rel, int attnum);
-static void get_attr_stat_type(Oid reloid, AttrNumber attnum,
+static bool get_attr_stat_type(Oid reloid, AttrNumber attnum,
 							   Oid *atttypid, int32 *atttypmod,
 							   char *atttyptype, Oid *atttypcoll,
 							   Oid *eq_opr, Oid *lt_opr);
@@ -129,10 +129,12 @@ static void init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
  * stored as an anyarray, and the representation of the array needs to store
  * the correct element type, which must be derived from the attribute.
  *
- * Major errors, such as the table not existing, the attribute not existing,
- * or a permissions failure are always reported at ERROR. Other errors, such
- * as a conversion failure on one statistic kind, are reported as a WARNING
- * and other statistic kinds may still be updated.
+ * This function is called during database upgrades and restorations, therefore
+ * it is imperative to avoid ERRORs that could potentially end the upgrade or
+ * restore unless. Major errors, such as the table not existing, the attribute
+ * not existing, or permissions failure are reported as WARNINGs with an end to
+ * the function, thus allowing the upgrade/restore to continue, but without the
+ * stats that can be regenereated once the database is online again.
  */
 static bool
 attribute_statistics_update(FunctionCallInfo fcinfo)
@@ -148,8 +150,8 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	HeapTuple	statup;
 
 	Oid			atttypid = InvalidOid;
-	int32		atttypmod;
-	char		atttyptype;
+	int32		atttypmod = -1;
+	char		atttyptype = TYPTYPE_PSEUDO; /* Not a great default, but there is no TYPTYPE_INVALID */
 	Oid			atttypcoll = InvalidOid;
 	Oid			eq_opr = InvalidOid;
 	Oid			lt_opr = InvalidOid;
@@ -176,38 +178,52 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 	bool		result = true;
 
-	stats_check_required_arg(fcinfo, attarginfo, ATTRELSCHEMA_ARG);
-	stats_check_required_arg(fcinfo, attarginfo, ATTRELNAME_ARG);
+	if (!stats_check_required_arg(fcinfo, attarginfo, ATTRELSCHEMA_ARG))
+		return false;
+	if (!stats_check_required_arg(fcinfo, attarginfo, ATTRELNAME_ARG))
+		return false;
 
 	nspname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELSCHEMA_ARG));
 	relname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELNAME_ARG));
 
 	reloid = stats_lookup_relid(nspname, relname);
+	if (!OidIsValid(reloid))
+		return false;
 
 	if (RecoveryInProgress())
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("recovery is in progress"),
 				 errhint("Statistics cannot be modified during recovery.")));
+		return false;
+	}
 
 	/* lock before looking up attribute */
-	stats_lock_check_privileges(reloid);
+	if (!stats_lock_check_privileges(reloid))
+		return false;
 
 	/* user can specify either attname or attnum, but not both */
 	if (!PG_ARGISNULL(ATTNAME_ARG))
 	{
 		if (!PG_ARGISNULL(ATTNUM_ARG))
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 					 errmsg("cannot specify both attname and attnum")));
+			return false;
+		}
 		attname = TextDatumGetCString(PG_GETARG_DATUM(ATTNAME_ARG));
 		attnum = get_attnum(reloid, attname);
 		/* note that this test covers attisdropped cases too: */
 		if (attnum == InvalidAttrNumber)
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 					(errcode(ERRCODE_UNDEFINED_COLUMN),
 					 errmsg("column \"%s\" of relation \"%s\" does not exist",
 							attname, relname)));
+			return false;
+		}
 	}
 	else if (!PG_ARGISNULL(ATTNUM_ARG))
 	{
@@ -216,27 +232,33 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 		/* annoyingly, get_attname doesn't check attisdropped */
 		if (attname == NULL ||
 			!SearchSysCacheExistsAttName(reloid, attname))
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 					(errcode(ERRCODE_UNDEFINED_COLUMN),
 					 errmsg("column %d of relation \"%s\" does not exist",
 							attnum, relname)));
+			return false;
+		}
 	}
 	else
 	{
-		ereport(ERROR,
+		ereport(WARNING,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("must specify either attname or attnum")));
-		attname = NULL;			/* keep compiler quiet */
-		attnum = 0;
+		return false;
 	}
 
 	if (attnum < 0)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot modify statistics on system column \"%s\"",
 						attname)));
+		return false;
+	}
 
-	stats_check_required_arg(fcinfo, attarginfo, INHERITED_ARG);
+	if (!stats_check_required_arg(fcinfo, attarginfo, INHERITED_ARG))
+		return false;
 	inherited = PG_GETARG_BOOL(INHERITED_ARG);
 
 	/*
@@ -285,10 +307,11 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	}
 
 	/* derive information from attribute */
-	get_attr_stat_type(reloid, attnum,
-					   &atttypid, &atttypmod,
-					   &atttyptype, &atttypcoll,
-					   &eq_opr, &lt_opr);
+	if (!get_attr_stat_type(reloid, attnum,
+							&atttypid, &atttypmod,
+							&atttyptype, &atttypcoll,
+							&eq_opr, &lt_opr))
+		result = false;
 
 	/* if needed, derive element type */
 	if (do_mcelem || do_dechist)
@@ -568,7 +591,7 @@ get_attr_expr(Relation rel, int attnum)
 /*
  * Derive type information from the attribute.
  */
-static void
+static bool
 get_attr_stat_type(Oid reloid, AttrNumber attnum,
 				   Oid *atttypid, int32 *atttypmod,
 				   char *atttyptype, Oid *atttypcoll,
@@ -585,18 +608,26 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum,
 
 	/* Attribute not found */
 	if (!HeapTupleIsValid(atup))
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 errmsg("attribute %d of relation \"%s\" does not exist",
 						attnum, RelationGetRelationName(rel))));
+		relation_close(rel, NoLock);
+		return false;
+	}
 
 	attr = (Form_pg_attribute) GETSTRUCT(atup);
 
 	if (attr->attisdropped)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 errmsg("attribute %d of relation \"%s\" does not exist",
 						attnum, RelationGetRelationName(rel))));
+		relation_close(rel, NoLock);
+		return false;
+	}
 
 	expr = get_attr_expr(rel, attr->attnum);
 
@@ -645,6 +676,7 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum,
 		*atttypcoll = DEFAULT_COLLATION_OID;
 
 	relation_close(rel, NoLock);
+	return true;
 }
 
 /*
@@ -770,6 +802,10 @@ set_stats_slot(Datum *values, bool *nulls, bool *replaces,
 	if (slotidx >= STATISTIC_NUM_SLOTS && first_empty >= 0)
 		slotidx = first_empty;
 
+	/*
+	 * Currently there is no datatype that can have more than STATISTIC_NUM_SLOTS
+	 * statistic kinds, so this can safely remain an ERROR for now.
+	 */
 	if (slotidx >= STATISTIC_NUM_SLOTS)
 		ereport(ERROR,
 				(errmsg("maximum number of statistics slots exceeded: %d",
@@ -915,38 +951,54 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 	AttrNumber	attnum;
 	bool		inherited;
 
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELSCHEMA_ARG);
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELNAME_ARG);
-	stats_check_required_arg(fcinfo, cleararginfo, C_ATTNAME_ARG);
-	stats_check_required_arg(fcinfo, cleararginfo, C_INHERITED_ARG);
+	if (!stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELSCHEMA_ARG))
+		PG_RETURN_VOID();
+	if (!stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELNAME_ARG))
+		PG_RETURN_VOID();
+	if (!stats_check_required_arg(fcinfo, cleararginfo, C_ATTNAME_ARG))
+		PG_RETURN_VOID();
+	if (!stats_check_required_arg(fcinfo, cleararginfo, C_INHERITED_ARG))
+		PG_RETURN_VOID();
 
 	nspname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELSCHEMA_ARG));
 	relname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELNAME_ARG));
 
 	reloid = stats_lookup_relid(nspname, relname);
+	if (!OidIsValid(reloid))
+		PG_RETURN_VOID();
 
 	if (RecoveryInProgress())
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("recovery is in progress"),
 				 errhint("Statistics cannot be modified during recovery.")));
+		PG_RETURN_VOID();
+	}
 
-	stats_lock_check_privileges(reloid);
+	if (!stats_lock_check_privileges(reloid))
+		PG_RETURN_VOID();
 
 	attname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTNAME_ARG));
 	attnum = get_attnum(reloid, attname);
 
 	if (attnum < 0)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot clear statistics on system column \"%s\"",
 						attname)));
+		PG_RETURN_VOID();
+	}
 
 	if (attnum == InvalidAttrNumber)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
 				 errmsg("column \"%s\" of relation \"%s\" does not exist",
 						attname, get_rel_name(reloid))));
+		PG_RETURN_VOID();
+	}
 
 	inherited = PG_GETARG_BOOL(C_INHERITED_ARG);
 
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index cd3a75b621a..7c47af15c9f 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -83,13 +83,18 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 	bool		nulls[4] = {0};
 	int			nreplaces = 0;
 
-	stats_check_required_arg(fcinfo, relarginfo, RELSCHEMA_ARG);
-	stats_check_required_arg(fcinfo, relarginfo, RELNAME_ARG);
+	if (!stats_check_required_arg(fcinfo, relarginfo, RELSCHEMA_ARG))
+		return false;
+
+	if (!stats_check_required_arg(fcinfo, relarginfo, RELNAME_ARG))
+		return false;
 
 	nspname = TextDatumGetCString(PG_GETARG_DATUM(RELSCHEMA_ARG));
 	relname = TextDatumGetCString(PG_GETARG_DATUM(RELNAME_ARG));
 
 	reloid = stats_lookup_relid(nspname, relname);
+	if (!OidIsValid(reloid))
+		return false;
 
 	if (RecoveryInProgress())
 		ereport(ERROR,
@@ -97,7 +102,8 @@ relation_statistics_update(FunctionCallInfo fcinfo)
 				 errmsg("recovery is in progress"),
 				 errhint("Statistics cannot be modified during recovery.")));
 
-	stats_lock_check_privileges(reloid);
+	if (!stats_lock_check_privileges(reloid))
+		return false;
 
 	if (!PG_ARGISNULL(RELPAGES_ARG))
 	{
diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c
index a9a3224efe6..d587e875457 100644
--- a/src/backend/statistics/stat_utils.c
+++ b/src/backend/statistics/stat_utils.c
@@ -33,16 +33,20 @@
 /*
  * Ensure that a given argument is not null.
  */
-void
+bool
 stats_check_required_arg(FunctionCallInfo fcinfo,
 						 struct StatsArgInfo *arginfo,
 						 int argnum)
 {
 	if (PG_ARGISNULL(argnum))
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("\"%s\" cannot be NULL",
 						arginfo[argnum].argname)));
+		return false;
+	}
+	return true;
 }
 
 /*
@@ -127,13 +131,14 @@ stats_check_arg_pair(FunctionCallInfo fcinfo,
  *   - the role owns the current database and the relation is not shared
  *   - the role has the MAINTAIN privilege on the relation
  */
-void
+bool
 stats_lock_check_privileges(Oid reloid)
 {
 	Relation	table;
 	Oid			table_oid = reloid;
 	Oid			index_oid = InvalidOid;
 	LOCKMODE	index_lockmode = NoLock;
+	bool		ok = true;
 
 	/*
 	 * For indexes, we follow the locking behavior in do_analyze_rel() and
@@ -173,14 +178,15 @@ stats_lock_check_privileges(Oid reloid)
 		case RELKIND_PARTITIONED_TABLE:
 			break;
 		default:
-			ereport(ERROR,
+			ereport(WARNING,
 					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 					 errmsg("cannot modify statistics for relation \"%s\"",
 							RelationGetRelationName(table)),
 					 errdetail_relkind_not_supported(table->rd_rel->relkind)));
+		ok = false;
 	}
 
-	if (OidIsValid(index_oid))
+	if (ok && (OidIsValid(index_oid)))
 	{
 		Relation	index;
 
@@ -193,25 +199,33 @@ stats_lock_check_privileges(Oid reloid)
 		relation_close(index, NoLock);
 	}
 
-	if (table->rd_rel->relisshared)
-		ereport(ERROR,
+	if (ok && (table->rd_rel->relisshared))
+	{
+		ereport(WARNING,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot modify statistics for shared relation")));
+		ok = false;
+	}
 
-	if (!object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId()))
+	if (ok && (!object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId())))
 	{
 		AclResult	aclresult = pg_class_aclcheck(RelationGetRelid(table),
 												  GetUserId(),
 												  ACL_MAINTAIN);
 
 		if (aclresult != ACLCHECK_OK)
-			aclcheck_error(aclresult,
-						   get_relkind_objtype(table->rd_rel->relkind),
-						   NameStr(table->rd_rel->relname));
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+						errmsg("permission denied for relation %s",
+							   NameStr(table->rd_rel->relname))));
+			ok = false;
+		}
 	}
 
 	/* retain lock on table */
 	relation_close(table, NoLock);
+	return ok;
 }
 
 /*
@@ -223,10 +237,20 @@ stats_lookup_relid(const char *nspname, const char *relname)
 	Oid			nspoid;
 	Oid			reloid;
 
-	nspoid = LookupExplicitNamespace(nspname, false);
+	nspoid = LookupExplicitNamespace(nspname, true);
+	if (!OidIsValid(nspoid))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_UNDEFINED_TABLE),
+				 errmsg("relation \"%s.%s\" does not exist",
+						nspname, relname)));
+
+		return InvalidOid;
+	}
+
 	reloid = get_relname_relid(relname, nspoid);
 	if (!OidIsValid(reloid))
-		ereport(ERROR,
+		ereport(WARNING,
 				(errcode(ERRCODE_UNDEFINED_TABLE),
 				 errmsg("relation \"%s.%s\" does not exist",
 						nspname, relname)));
@@ -303,9 +327,12 @@ stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
 								  &args, &types, &argnulls);
 
 	if (nargs % 2 != 0)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 				errmsg("variadic arguments must be name/value pairs"),
 				errhint("Provide an even number of variadic arguments that can be divided into pairs."));
+		return false;
+	}
 
 	/*
 	 * For each argument name/value pair, find corresponding positional
@@ -318,14 +345,20 @@ stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
 		char	   *argname;
 
 		if (argnulls[i])
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 					(errmsg("name at variadic position %d is NULL", i + 1)));
+			return false;
+		}
 
 		if (types[i] != TEXTOID)
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 					(errmsg("name at variadic position %d has type \"%s\", expected type \"%s\"",
 							i + 1, format_type_be(types[i]),
 							format_type_be(TEXTOID))));
+			return false;
+		}
 
 		if (argnulls[i + 1])
 			continue;
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index 48d6392b4ad..161cf67b711 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -46,49 +46,85 @@ SELECT pg_clear_relation_stats('stats_import', 'test');
 --
 -- relstats tests
 --
--- error: schemaname missing
+-- warning: schemaname missing, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'relname', 'test',
         'relpages', 17::integer);
-ERROR:  "schemaname" cannot be NULL
--- error: relname missing
+WARNING:  "schemaname" cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+-- warning: relname missing, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relpages', 17::integer);
-ERROR:  "relname" cannot be NULL
---- error: schemaname is wrong type
+WARNING:  "relname" cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+--- warning: schemaname is wrong type, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 3.6::float,
         'relname', 'test',
         'relpages', 17::integer);
 WARNING:  argument "schemaname" has type "double precision", expected type "text"
-ERROR:  "schemaname" cannot be NULL
---- error: relname is wrong type
+WARNING:  "schemaname" cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+--- warning: relname is wrong type, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 0::oid,
         'relpages', 17::integer);
 WARNING:  argument "relname" has type "oid", expected type "text"
-ERROR:  "relname" cannot be NULL
--- error: relation not found
+WARNING:  "relname" cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+-- warning: relation not found, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'nope',
         'relpages', 17::integer);
-ERROR:  relation "stats_import.nope" does not exist
--- error: odd number of variadic arguments cannot be pairs
+WARNING:  relation "stats_import.nope" does not exist
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+-- warning: odd number of variadic arguments cannot be pairs, nothing updated
 SELECT pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'test',
         'relallvisible');
-ERROR:  variadic arguments must be name/value pairs
+WARNING:  variadic arguments must be name/value pairs
 HINT:  Provide an even number of variadic arguments that can be divided into pairs.
--- error: argument name is NULL
+WARNING:  "schemaname" cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+-- warning: argument name is NULL, nothing updated
 SELECT pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'test',
         NULL, '17'::integer);
-ERROR:  name at variadic position 5 is NULL
+WARNING:  name at variadic position 5 is NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
 -- starting stats
 SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
@@ -340,65 +376,110 @@ CREATE SEQUENCE stats_import.testseq;
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'testseq');
-ERROR:  cannot modify statistics for relation "testseq"
+WARNING:  cannot modify statistics for relation "testseq"
 DETAIL:  This operation is not supported for sequences.
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
 SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testseq');
-ERROR:  cannot modify statistics for relation "testseq"
+WARNING:  cannot modify statistics for relation "testseq"
 DETAIL:  This operation is not supported for sequences.
+ pg_clear_relation_stats 
+-------------------------
+ 
+(1 row)
+
 CREATE VIEW stats_import.testview AS SELECT * FROM stats_import.test;
 SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname => 'testview');
-ERROR:  cannot modify statistics for relation "testview"
+WARNING:  cannot modify statistics for relation "testview"
 DETAIL:  This operation is not supported for views.
+ pg_clear_relation_stats 
+-------------------------
+ 
+(1 row)
+
 --
 -- attribute stats
 --
--- error: schemaname missing
+-- warning: schemaname missing, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relname', 'test',
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  "schemaname" cannot be NULL
--- error: schema does not exist
+WARNING:  "schemaname" cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: schema does not exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'nope',
     'relname', 'test',
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  schema "nope" does not exist
--- error: relname missing
+WARNING:  relation "nope.test" does not exist
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: relname missing, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  "relname" cannot be NULL
--- error: relname does not exist
+WARNING:  "relname" cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: relname does not exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'nope',
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  relation "stats_import.nope" does not exist
--- error: relname null
+WARNING:  relation "stats_import.nope" does not exist
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: relname null, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', NULL,
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  "relname" cannot be NULL
--- error: NULL attname
+WARNING:  "relname" cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: NULL attname, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
     'attname', NULL,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  must specify either attname or attnum
--- error: attname doesn't exist
+WARNING:  must specify either attname or attnum
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: attname doesn't exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -407,8 +488,13 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'null_frac', 0.1::real,
     'avg_width', 2::integer,
     'n_distinct', 0.3::real);
-ERROR:  column "nope" of relation "test" does not exist
--- error: both attname and attnum
+WARNING:  column "nope" of relation "test" does not exist
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: both attname and attnum, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -416,30 +502,50 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'attnum', 1::smallint,
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  cannot specify both attname and attnum
--- error: neither attname nor attnum
+WARNING:  cannot specify both attname and attnum
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: neither attname nor attnum, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  must specify either attname or attnum
--- error: attribute is system column
+WARNING:  must specify either attname or attnum
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: attribute is system column, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
     'attname', 'xmin',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
-ERROR:  cannot modify statistics on system column "xmin"
--- error: inherited null
+WARNING:  cannot modify statistics on system column "xmin"
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- warning: inherited null, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
     'attname', 'id',
     'inherited', NULL::boolean,
     'null_frac', 0.1::real);
-ERROR:  "inherited" cannot be NULL
+WARNING:  "inherited" cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
 -- ok: just the fixed values, with version, no stakinds
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index d140733a750..be8045ceea5 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -39,41 +39,41 @@ SELECT pg_clear_relation_stats('stats_import', 'test');
 -- relstats tests
 --
 
--- error: schemaname missing
+-- warning: schemaname missing, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'relname', 'test',
         'relpages', 17::integer);
 
--- error: relname missing
+-- warning: relname missing, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relpages', 17::integer);
 
---- error: schemaname is wrong type
+--- warning: schemaname is wrong type, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 3.6::float,
         'relname', 'test',
         'relpages', 17::integer);
 
---- error: relname is wrong type
+--- warning: relname is wrong type, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 0::oid,
         'relpages', 17::integer);
 
--- error: relation not found
+-- warning: relation not found, nothing updated
 SELECT pg_catalog.pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'nope',
         'relpages', 17::integer);
 
--- error: odd number of variadic arguments cannot be pairs
+-- warning: odd number of variadic arguments cannot be pairs, nothing updated
 SELECT pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'test',
         'relallvisible');
 
--- error: argument name is NULL
+-- warning: argument name is NULL, nothing updated
 SELECT pg_restore_relation_stats(
         'schemaname', 'stats_import',
         'relname', 'test',
@@ -246,14 +246,14 @@ SELECT pg_catalog.pg_clear_relation_stats(schemaname => 'stats_import', relname
 -- attribute stats
 --
 
--- error: schemaname missing
+-- warning: schemaname missing, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'relname', 'test',
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: schema does not exist
+-- warning: schema does not exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'nope',
     'relname', 'test',
@@ -261,14 +261,14 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: relname missing
+-- warning: relname missing, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'attname', 'id',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: relname does not exist
+-- warning: relname does not exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'nope',
@@ -276,7 +276,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: relname null
+-- warning: relname null, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', NULL,
@@ -284,7 +284,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: NULL attname
+-- warning: NULL attname, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -292,7 +292,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: attname doesn't exist
+-- warning: attname doesn't exist, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -302,7 +302,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'avg_width', 2::integer,
     'n_distinct', 0.3::real);
 
--- error: both attname and attnum
+-- warning: both attname and attnum, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -311,14 +311,14 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: neither attname nor attnum
+-- warning: neither attname nor attnum, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: attribute is system column
+-- warning: attribute is system column, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
@@ -326,7 +326,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
     'inherited', false::boolean,
     'null_frac', 0.1::real);
 
--- error: inherited null
+-- warning: inherited null, nothing updated
 SELECT pg_catalog.pg_restore_attribute_stats(
     'schemaname', 'stats_import',
     'relname', 'test',
-- 
2.49.0

#504Robert Haas
robertmhaas@gmail.com
In reply to: Greg Sabino Mullane (#398)
Re: Statistics Import and Export

On Thu, Feb 27, 2025 at 10:43 PM Greg Sabino Mullane <htamfids@gmail.com> wrote:

I know I'm coming late to this, but I would like us to rethink having statistics dumped by default.

+1. I think I said this before, but I don't think it's correct to
regard the statistics as part of the database. It's great for
pg_upgrade to preserve them, but I think doing so for a regular dump
should be opt-in.

--
Robert Haas
EDB: http://www.enterprisedb.com

#505Jeff Davis
pgsql@j-davis.com
In reply to: Robert Haas (#504)
Re: Statistics Import and Export

On Mon, 2025-03-31 at 13:39 -0400, Robert Haas wrote:

+1. I think I said this before, but I don't think it's correct to
regard the statistics as part of the database. It's great for
pg_upgrade to preserve them, but I think doing so for a regular dump
should be opt-in.

I'm confused about the timing of this message -- we already have an
Open Item for 18 to make this decision. After commit bde2fb797a,
changing the default is a one-line change, so there's no technical
problem.

I thought the general plan was to decide during beta. Would you like to
make the decision now for some reason?

Regards,
Jeff Davis

#506Nathan Bossart
nathandbossart@gmail.com
In reply to: Corey Huinker (#503)
3 attachment(s)
Re: Statistics Import and Export

On Mon, Mar 31, 2025 at 11:11:47AM -0400, Corey Huinker wrote:

In light of v11-0001 being committed as 4694aedf63bf, I've rebased the
remaining patches.

I spent the day preparing these for commit. A few notes:

* I've added a new prerequisite patch that skips the second WriteToc() call
for custom-format dumps that do not include data. After some testing and
code analysis, I haven't identified any examples where this produces
different output. This doesn't help much on its own, but it will become
rather important when we move the attribute statistics queries to happen
within WriteToc() in 0002.

* I was a little worried about the correctness of 0002 for dumps that run
the attribute statistics queries twice, but I couldn't identify any
problems here either.

* I removed a lot of miscellaneous refactoring that seemed unnecessary for
these patches. Let's move that to another patch set and keep these as
simple as possible.

* I made a small adjustment to the TOC scan restarting logic in
fetchAttributeStats(). Specifically, we now only allow the scan to
restart once for custom-format dumps that include data.

* While these patches help decrease pg_dump's memory footprint, I believe
pg_restore still reads the entire TOC into memory. That's not this patch
set's problem, but I think it's still an important consideration for the
bigger picture.

Regarding whether pg_dump should dump statistics by default, my current
thinking is that it shouldn't, but I think we _should_ have pg_upgrade
dump/restore statistics by default because that is arguably the most
important use-case. This is more a gut feeling than anything, so I reserve
the right to change my opinion.

My goal is to commit the attached patches on Friday morning, but of course
that is subject to change based on any feedback or objections that emerge
in the meantime.

--
nathan

Attachments:

v12n-0001-Skip-second-WriteToc-for-custom-format-dumps-wi.patchtext/plain; charset=us-asciiDownload
From 0256405dba245baa90c3fe35ee69e3cb1ce6253e Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 31 Mar 2025 10:44:26 -0500
Subject: [PATCH v12n 1/3] Skip second WriteToc() for custom-format dumps
 without data.

Presently, "pg_dump --format=custom" calls WriteToc() twice.  The
second call is intended to update the data offset information,
which allegedly makes parallel pg_restore significantly faster.
However, if we aren't dumping any data, this step accomplishes
nothing and can be skipped.  This is a preparatory optimization for
a follow-up commit that will move the queries for per-attribute
statistics to WriteToc() to save memory.

Discussion: https://postgr.es/m/Z9c1rbzZegYQTOQE%40nathan
---
 src/bin/pg_dump/pg_backup_custom.c | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup_custom.c b/src/bin/pg_dump/pg_backup_custom.c
index e44b887eb29..b971e3bc16e 100644
--- a/src/bin/pg_dump/pg_backup_custom.c
+++ b/src/bin/pg_dump/pg_backup_custom.c
@@ -755,9 +755,11 @@ _CloseArchive(ArchiveHandle *AH)
 		 * If possible, re-write the TOC in order to update the data offset
 		 * information.  This is not essential, as pg_restore can cope in most
 		 * cases without it; but it can make pg_restore significantly faster
-		 * in some situations (especially parallel restore).
+		 * in some situations (especially parallel restore).  We can skip this
+		 * step if we're not dumping any data.
 		 */
-		if (ctx->hasSeek &&
+		if (AH->public.dopt->dumpData &&
+			ctx->hasSeek &&
 			fseeko(AH->FH, tpos, SEEK_SET) == 0)
 			WriteToc(AH);
 	}
-- 
2.39.5 (Apple Git-154)

v12n-0002-pg_dump-Reduce-memory-usage-of-dumps-with-stati.patchtext/plain; charset=us-asciiDownload
From 6e41a1b2a175f7e9a859429e57c2ffb17ec9051d Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 31 Mar 2025 14:53:11 -0500
Subject: [PATCH v12n 2/3] pg_dump: Reduce memory usage of dumps with
 statistics.

Right now, pg_dump stores all generated commands for statistics in
memory.  These commands can be quite large and therefore can
significantly increase pg_dump's memory footprint.  To fix, wait
until we are about to write out the commands before generating
them, and be sure to free the commands after writing.  This is
implemented via a new defnDumper callback that works much like the
dataDumper one but is specially designed for TOC entries.

One drawback of this change is that custom dumps that include data
will run the statistics queries twice.  However, a follow-up commit
will add batching for these queries that our testing indicates
should greatly improve dump speed (even when compared to pg_dump
without this commit).

Author: Corey Huinker <corey.huinker@gmail.com>
Discussion: https://postgr.es/m/CADkLM%3Dc%2Br05srPy9w%2B-%2BnbmLEo15dKXYQ03Q_xyK%2BriJerigLQ%40mail.gmail.com
---
 src/bin/pg_dump/pg_backup.h          |  1 +
 src/bin/pg_dump/pg_backup_archiver.c | 35 ++++++++++++++++++----
 src/bin/pg_dump/pg_backup_archiver.h |  5 ++++
 src/bin/pg_dump/pg_dump.c            | 45 ++++++++++++++++++++--------
 4 files changed, 69 insertions(+), 17 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 658986de6f8..781f8fa1cc9 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -285,6 +285,7 @@ typedef int DumpId;
  * Function pointer prototypes for assorted callback methods.
  */
 
+typedef char *(*DefnDumperPtr) (Archive *AH, const void *userArg);
 typedef int (*DataDumperPtr) (Archive *AH, const void *userArg);
 
 typedef void (*SetupWorkerPtrType) (Archive *AH);
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 1d131e5a57d..4b73749b4e4 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -1266,6 +1266,9 @@ ArchiveEntry(Archive *AHX, CatalogId catalogId, DumpId dumpId,
 	newToc->dataDumperArg = opts->dumpArg;
 	newToc->hadDumper = opts->dumpFn ? true : false;
 
+	newToc->defnDumper = opts->defnFn;
+	newToc->defnDumperArg = opts->defnArg;
+
 	newToc->formatData = NULL;
 	newToc->dataLength = 0;
 
@@ -2621,7 +2624,17 @@ WriteToc(ArchiveHandle *AH)
 		WriteStr(AH, te->tag);
 		WriteStr(AH, te->desc);
 		WriteInt(AH, te->section);
-		WriteStr(AH, te->defn);
+
+		if (te->defnDumper)
+		{
+			char	   *defn = te->defnDumper((Archive *) AH, te->defnDumperArg);
+
+			WriteStr(AH, defn);
+			pg_free(defn);
+		}
+		else
+			WriteStr(AH, te->defn);
+
 		WriteStr(AH, te->dropStmt);
 		WriteStr(AH, te->copyStmt);
 		WriteStr(AH, te->namespace);
@@ -3849,7 +3862,7 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 
 	/*
 	 * Actually print the definition.  Normally we can just print the defn
-	 * string if any, but we have three special cases:
+	 * string if any, but we have four special cases:
 	 *
 	 * 1. A crude hack for suppressing AUTHORIZATION clause that old pg_dump
 	 * versions put into CREATE SCHEMA.  Don't mutate the variant for schema
@@ -3862,6 +3875,10 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 	 * 3. ACL LARGE OBJECTS entries need special processing because they
 	 * contain only one copy of the ACL GRANT/REVOKE commands, which we must
 	 * apply to each large object listed in the associated BLOB METADATA.
+	 *
+	 * 4. Entries with a defnDumper need to call it to generate the
+	 * definition.  This is primarily intended to provide a way to save memory
+	 * for objects that need a lot of it (e.g., statistics data).
 	 */
 	if (ropt->noOwner &&
 		strcmp(te->desc, "SCHEMA") == 0 && strncmp(te->defn, "--", 2) != 0)
@@ -3877,9 +3894,14 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 	{
 		IssueACLPerBlob(AH, te);
 	}
-	else if (te->defn && strlen(te->defn) > 0)
+	else if (te->defnDumper || (te->defn && strlen(te->defn) > 0))
 	{
-		ahprintf(AH, "%s\n\n", te->defn);
+		char	   *defn = te->defn;
+
+		if (te->defnDumper)
+			defn = te->defnDumper((Archive *) AH, te->defnDumperArg);
+
+		ahprintf(AH, "%s\n\n", defn);
 
 		/*
 		 * If the defn string contains multiple SQL commands, txn_size mode
@@ -3892,7 +3914,7 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 			strcmp(te->desc, "FUNCTION") != 0 &&
 			strcmp(te->desc, "PROCEDURE") != 0)
 		{
-			const char *p = te->defn;
+			const char *p = defn;
 			int			nsemis = 0;
 
 			while ((p = strchr(p, ';')) != NULL)
@@ -3903,6 +3925,9 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 			if (nsemis > 1)
 				AH->txnCount += nsemis - 1;
 		}
+
+		if (te->defnDumper)
+			pg_free(defn);
 	}
 
 	/*
diff --git a/src/bin/pg_dump/pg_backup_archiver.h b/src/bin/pg_dump/pg_backup_archiver.h
index a2064f471ed..fc65e0e34d3 100644
--- a/src/bin/pg_dump/pg_backup_archiver.h
+++ b/src/bin/pg_dump/pg_backup_archiver.h
@@ -368,6 +368,9 @@ struct _tocEntry
 	const void *dataDumperArg;	/* Arg for above routine */
 	void	   *formatData;		/* TOC Entry data specific to file format */
 
+	DefnDumperPtr defnDumper;	/* Routine to dump create statement */
+	const void *defnDumperArg;	/* Arg for above routine */
+
 	/* working state while dumping/restoring */
 	pgoff_t		dataLength;		/* item's data size; 0 if none or unknown */
 	int			reqs;			/* do we need schema and/or data of object
@@ -407,6 +410,8 @@ typedef struct _archiveOpts
 	int			nDeps;
 	DataDumperPtr dumpFn;
 	const void *dumpArg;
+	DefnDumperPtr defnFn;
+	const void *defnArg;
 } ArchiveOpts;
 #define ARCHIVE_OPTS(...) &(ArchiveOpts){__VA_ARGS__}
 /* Called to add a TOC entry */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 4ca34be230c..9fa2cb0672e 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -10560,13 +10560,17 @@ appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
 }
 
 /*
- * dumpRelationStats --
+ * dumpRelationStats_dumper --
+ *
+ * Generate command to import stats into the relation on the new database.
  *
- * Dump command to import stats into the relation on the new database.
+ * This routine is called by the Archiver when it wants the statistics to be
+ * dumped.
  */
-static void
-dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+static char *
+dumpRelationStats_dumper(Archive *fout, const void *userArg)
 {
+	const RelStatsInfo *rsinfo = (RelStatsInfo *) userArg;
 	const DumpableObject *dobj = &rsinfo->dobj;
 	PGresult   *res;
 	PQExpBuffer query;
@@ -10586,10 +10590,7 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 	int			i_range_length_histogram;
 	int			i_range_empty_frac;
 	int			i_range_bounds_histogram;
-
-	/* nothing to do if we are not dumping statistics */
-	if (!fout->dopt->dumpStatistics)
-		return;
+	char	   *ret;
 
 	query = createPQExpBuffer();
 	if (!fout->is_prepared[PREPQUERY_GETATTRIBUTESTATS])
@@ -10770,17 +10771,37 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 
 	PQclear(res);
 
+	destroyPQExpBuffer(query);
+	ret = out->data;
+	pg_free(out);
+	return ret;
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Make an ArchiveEntry for the relation statistics.  The Archiver will take
+ * care of gathering the statistics and generating the restore commands when
+ * they are needed.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	const DumpableObject *dobj = &rsinfo->dobj;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
 	ArchiveEntry(fout, nilCatalogId, createDumpId(),
 				 ARCHIVE_OPTS(.tag = dobj->name,
 							  .namespace = dobj->namespace->dobj.name,
 							  .description = "STATISTICS DATA",
 							  .section = rsinfo->section,
-							  .createStmt = out->data,
+							  .defnFn = dumpRelationStats_dumper,
+							  .defnArg = rsinfo,
 							  .deps = dobj->dependencies,
 							  .nDeps = dobj->nDeps));
-
-	destroyPQExpBuffer(out);
-	destroyPQExpBuffer(query);
 }
 
 /*
-- 
2.39.5 (Apple Git-154)

v12n-0003-pg_dump-Batch-queries-for-retrieving-attribute-.patchtext/plain; charset=us-asciiDownload
From 9b493c782685a05ed17f3c2dd3ff9673d8f5f6d4 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 31 Mar 2025 20:48:07 -0500
Subject: [PATCH v12n 3/3] pg_dump: Batch queries for retrieving attribute
 statistics.

Currently, pg_dump gathers attribute statistics with a query per
relation, which can cause pg_dump to take significantly longer,
especially when there are many tables.  This commit improves
matters by gathering attribute statistics for 64 relations at a
time.  Some simple testing showed this was the ideal batch size,
but performance may vary depending on workload.

To construct the next set of relations for the query, we scan
through the TOC list for relevant entries.  Ordinarily, we can stop
issuing queries once we reach the end of the list.  However,
custom-format dumps that include data run the statistics queries
twice (thanks to commit XXXXXXXXXX), so we allow a second pass in
that case.

This change increases the memory usage of pg_dump a bit, but that
isn't expected to be too egregious and is arguably well worth the
trade-off.

Author: Corey Huinker <corey.huinker@gmail.com>
Discussion: https://postgr.es/m/CADkLM%3Dc%2Br05srPy9w%2B-%2BnbmLEo15dKXYQ03Q_xyK%2BriJerigLQ%40mail.gmail.com
---
 src/bin/pg_dump/pg_dump.c        | 131 +++++++++++++++++++++++++++----
 src/tools/pgindent/typedefs.list |   1 +
 2 files changed, 115 insertions(+), 17 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 9fa2cb0672e..5506286f26f 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -143,6 +143,13 @@ typedef enum OidOptions
 	zeroAsNone = 4,
 } OidOptions;
 
+typedef struct
+{
+	PGresult   *res;			/* most recent fetchAttributeStats() result */
+	int			idx;			/* first un-consumed row of results */
+	TocEntry   *te;				/* next TOC entry to search */
+} AttStatsCache;
+
 /* global decls */
 static bool dosync = true;		/* Issue fsync() to make dump durable on disk. */
 
@@ -209,6 +216,11 @@ static int	nbinaryUpgradeClassOids = 0;
 static SequenceItem *sequences = NULL;
 static int	nsequences = 0;
 
+static AttStatsCache attStats;
+
+/* Maximum number of relations to fetch in a fetchAttributeStats() call. */
+#define MAX_ATTR_STATS_RELS 64
+
 /*
  * The default number of rows per INSERT when
  * --inserts is specified without --rows-per-insert
@@ -10559,6 +10571,81 @@ appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
 	appendPQExpBuffer(out, "::%s", argtype);
 }
 
+/*
+ * fetchAttributeStats --
+ *
+ * Fetch next batch of rows for getAttributeStats().
+ */
+static void
+fetchAttributeStats(Archive *fout)
+{
+	ArchiveHandle *AH = (ArchiveHandle *) fout;
+	PQExpBuffer nspnames = createPQExpBuffer();
+	PQExpBuffer relnames = createPQExpBuffer();
+	int			count = 0;
+	static bool restarted;
+
+	/* free last result set */
+	PQclear(attStats.res);
+	attStats.res = NULL;
+
+	/*
+	 * If we're just starting, set our TOC pointer.
+	 */
+	if (!attStats.te)
+		attStats.te = AH->toc->next;
+
+	/*
+	 * Restart the TOC scan once for custom-format dumps that include data.
+	 * This is necessary because we'll call WriteToc() twice in that case.
+	 */
+	if (!restarted && attStats.te == AH->toc &&
+		AH->format == archCustom && fout->dopt->dumpData)
+	{
+		attStats.te = AH->toc->next;
+		restarted = true;
+	}
+
+	/*
+	 * Walk ahead looking for stats entries that are active in this section.
+	 */
+	while (attStats.te != AH->toc && count < MAX_ATTR_STATS_RELS)
+	{
+		if (attStats.te->reqs &&
+			strcmp(attStats.te->desc, "STATISTICS DATA") == 0)
+		{
+			RelStatsInfo *rsinfo = (RelStatsInfo *) attStats.te->defnDumperArg;
+
+			appendPQExpBuffer(nspnames, "%s%s", count ? "," : "",
+							  fmtId(rsinfo->dobj.namespace->dobj.name));
+			appendPQExpBuffer(relnames, "%s%s", count ? "," : "",
+							  fmtId(rsinfo->dobj.name));
+			count++;
+		}
+
+		attStats.te = attStats.te->next;
+	}
+
+	/*
+	 * Execute the query for the next batch of relations.
+	 */
+	if (count > 0)
+	{
+		PQExpBuffer query = createPQExpBuffer();
+
+		appendPQExpBuffer(query, "EXECUTE getAttributeStats("
+						  "'{%s}'::pg_catalog.name[],"
+						  "'{%s}'::pg_catalog.name[])",
+						  nspnames->data, relnames->data);
+		attStats.res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+		attStats.idx = 0;
+		destroyPQExpBuffer(query);
+	}
+
+	destroyPQExpBuffer(nspnames);
+	destroyPQExpBuffer(relnames);
+}
+
 /*
  * dumpRelationStats_dumper --
  *
@@ -10575,6 +10662,8 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 	PGresult   *res;
 	PQExpBuffer query;
 	PQExpBuffer out;
+	int			i_schemaname;
+	int			i_tablename;
 	int			i_attname;
 	int			i_inherited;
 	int			i_null_frac;
@@ -10596,8 +10685,8 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 	if (!fout->is_prepared[PREPQUERY_GETATTRIBUTESTATS])
 	{
 		appendPQExpBufferStr(query,
-							 "PREPARE getAttributeStats(pg_catalog.name, pg_catalog.name) AS\n"
-							 "SELECT s.attname, s.inherited, "
+							 "PREPARE getAttributeStats(pg_catalog.name[], pg_catalog.name[]) AS\n"
+							 "SELECT s.schemaname, s.tablename, s.attname, s.inherited, "
 							 "s.null_frac, s.avg_width, s.n_distinct, "
 							 "s.most_common_vals, s.most_common_freqs, "
 							 "s.histogram_bounds, s.correlation, "
@@ -10617,9 +10706,11 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 
 		appendPQExpBufferStr(query,
 							 "FROM pg_catalog.pg_stats s "
-							 "WHERE s.schemaname = $1 "
-							 "AND s.tablename = $2 "
-							 "ORDER BY s.attname, s.inherited");
+							 "JOIN unnest($1, $2) WITH ORDINALITY AS u (schemaname, tablename, ord) "
+							 "ON s.schemaname = u.schemaname "
+							 "AND s.tablename = u.tablename "
+							 "WHERE s.tablename = ANY($2) "
+							 "ORDER BY u.ord, s.attname, s.inherited");
 
 		ExecuteSqlStatement(fout, query->data);
 
@@ -10649,16 +10740,16 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 
 	appendPQExpBufferStr(out, "\n);\n");
 
+	/*
+	 * If the attribute stats cache is uninitialized or exhausted, fetch the
+	 * next batch of attributes statistics.
+	 */
+	if (attStats.idx >= PQntuples(attStats.res))
+		fetchAttributeStats(fout);
+	res = attStats.res;
 
-	/* fetch attribute stats */
-	appendPQExpBufferStr(query, "EXECUTE getAttributeStats(");
-	appendStringLiteralAH(query, dobj->namespace->dobj.name, fout);
-	appendPQExpBufferStr(query, ", ");
-	appendStringLiteralAH(query, dobj->name, fout);
-	appendPQExpBufferStr(query, ");");
-
-	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
-
+	i_schemaname = PQfnumber(res, "schemaname");
+	i_tablename = PQfnumber(res, "tablename");
 	i_attname = PQfnumber(res, "attname");
 	i_inherited = PQfnumber(res, "inherited");
 	i_null_frac = PQfnumber(res, "null_frac");
@@ -10676,9 +10767,17 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 	i_range_bounds_histogram = PQfnumber(res, "range_bounds_histogram");
 
 	/* restore attribute stats */
-	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	for (; attStats.idx < PQntuples(res); attStats.idx++)
 	{
 		const char *attname;
+		int			rownum = attStats.idx;
+
+		/*
+		 * Stop if the next stat row in our cache isn't for this relation.
+		 */
+		if (strcmp(dobj->name, PQgetvalue(res, rownum, i_tablename)) != 0 ||
+			strcmp(dobj->namespace->dobj.name, PQgetvalue(res, rownum, i_schemaname)) != 0)
+			break;
 
 		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
 		appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
@@ -10769,8 +10868,6 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 		appendPQExpBufferStr(out, "\n);\n");
 	}
 
-	PQclear(res);
-
 	destroyPQExpBuffer(query);
 	ret = out->data;
 	pg_free(out);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index b66cecd8799..2719a6642ad 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -162,6 +162,7 @@ AsyncQueueEntry
 AsyncRequest
 ATAlterConstraint
 AttInMetadata
+AttStatsCache
 AttStatsSlot
 AttoptCacheEntry
 AttoptCacheKey
-- 
2.39.5 (Apple Git-154)

#507Robert Treat
rob@xzilla.net
In reply to: Nathan Bossart (#506)
Re: Statistics Import and Export

On Mon, Mar 31, 2025 at 10:33 PM Nathan Bossart
<nathandbossart@gmail.com> wrote:

On Mon, Mar 31, 2025 at 11:11:47AM -0400, Corey Huinker wrote:
Regarding whether pg_dump should dump statistics by default, my current
thinking is that it shouldn't, but I think we _should_ have pg_upgrade
dump/restore statistics by default because that is arguably the most
important use-case. This is more a gut feeling than anything, so I reserve
the right to change my opinion.

I did some mental exercises on a number of different use cases and
scenarios (pagila work, pgextractor type stuff, backups, etc...) and I
couldn't come up with any strong arguments against including the stats
by default, generally because I think when your process needs to care
about the output of pg_dump, it seems like most cases require enough
specificity that this wouldn't actually break that.

Still, I am sympathetic to Greg's earlier concerns on the topic, but
would also agree it seems like a clear win for pg_upgrade, so I think
our gut feelings might actually be aligned on this one ;-)

Robert Treat
https://xzilla.net

#508Robert Haas
robertmhaas@gmail.com
In reply to: Jeff Davis (#505)
Re: Statistics Import and Export

On Mon, Mar 31, 2025 at 6:04 PM Jeff Davis <pgsql@j-davis.com> wrote:

On Mon, 2025-03-31 at 13:39 -0400, Robert Haas wrote:

+1. I think I said this before, but I don't think it's correct to
regard the statistics as part of the database. It's great for
pg_upgrade to preserve them, but I think doing so for a regular dump
should be opt-in.

I'm confused about the timing of this message -- we already have an
Open Item for 18 to make this decision. After commit bde2fb797a,
changing the default is a one-line change, so there's no technical
problem.

I thought the general plan was to decide during beta. Would you like to
make the decision now for some reason?

I don't think I was aware of the open item; I was just catching up on
email. But I also don't really see the value of waiting until beta to
make this decision. I seriously doubt that my opinion is going to
change. Maybe other people's will, though: I can only speak for
myself.

--
Robert Haas
EDB: http://www.enterprisedb.com

#509Nathan Bossart
nathandbossart@gmail.com
In reply to: Nathan Bossart (#506)
3 attachment(s)
Re: Statistics Import and Export

On Mon, Mar 31, 2025 at 09:33:15PM -0500, Nathan Bossart wrote:

My goal is to commit the attached patches on Friday morning, but of course
that is subject to change based on any feedback or objections that emerge
in the meantime.

I spent some more time polishing these patches this morning. There should
be no functional differences, but I did restructure 0003 to make it even
simpler.

--
nathan

Attachments:

v12n2-0001-Skip-second-WriteToc-for-custom-format-dumps-w.patchtext/plain; charset=us-asciiDownload
From 85e7b507629b096275d3a7386149876af7466f74 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 31 Mar 2025 10:44:26 -0500
Subject: [PATCH v12n2 1/3] Skip second WriteToc() for custom-format dumps
 without data.

Presently, "pg_dump --format=custom" calls WriteToc() twice.  The
second call is intended to update the data offset information,
which allegedly makes parallel pg_restore significantly faster.
However, if we aren't dumping any data, this step accomplishes
nothing and can be skipped.  This is a preparatory optimization for
a follow-up commit that will move the queries for attribute
statistics to WriteToc() to save memory.

Discussion: https://postgr.es/m/Z9c1rbzZegYQTOQE%40nathan
---
 src/bin/pg_dump/pg_backup_custom.c | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup_custom.c b/src/bin/pg_dump/pg_backup_custom.c
index e44b887eb29..b971e3bc16e 100644
--- a/src/bin/pg_dump/pg_backup_custom.c
+++ b/src/bin/pg_dump/pg_backup_custom.c
@@ -755,9 +755,11 @@ _CloseArchive(ArchiveHandle *AH)
 		 * If possible, re-write the TOC in order to update the data offset
 		 * information.  This is not essential, as pg_restore can cope in most
 		 * cases without it; but it can make pg_restore significantly faster
-		 * in some situations (especially parallel restore).
+		 * in some situations (especially parallel restore).  We can skip this
+		 * step if we're not dumping any data.
 		 */
-		if (ctx->hasSeek &&
+		if (AH->public.dopt->dumpData &&
+			ctx->hasSeek &&
 			fseeko(AH->FH, tpos, SEEK_SET) == 0)
 			WriteToc(AH);
 	}
-- 
2.39.5 (Apple Git-154)

v12n2-0002-pg_dump-Reduce-memory-usage-of-dumps-with-stat.patchtext/plain; charset=us-asciiDownload
From df47e0bcafddce7a2ace0b52c139ba948535a2f0 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 31 Mar 2025 14:53:11 -0500
Subject: [PATCH v12n2 2/3] pg_dump: Reduce memory usage of dumps with
 statistics.

Right now, pg_dump stores all generated commands for statistics in
memory.  These commands can be quite large and therefore can
significantly increase pg_dump's memory footprint.  To fix, wait
until we are about to write out the commands before generating
them, and be sure to free the commands after writing.  This is
implemented via a new defnDumper callback that works much like the
dataDumper one but is specially designed for TOC entries.

One drawback of this change is that custom dumps that include data
will run the attribute statistics queries twice.  However, a
follow-up commit will add batching for these queries that our
testing indicates should improve dump speed (even when compared to
pg_dump before this commit).

Author: Corey Huinker <corey.huinker@gmail.com>
Discussion: https://postgr.es/m/CADkLM%3Dc%2Br05srPy9w%2B-%2BnbmLEo15dKXYQ03Q_xyK%2BriJerigLQ%40mail.gmail.com
---
 src/bin/pg_dump/pg_backup.h          |  1 +
 src/bin/pg_dump/pg_backup_archiver.c | 35 ++++++++++++++++++----
 src/bin/pg_dump/pg_backup_archiver.h |  5 ++++
 src/bin/pg_dump/pg_dump.c            | 44 ++++++++++++++++++++--------
 4 files changed, 68 insertions(+), 17 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 658986de6f8..781f8fa1cc9 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -285,6 +285,7 @@ typedef int DumpId;
  * Function pointer prototypes for assorted callback methods.
  */
 
+typedef char *(*DefnDumperPtr) (Archive *AH, const void *userArg);
 typedef int (*DataDumperPtr) (Archive *AH, const void *userArg);
 
 typedef void (*SetupWorkerPtrType) (Archive *AH);
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 1d131e5a57d..4b73749b4e4 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -1266,6 +1266,9 @@ ArchiveEntry(Archive *AHX, CatalogId catalogId, DumpId dumpId,
 	newToc->dataDumperArg = opts->dumpArg;
 	newToc->hadDumper = opts->dumpFn ? true : false;
 
+	newToc->defnDumper = opts->defnFn;
+	newToc->defnDumperArg = opts->defnArg;
+
 	newToc->formatData = NULL;
 	newToc->dataLength = 0;
 
@@ -2621,7 +2624,17 @@ WriteToc(ArchiveHandle *AH)
 		WriteStr(AH, te->tag);
 		WriteStr(AH, te->desc);
 		WriteInt(AH, te->section);
-		WriteStr(AH, te->defn);
+
+		if (te->defnDumper)
+		{
+			char	   *defn = te->defnDumper((Archive *) AH, te->defnDumperArg);
+
+			WriteStr(AH, defn);
+			pg_free(defn);
+		}
+		else
+			WriteStr(AH, te->defn);
+
 		WriteStr(AH, te->dropStmt);
 		WriteStr(AH, te->copyStmt);
 		WriteStr(AH, te->namespace);
@@ -3849,7 +3862,7 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 
 	/*
 	 * Actually print the definition.  Normally we can just print the defn
-	 * string if any, but we have three special cases:
+	 * string if any, but we have four special cases:
 	 *
 	 * 1. A crude hack for suppressing AUTHORIZATION clause that old pg_dump
 	 * versions put into CREATE SCHEMA.  Don't mutate the variant for schema
@@ -3862,6 +3875,10 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 	 * 3. ACL LARGE OBJECTS entries need special processing because they
 	 * contain only one copy of the ACL GRANT/REVOKE commands, which we must
 	 * apply to each large object listed in the associated BLOB METADATA.
+	 *
+	 * 4. Entries with a defnDumper need to call it to generate the
+	 * definition.  This is primarily intended to provide a way to save memory
+	 * for objects that need a lot of it (e.g., statistics data).
 	 */
 	if (ropt->noOwner &&
 		strcmp(te->desc, "SCHEMA") == 0 && strncmp(te->defn, "--", 2) != 0)
@@ -3877,9 +3894,14 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 	{
 		IssueACLPerBlob(AH, te);
 	}
-	else if (te->defn && strlen(te->defn) > 0)
+	else if (te->defnDumper || (te->defn && strlen(te->defn) > 0))
 	{
-		ahprintf(AH, "%s\n\n", te->defn);
+		char	   *defn = te->defn;
+
+		if (te->defnDumper)
+			defn = te->defnDumper((Archive *) AH, te->defnDumperArg);
+
+		ahprintf(AH, "%s\n\n", defn);
 
 		/*
 		 * If the defn string contains multiple SQL commands, txn_size mode
@@ -3892,7 +3914,7 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 			strcmp(te->desc, "FUNCTION") != 0 &&
 			strcmp(te->desc, "PROCEDURE") != 0)
 		{
-			const char *p = te->defn;
+			const char *p = defn;
 			int			nsemis = 0;
 
 			while ((p = strchr(p, ';')) != NULL)
@@ -3903,6 +3925,9 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 			if (nsemis > 1)
 				AH->txnCount += nsemis - 1;
 		}
+
+		if (te->defnDumper)
+			pg_free(defn);
 	}
 
 	/*
diff --git a/src/bin/pg_dump/pg_backup_archiver.h b/src/bin/pg_dump/pg_backup_archiver.h
index a2064f471ed..fc65e0e34d3 100644
--- a/src/bin/pg_dump/pg_backup_archiver.h
+++ b/src/bin/pg_dump/pg_backup_archiver.h
@@ -368,6 +368,9 @@ struct _tocEntry
 	const void *dataDumperArg;	/* Arg for above routine */
 	void	   *formatData;		/* TOC Entry data specific to file format */
 
+	DefnDumperPtr defnDumper;	/* Routine to dump create statement */
+	const void *defnDumperArg;	/* Arg for above routine */
+
 	/* working state while dumping/restoring */
 	pgoff_t		dataLength;		/* item's data size; 0 if none or unknown */
 	int			reqs;			/* do we need schema and/or data of object
@@ -407,6 +410,8 @@ typedef struct _archiveOpts
 	int			nDeps;
 	DataDumperPtr dumpFn;
 	const void *dumpArg;
+	DefnDumperPtr defnFn;
+	const void *defnArg;
 } ArchiveOpts;
 #define ARCHIVE_OPTS(...) &(ArchiveOpts){__VA_ARGS__}
 /* Called to add a TOC entry */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 4ca34be230c..46fb70e0a8b 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -10560,13 +10560,16 @@ appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
 }
 
 /*
- * dumpRelationStats --
+ * dumpRelationStats_dumper --
  *
- * Dump command to import stats into the relation on the new database.
+ * Generate command to import stats into the relation on the new database.
+ * This routine is called by the Archiver when it wants the statistics to be
+ * dumped.
  */
-static void
-dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+static char *
+dumpRelationStats_dumper(Archive *fout, const void *userArg)
 {
+	const RelStatsInfo *rsinfo = (RelStatsInfo *) userArg;
 	const DumpableObject *dobj = &rsinfo->dobj;
 	PGresult   *res;
 	PQExpBuffer query;
@@ -10586,10 +10589,7 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 	int			i_range_length_histogram;
 	int			i_range_empty_frac;
 	int			i_range_bounds_histogram;
-
-	/* nothing to do if we are not dumping statistics */
-	if (!fout->dopt->dumpStatistics)
-		return;
+	char	   *ret;
 
 	query = createPQExpBuffer();
 	if (!fout->is_prepared[PREPQUERY_GETATTRIBUTESTATS])
@@ -10770,17 +10770,37 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 
 	PQclear(res);
 
+	destroyPQExpBuffer(query);
+	ret = out->data;
+	pg_free(out);
+	return ret;
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Make an ArchiveEntry for the relation statistics.  The Archiver will take
+ * care of gathering the statistics and generating the restore commands when
+ * they are needed.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	const DumpableObject *dobj = &rsinfo->dobj;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
 	ArchiveEntry(fout, nilCatalogId, createDumpId(),
 				 ARCHIVE_OPTS(.tag = dobj->name,
 							  .namespace = dobj->namespace->dobj.name,
 							  .description = "STATISTICS DATA",
 							  .section = rsinfo->section,
-							  .createStmt = out->data,
+							  .defnFn = dumpRelationStats_dumper,
+							  .defnArg = rsinfo,
 							  .deps = dobj->dependencies,
 							  .nDeps = dobj->nDeps));
-
-	destroyPQExpBuffer(out);
-	destroyPQExpBuffer(query);
 }
 
 /*
-- 
2.39.5 (Apple Git-154)

v12n2-0003-pg_dump-Retrieve-attribute-statistics-in-batch.patchtext/plain; charset=us-asciiDownload
From 5f526376ac17b3e33a34725c035f9d8cc7adb12e Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Tue, 1 Apr 2025 11:35:19 -0500
Subject: [PATCH v12n2 3/3] pg_dump: Retrieve attribute statistics in batches.

Currently, pg_dump gathers attribute statistics with a query per
relation, which can cause pg_dump to take significantly longer,
especially when there are many tables.  This commit improves
matters by gathering attribute statistics for 64 relations at a
time.  Some simple testing showed this was the ideal batch size,
but performance may vary depending on workload.

To construct the next set of relations for the query, we scan
through the TOC list for relevant entries.  Ordinarily, we can stop
issuing queries once we reach the end of the list.  However,
custom-format dumps that include data run the statistics queries
twice (thanks to commit XXXXXXXXXX), so we allow a second pass in
that case.  Our tests showed that batching more than makes up for
any losses from running the queries twice.

This change increases the memory usage of pg_dump a bit, but that
isn't expected to be too egregious and is arguably well worth the
trade-off.

Author: Corey Huinker <corey.huinker@gmail.com>
Discussion: https://postgr.es/m/CADkLM%3Dc%2Br05srPy9w%2B-%2BnbmLEo15dKXYQ03Q_xyK%2BriJerigLQ%40mail.gmail.com
---
 src/bin/pg_dump/pg_dump.c | 111 +++++++++++++++++++++++++++++++-------
 1 file changed, 93 insertions(+), 18 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 46fb70e0a8b..5dfa12e2ce9 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -209,6 +209,9 @@ static int	nbinaryUpgradeClassOids = 0;
 static SequenceItem *sequences = NULL;
 static int	nsequences = 0;
 
+/* Maximum number of relations to fetch in a fetchAttributeStats() call. */
+#define MAX_ATTR_STATS_RELS 64
+
 /*
  * The default number of rows per INSERT when
  * --inserts is specified without --rows-per-insert
@@ -10559,6 +10562,70 @@ appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
 	appendPQExpBuffer(out, "::%s", argtype);
 }
 
+/*
+ * fetchAttributeStats --
+ *
+ * Fetch next batch of rows for getAttributeStats().
+ */
+static PGresult *
+fetchAttributeStats(Archive *fout)
+{
+	ArchiveHandle *AH = (ArchiveHandle *) fout;
+	PQExpBuffer nspnames = createPQExpBuffer();
+	PQExpBuffer relnames = createPQExpBuffer();
+	int			count = 0;
+	PGresult   *res = NULL;
+	static bool restarted;
+	static TocEntry *te;
+
+	/* If we're just starting, set our TOC pointer. */
+	if (!te)
+		te = AH->toc->next;
+
+	/*
+	 * Restart the TOC scan once for custom-format dumps that include data.
+	 * This is necessary because we'll call WriteToc() twice in that case.
+	 */
+	if (!restarted && te == AH->toc &&
+		AH->format == archCustom && fout->dopt->dumpData)
+	{
+		te = AH->toc->next;
+		restarted = true;
+	}
+
+	/* Scan the TOC for the next set of relevant stats entries. */
+	for (; te != AH->toc && count < MAX_ATTR_STATS_RELS; te = te->next)
+	{
+		if (te->reqs && strcmp(te->desc, "STATISTICS DATA") == 0)
+		{
+			RelStatsInfo *rsinfo = (RelStatsInfo *) te->defnDumperArg;
+
+			appendPQExpBuffer(nspnames, "%s%s", count ? "," : "",
+							  fmtId(rsinfo->dobj.namespace->dobj.name));
+			appendPQExpBuffer(relnames, "%s%s", count ? "," : "",
+							  fmtId(rsinfo->dobj.name));
+			count++;
+		}
+	}
+
+	/* Execute the query for the next batch of relations. */
+	if (count > 0)
+	{
+		PQExpBuffer query = createPQExpBuffer();
+
+		appendPQExpBuffer(query, "EXECUTE getAttributeStats("
+						  "'{%s}'::pg_catalog.name[],"
+						  "'{%s}'::pg_catalog.name[])",
+						  nspnames->data, relnames->data);
+		res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+		destroyPQExpBuffer(query);
+	}
+
+	destroyPQExpBuffer(nspnames);
+	destroyPQExpBuffer(relnames);
+	return res;
+}
+
 /*
  * dumpRelationStats_dumper --
  *
@@ -10571,9 +10638,12 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 {
 	const RelStatsInfo *rsinfo = (RelStatsInfo *) userArg;
 	const DumpableObject *dobj = &rsinfo->dobj;
-	PGresult   *res;
+	static PGresult *res;
+	static int	rownum;
 	PQExpBuffer query;
 	PQExpBuffer out;
+	int			i_schemaname;
+	int			i_tablename;
 	int			i_attname;
 	int			i_inherited;
 	int			i_null_frac;
@@ -10595,8 +10665,8 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 	if (!fout->is_prepared[PREPQUERY_GETATTRIBUTESTATS])
 	{
 		appendPQExpBufferStr(query,
-							 "PREPARE getAttributeStats(pg_catalog.name, pg_catalog.name) AS\n"
-							 "SELECT s.attname, s.inherited, "
+							 "PREPARE getAttributeStats(pg_catalog.name[], pg_catalog.name[]) AS\n"
+							 "SELECT s.schemaname, s.tablename, s.attname, s.inherited, "
 							 "s.null_frac, s.avg_width, s.n_distinct, "
 							 "s.most_common_vals, s.most_common_freqs, "
 							 "s.histogram_bounds, s.correlation, "
@@ -10616,9 +10686,11 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 
 		appendPQExpBufferStr(query,
 							 "FROM pg_catalog.pg_stats s "
-							 "WHERE s.schemaname = $1 "
-							 "AND s.tablename = $2 "
-							 "ORDER BY s.attname, s.inherited");
+							 "JOIN unnest($1, $2) WITH ORDINALITY AS u (schemaname, tablename, ord) "
+							 "ON s.schemaname = u.schemaname "
+							 "AND s.tablename = u.tablename "
+							 "WHERE s.tablename = ANY($2) "
+							 "ORDER BY u.ord, s.attname, s.inherited");
 
 		ExecuteSqlStatement(fout, query->data);
 
@@ -10648,16 +10720,16 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 
 	appendPQExpBufferStr(out, "\n);\n");
 
+	/* Fetch the next batch of attribute statistics if needed. */
+	if (rownum >= PQntuples(res))
+	{
+		PQclear(res);
+		res = fetchAttributeStats(fout);
+		rownum = 0;
+	}
 
-	/* fetch attribute stats */
-	appendPQExpBufferStr(query, "EXECUTE getAttributeStats(");
-	appendStringLiteralAH(query, dobj->namespace->dobj.name, fout);
-	appendPQExpBufferStr(query, ", ");
-	appendStringLiteralAH(query, dobj->name, fout);
-	appendPQExpBufferStr(query, ");");
-
-	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
-
+	i_schemaname = PQfnumber(res, "schemaname");
+	i_tablename = PQfnumber(res, "tablename");
 	i_attname = PQfnumber(res, "attname");
 	i_inherited = PQfnumber(res, "inherited");
 	i_null_frac = PQfnumber(res, "null_frac");
@@ -10675,10 +10747,15 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 	i_range_bounds_histogram = PQfnumber(res, "range_bounds_histogram");
 
 	/* restore attribute stats */
-	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	for (; rownum < PQntuples(res); rownum++)
 	{
 		const char *attname;
 
+		/* Stop if the next stat row in our cache isn't for this relation. */
+		if (strcmp(dobj->name, PQgetvalue(res, rownum, i_tablename)) != 0 ||
+			strcmp(dobj->namespace->dobj.name, PQgetvalue(res, rownum, i_schemaname)) != 0)
+			break;
+
 		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
 		appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
 						  fout->remoteVersion);
@@ -10768,8 +10845,6 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 		appendPQExpBufferStr(out, "\n);\n");
 	}
 
-	PQclear(res);
-
 	destroyPQExpBuffer(query);
 	ret = out->data;
 	pg_free(out);
-- 
2.39.5 (Apple Git-154)

#510Nathan Bossart
nathandbossart@gmail.com
In reply to: Nathan Bossart (#509)
3 attachment(s)
Re: Statistics Import and Export

On Tue, Apr 01, 2025 at 01:20:30PM -0500, Nathan Bossart wrote:

On Mon, Mar 31, 2025 at 09:33:15PM -0500, Nathan Bossart wrote:

My goal is to commit the attached patches on Friday morning, but of course
that is subject to change based on any feedback or objections that emerge
in the meantime.

I spent some more time polishing these patches this morning. There should
be no functional differences, but I did restructure 0003 to make it even
simpler.

Apologies for the noise. I noticed one more way to simplify 0002. As
before, there should be no functional differences.

--
nathan

Attachments:

v12n3-0001-Skip-second-WriteToc-for-custom-format-dumps-w.patchtext/plain; charset=us-asciiDownload
From 85e7b507629b096275d3a7386149876af7466f74 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 31 Mar 2025 10:44:26 -0500
Subject: [PATCH v12n3 1/3] Skip second WriteToc() for custom-format dumps
 without data.

Presently, "pg_dump --format=custom" calls WriteToc() twice.  The
second call is intended to update the data offset information,
which allegedly makes parallel pg_restore significantly faster.
However, if we aren't dumping any data, this step accomplishes
nothing and can be skipped.  This is a preparatory optimization for
a follow-up commit that will move the queries for attribute
statistics to WriteToc() to save memory.

Discussion: https://postgr.es/m/Z9c1rbzZegYQTOQE%40nathan
---
 src/bin/pg_dump/pg_backup_custom.c | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup_custom.c b/src/bin/pg_dump/pg_backup_custom.c
index e44b887eb29..b971e3bc16e 100644
--- a/src/bin/pg_dump/pg_backup_custom.c
+++ b/src/bin/pg_dump/pg_backup_custom.c
@@ -755,9 +755,11 @@ _CloseArchive(ArchiveHandle *AH)
 		 * If possible, re-write the TOC in order to update the data offset
 		 * information.  This is not essential, as pg_restore can cope in most
 		 * cases without it; but it can make pg_restore significantly faster
-		 * in some situations (especially parallel restore).
+		 * in some situations (especially parallel restore).  We can skip this
+		 * step if we're not dumping any data.
 		 */
-		if (ctx->hasSeek &&
+		if (AH->public.dopt->dumpData &&
+			ctx->hasSeek &&
 			fseeko(AH->FH, tpos, SEEK_SET) == 0)
 			WriteToc(AH);
 	}
-- 
2.39.5 (Apple Git-154)

v12n3-0002-pg_dump-Reduce-memory-usage-of-dumps-with-stat.patchtext/plain; charset=us-asciiDownload
From 3804a6e489601bb3ac6770c413c7e9d3865ce524 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 31 Mar 2025 14:53:11 -0500
Subject: [PATCH v12n3 2/3] pg_dump: Reduce memory usage of dumps with
 statistics.

Right now, pg_dump stores all generated commands for statistics in
memory.  These commands can be quite large and therefore can
significantly increase pg_dump's memory footprint.  To fix, wait
until we are about to write out the commands before generating
them, and be sure to free the commands after writing.  This is
implemented via a new defnDumper callback that works much like the
dataDumper one but is specially designed for TOC entries.

One drawback of this change is that custom dumps that include data
will run the attribute statistics queries twice.  However, a
follow-up commit will add batching for these queries that our
testing indicates should improve dump speed (even when compared to
pg_dump before this commit).

Author: Corey Huinker <corey.huinker@gmail.com>
Discussion: https://postgr.es/m/CADkLM%3Dc%2Br05srPy9w%2B-%2BnbmLEo15dKXYQ03Q_xyK%2BriJerigLQ%40mail.gmail.com
---
 src/bin/pg_dump/pg_backup.h          |  1 +
 src/bin/pg_dump/pg_backup_archiver.c | 28 ++++++++++++++++--
 src/bin/pg_dump/pg_backup_archiver.h |  5 ++++
 src/bin/pg_dump/pg_dump.c            | 44 ++++++++++++++++++++--------
 4 files changed, 64 insertions(+), 14 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 658986de6f8..781f8fa1cc9 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -285,6 +285,7 @@ typedef int DumpId;
  * Function pointer prototypes for assorted callback methods.
  */
 
+typedef char *(*DefnDumperPtr) (Archive *AH, const void *userArg);
 typedef int (*DataDumperPtr) (Archive *AH, const void *userArg);
 
 typedef void (*SetupWorkerPtrType) (Archive *AH);
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 1d131e5a57d..bb5a02415f2 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -1266,6 +1266,9 @@ ArchiveEntry(Archive *AHX, CatalogId catalogId, DumpId dumpId,
 	newToc->dataDumperArg = opts->dumpArg;
 	newToc->hadDumper = opts->dumpFn ? true : false;
 
+	newToc->defnDumper = opts->defnFn;
+	newToc->defnDumperArg = opts->defnArg;
+
 	newToc->formatData = NULL;
 	newToc->dataLength = 0;
 
@@ -2621,7 +2624,17 @@ WriteToc(ArchiveHandle *AH)
 		WriteStr(AH, te->tag);
 		WriteStr(AH, te->desc);
 		WriteInt(AH, te->section);
-		WriteStr(AH, te->defn);
+
+		if (te->defnDumper)
+		{
+			char	   *defn = te->defnDumper((Archive *) AH, te->defnDumperArg);
+
+			WriteStr(AH, defn);
+			pg_free(defn);
+		}
+		else
+			WriteStr(AH, te->defn);
+
 		WriteStr(AH, te->dropStmt);
 		WriteStr(AH, te->copyStmt);
 		WriteStr(AH, te->namespace);
@@ -3849,7 +3862,7 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 
 	/*
 	 * Actually print the definition.  Normally we can just print the defn
-	 * string if any, but we have three special cases:
+	 * string if any, but we have four special cases:
 	 *
 	 * 1. A crude hack for suppressing AUTHORIZATION clause that old pg_dump
 	 * versions put into CREATE SCHEMA.  Don't mutate the variant for schema
@@ -3862,6 +3875,10 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 	 * 3. ACL LARGE OBJECTS entries need special processing because they
 	 * contain only one copy of the ACL GRANT/REVOKE commands, which we must
 	 * apply to each large object listed in the associated BLOB METADATA.
+	 *
+	 * 4. Entries with a defnDumper need to call it to generate the
+	 * definition.  This is primarily intended to provide a way to save memory
+	 * for objects that need a lot of it (e.g., statistics data).
 	 */
 	if (ropt->noOwner &&
 		strcmp(te->desc, "SCHEMA") == 0 && strncmp(te->defn, "--", 2) != 0)
@@ -3877,6 +3894,13 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 	{
 		IssueACLPerBlob(AH, te);
 	}
+	else if (te->defnDumper)
+	{
+		char	   *defn = te->defnDumper((Archive *) AH, te->defnDumperArg);
+
+		ahprintf(AH, "%s\n\n", defn);
+		pg_free(defn);
+	}
 	else if (te->defn && strlen(te->defn) > 0)
 	{
 		ahprintf(AH, "%s\n\n", te->defn);
diff --git a/src/bin/pg_dump/pg_backup_archiver.h b/src/bin/pg_dump/pg_backup_archiver.h
index a2064f471ed..fc65e0e34d3 100644
--- a/src/bin/pg_dump/pg_backup_archiver.h
+++ b/src/bin/pg_dump/pg_backup_archiver.h
@@ -368,6 +368,9 @@ struct _tocEntry
 	const void *dataDumperArg;	/* Arg for above routine */
 	void	   *formatData;		/* TOC Entry data specific to file format */
 
+	DefnDumperPtr defnDumper;	/* Routine to dump create statement */
+	const void *defnDumperArg;	/* Arg for above routine */
+
 	/* working state while dumping/restoring */
 	pgoff_t		dataLength;		/* item's data size; 0 if none or unknown */
 	int			reqs;			/* do we need schema and/or data of object
@@ -407,6 +410,8 @@ typedef struct _archiveOpts
 	int			nDeps;
 	DataDumperPtr dumpFn;
 	const void *dumpArg;
+	DefnDumperPtr defnFn;
+	const void *defnArg;
 } ArchiveOpts;
 #define ARCHIVE_OPTS(...) &(ArchiveOpts){__VA_ARGS__}
 /* Called to add a TOC entry */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 4ca34be230c..46fb70e0a8b 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -10560,13 +10560,16 @@ appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
 }
 
 /*
- * dumpRelationStats --
+ * dumpRelationStats_dumper --
  *
- * Dump command to import stats into the relation on the new database.
+ * Generate command to import stats into the relation on the new database.
+ * This routine is called by the Archiver when it wants the statistics to be
+ * dumped.
  */
-static void
-dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+static char *
+dumpRelationStats_dumper(Archive *fout, const void *userArg)
 {
+	const RelStatsInfo *rsinfo = (RelStatsInfo *) userArg;
 	const DumpableObject *dobj = &rsinfo->dobj;
 	PGresult   *res;
 	PQExpBuffer query;
@@ -10586,10 +10589,7 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 	int			i_range_length_histogram;
 	int			i_range_empty_frac;
 	int			i_range_bounds_histogram;
-
-	/* nothing to do if we are not dumping statistics */
-	if (!fout->dopt->dumpStatistics)
-		return;
+	char	   *ret;
 
 	query = createPQExpBuffer();
 	if (!fout->is_prepared[PREPQUERY_GETATTRIBUTESTATS])
@@ -10770,17 +10770,37 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 
 	PQclear(res);
 
+	destroyPQExpBuffer(query);
+	ret = out->data;
+	pg_free(out);
+	return ret;
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Make an ArchiveEntry for the relation statistics.  The Archiver will take
+ * care of gathering the statistics and generating the restore commands when
+ * they are needed.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	const DumpableObject *dobj = &rsinfo->dobj;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
 	ArchiveEntry(fout, nilCatalogId, createDumpId(),
 				 ARCHIVE_OPTS(.tag = dobj->name,
 							  .namespace = dobj->namespace->dobj.name,
 							  .description = "STATISTICS DATA",
 							  .section = rsinfo->section,
-							  .createStmt = out->data,
+							  .defnFn = dumpRelationStats_dumper,
+							  .defnArg = rsinfo,
 							  .deps = dobj->dependencies,
 							  .nDeps = dobj->nDeps));
-
-	destroyPQExpBuffer(out);
-	destroyPQExpBuffer(query);
 }
 
 /*
-- 
2.39.5 (Apple Git-154)

v12n3-0003-pg_dump-Retrieve-attribute-statistics-in-batch.patchtext/plain; charset=us-asciiDownload
From 3c7ff6e0eea0ed6435e17160df67227f3d43d7cf Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Tue, 1 Apr 2025 11:35:19 -0500
Subject: [PATCH v12n3 3/3] pg_dump: Retrieve attribute statistics in batches.

Currently, pg_dump gathers attribute statistics with a query per
relation, which can cause pg_dump to take significantly longer,
especially when there are many tables.  This commit improves
matters by gathering attribute statistics for 64 relations at a
time.  Some simple testing showed this was the ideal batch size,
but performance may vary depending on workload.

To construct the next set of relations for the query, we scan
through the TOC list for relevant entries.  Ordinarily, we can stop
issuing queries once we reach the end of the list.  However,
custom-format dumps that include data run the statistics queries
twice (thanks to commit XXXXXXXXXX), so we allow a second pass in
that case.  Our tests showed that batching more than makes up for
any losses from running the queries twice.

This change increases the memory usage of pg_dump a bit, but that
isn't expected to be too egregious and is arguably well worth the
trade-off.

Author: Corey Huinker <corey.huinker@gmail.com>
Discussion: https://postgr.es/m/CADkLM%3Dc%2Br05srPy9w%2B-%2BnbmLEo15dKXYQ03Q_xyK%2BriJerigLQ%40mail.gmail.com
---
 src/bin/pg_dump/pg_dump.c | 111 +++++++++++++++++++++++++++++++-------
 1 file changed, 93 insertions(+), 18 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 46fb70e0a8b..5dfa12e2ce9 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -209,6 +209,9 @@ static int	nbinaryUpgradeClassOids = 0;
 static SequenceItem *sequences = NULL;
 static int	nsequences = 0;
 
+/* Maximum number of relations to fetch in a fetchAttributeStats() call. */
+#define MAX_ATTR_STATS_RELS 64
+
 /*
  * The default number of rows per INSERT when
  * --inserts is specified without --rows-per-insert
@@ -10559,6 +10562,70 @@ appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
 	appendPQExpBuffer(out, "::%s", argtype);
 }
 
+/*
+ * fetchAttributeStats --
+ *
+ * Fetch next batch of rows for getAttributeStats().
+ */
+static PGresult *
+fetchAttributeStats(Archive *fout)
+{
+	ArchiveHandle *AH = (ArchiveHandle *) fout;
+	PQExpBuffer nspnames = createPQExpBuffer();
+	PQExpBuffer relnames = createPQExpBuffer();
+	int			count = 0;
+	PGresult   *res = NULL;
+	static bool restarted;
+	static TocEntry *te;
+
+	/* If we're just starting, set our TOC pointer. */
+	if (!te)
+		te = AH->toc->next;
+
+	/*
+	 * Restart the TOC scan once for custom-format dumps that include data.
+	 * This is necessary because we'll call WriteToc() twice in that case.
+	 */
+	if (!restarted && te == AH->toc &&
+		AH->format == archCustom && fout->dopt->dumpData)
+	{
+		te = AH->toc->next;
+		restarted = true;
+	}
+
+	/* Scan the TOC for the next set of relevant stats entries. */
+	for (; te != AH->toc && count < MAX_ATTR_STATS_RELS; te = te->next)
+	{
+		if (te->reqs && strcmp(te->desc, "STATISTICS DATA") == 0)
+		{
+			RelStatsInfo *rsinfo = (RelStatsInfo *) te->defnDumperArg;
+
+			appendPQExpBuffer(nspnames, "%s%s", count ? "," : "",
+							  fmtId(rsinfo->dobj.namespace->dobj.name));
+			appendPQExpBuffer(relnames, "%s%s", count ? "," : "",
+							  fmtId(rsinfo->dobj.name));
+			count++;
+		}
+	}
+
+	/* Execute the query for the next batch of relations. */
+	if (count > 0)
+	{
+		PQExpBuffer query = createPQExpBuffer();
+
+		appendPQExpBuffer(query, "EXECUTE getAttributeStats("
+						  "'{%s}'::pg_catalog.name[],"
+						  "'{%s}'::pg_catalog.name[])",
+						  nspnames->data, relnames->data);
+		res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+		destroyPQExpBuffer(query);
+	}
+
+	destroyPQExpBuffer(nspnames);
+	destroyPQExpBuffer(relnames);
+	return res;
+}
+
 /*
  * dumpRelationStats_dumper --
  *
@@ -10571,9 +10638,12 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 {
 	const RelStatsInfo *rsinfo = (RelStatsInfo *) userArg;
 	const DumpableObject *dobj = &rsinfo->dobj;
-	PGresult   *res;
+	static PGresult *res;
+	static int	rownum;
 	PQExpBuffer query;
 	PQExpBuffer out;
+	int			i_schemaname;
+	int			i_tablename;
 	int			i_attname;
 	int			i_inherited;
 	int			i_null_frac;
@@ -10595,8 +10665,8 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 	if (!fout->is_prepared[PREPQUERY_GETATTRIBUTESTATS])
 	{
 		appendPQExpBufferStr(query,
-							 "PREPARE getAttributeStats(pg_catalog.name, pg_catalog.name) AS\n"
-							 "SELECT s.attname, s.inherited, "
+							 "PREPARE getAttributeStats(pg_catalog.name[], pg_catalog.name[]) AS\n"
+							 "SELECT s.schemaname, s.tablename, s.attname, s.inherited, "
 							 "s.null_frac, s.avg_width, s.n_distinct, "
 							 "s.most_common_vals, s.most_common_freqs, "
 							 "s.histogram_bounds, s.correlation, "
@@ -10616,9 +10686,11 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 
 		appendPQExpBufferStr(query,
 							 "FROM pg_catalog.pg_stats s "
-							 "WHERE s.schemaname = $1 "
-							 "AND s.tablename = $2 "
-							 "ORDER BY s.attname, s.inherited");
+							 "JOIN unnest($1, $2) WITH ORDINALITY AS u (schemaname, tablename, ord) "
+							 "ON s.schemaname = u.schemaname "
+							 "AND s.tablename = u.tablename "
+							 "WHERE s.tablename = ANY($2) "
+							 "ORDER BY u.ord, s.attname, s.inherited");
 
 		ExecuteSqlStatement(fout, query->data);
 
@@ -10648,16 +10720,16 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 
 	appendPQExpBufferStr(out, "\n);\n");
 
+	/* Fetch the next batch of attribute statistics if needed. */
+	if (rownum >= PQntuples(res))
+	{
+		PQclear(res);
+		res = fetchAttributeStats(fout);
+		rownum = 0;
+	}
 
-	/* fetch attribute stats */
-	appendPQExpBufferStr(query, "EXECUTE getAttributeStats(");
-	appendStringLiteralAH(query, dobj->namespace->dobj.name, fout);
-	appendPQExpBufferStr(query, ", ");
-	appendStringLiteralAH(query, dobj->name, fout);
-	appendPQExpBufferStr(query, ");");
-
-	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
-
+	i_schemaname = PQfnumber(res, "schemaname");
+	i_tablename = PQfnumber(res, "tablename");
 	i_attname = PQfnumber(res, "attname");
 	i_inherited = PQfnumber(res, "inherited");
 	i_null_frac = PQfnumber(res, "null_frac");
@@ -10675,10 +10747,15 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 	i_range_bounds_histogram = PQfnumber(res, "range_bounds_histogram");
 
 	/* restore attribute stats */
-	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	for (; rownum < PQntuples(res); rownum++)
 	{
 		const char *attname;
 
+		/* Stop if the next stat row in our cache isn't for this relation. */
+		if (strcmp(dobj->name, PQgetvalue(res, rownum, i_tablename)) != 0 ||
+			strcmp(dobj->namespace->dobj.name, PQgetvalue(res, rownum, i_schemaname)) != 0)
+			break;
+
 		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
 		appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
 						  fout->remoteVersion);
@@ -10768,8 +10845,6 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 		appendPQExpBufferStr(out, "\n);\n");
 	}
 
-	PQclear(res);
-
 	destroyPQExpBuffer(query);
 	ret = out->data;
 	pg_free(out);
-- 
2.39.5 (Apple Git-154)

#511Jeff Davis
pgsql@j-davis.com
In reply to: Robert Haas (#508)
Re: Statistics Import and Export

On Tue, 2025-04-01 at 09:37 -0400, Robert Haas wrote:

I don't think I was aware of the open item; I was just catching up on
email.

I lean towards making it opt-in for pg_dump and opt-out for pg_upgrade.
But I think we should leave open the possibility for changing the
default to opt-out for pg_dump in the future.

My reasoning for pg_dump is that releasing with stats as opt-in doesn't
put us in a worse position for making it opt-out later, so long as we
have the right set of both positive and negative options. It may even
be a better position because people have time to make their scripts
future proof by using the right combination of options.

But I also don't really see the value of waiting until beta to
make this decision. I seriously doubt that my opinion is going to
change. Maybe other people's will, though: I can only speak for
myself.

I don't think the last week before feature freeze, deep in a 400-email
thread is the best way to make decisions like this. Let's at least have
a focused thread on this topic and see if we can solicit opinions from
both sides.

Also, waiting to see if the performance improvements make it in, or
waiting for beta reports, may yield some new information that could
change minds.

Mid-beta might be too long, but let's wait for the final CF to settle
and give people the chance to respond to a top-level thread?

Regards,
Jeff Davis

#512Jeff Davis
pgsql@j-davis.com
In reply to: Nathan Bossart (#510)
Re: Statistics Import and Export

On Tue, 2025-04-01 at 13:44 -0500, Nathan Bossart wrote:

Apologies for the noise.  I noticed one more way to simplify 0002. 
As
before, there should be no functional differences.

To restate the problem: one of the problems being solved here is that
the existing code for custom-format dumps calls WriteToc twice. That
was not a big problem before this patch, when the contents of the
entries was easily accessible in memory. But the point of 0002 is to
avoid keeping all of the stats in memory at once, because that causes
bloat; and instead to query it on demand.

In theory, we could fix the pre-existing code by making the second pass
able to jump over the other contents of the entry and just update the
data offsets. But that seems invasive, at least to do it properly.

0001 sidesteps the problem by skipping the second pass if data's not
being dumped (because there are no offsets that need updating). The
worst case is when there are a lot of objects with a small amount of
data. But that's a worst case for stats in general, so I don't think
that needs to be solved here.

Issuing the stats queries twice is not great, though. If there's any
non-deterministic output in the query, that could lead to strangeness.
How bad can that be? If the results change in some way that looks
benign, but changes the length of the definition string, can it lead to
corruption of a ToC entry? I'm not saying there's a problem, but trying
to understand the risk of future problems.

For 0003, it makes an assumption about the way the scan happens in
WriteToc(). Can you add some additional sanity checks to verify that
something doesn't happen in a different order than we expect?

Also, why do we need the clause "WHERE s.tablename = ANY($2)"? Isn't
that already implied by "JOIN unnest($1, $2) ... s.tablename =
u.tablename"?

Regards,
Jeff Davis

#513Robert Haas
robertmhaas@gmail.com
In reply to: Jeff Davis (#511)
Re: Statistics Import and Export

On Tue, Apr 1, 2025 at 4:24 PM Jeff Davis <pgsql@j-davis.com> wrote:

On Tue, 2025-04-01 at 09:37 -0400, Robert Haas wrote:

I don't think I was aware of the open item; I was just catching up on
email.

I lean towards making it opt-in for pg_dump and opt-out for pg_upgrade.

Big +1.

But I think we should leave open the possibility for changing the
default to opt-out for pg_dump in the future.

We can always decide to change things, but this is different from some
other cases. Sometimes we're waiting to enable a feature by default
until, say, we think it's stable. This case is more about user
expectations, which might not be so prone to changing over time (but
you never know).

Mid-beta might be too long, but let's wait for the final CF to settle
and give people the chance to respond to a top-level thread?

wfm, thanks.

--
Robert Haas
EDB: http://www.enterprisedb.com

#514Nathan Bossart
nathandbossart@gmail.com
In reply to: Jeff Davis (#512)
3 attachment(s)
Re: Statistics Import and Export

On Tue, Apr 01, 2025 at 03:05:59PM -0700, Jeff Davis wrote:

To restate the problem: one of the problems being solved here is that
the existing code for custom-format dumps calls WriteToc twice. That
was not a big problem before this patch, when the contents of the
entries was easily accessible in memory. But the point of 0002 is to
avoid keeping all of the stats in memory at once, because that causes
bloat; and instead to query it on demand.

In theory, we could fix the pre-existing code by making the second pass
able to jump over the other contents of the entry and just update the
data offsets. But that seems invasive, at least to do it properly.

0001 sidesteps the problem by skipping the second pass if data's not
being dumped (because there are no offsets that need updating). The
worst case is when there are a lot of objects with a small amount of
data. But that's a worst case for stats in general, so I don't think
that needs to be solved here.

Issuing the stats queries twice is not great, though. If there's any
non-deterministic output in the query, that could lead to strangeness.
How bad can that be? If the results change in some way that looks
benign, but changes the length of the definition string, can it lead to
corruption of a ToC entry? I'm not saying there's a problem, but trying
to understand the risk of future problems.

It certainly feels risky. I was able to avoid executing the queries twice
in all cases by saving the definition length in the TOC entry and skipping
that many bytes the second time round. That's simple enough, but it relies
on various assumptions such as fseeko() being available (IIUC the file will
only be open for writing so we cannot fall back on fread()) and WriteStr()
returning an accurate value (which I'm skeptical of because some formats
compress this data). But AFAICT custom format is the only format that does
a second WriteToc() pass at the moment, and it only does so when fseeko()
is usable. Plus, custom format doesn't appear to compress anything written
via WriteStr().

We might be able to improve this by inventing a new callback that fails for
all formats except for custom with feesko() available. That would at least
ensure hard failures if these assumptions change. That problably wouldn't
be terribly invasive. I'm curious what you think.

For 0003, it makes an assumption about the way the scan happens in
WriteToc(). Can you add some additional sanity checks to verify that
something doesn't happen in a different order than we expect?

Hm. One thing we could do is to send the TocEntry to the callback and
verify that matches the one we were expecting to see next (as set by a
previous call). Does that sound like a strong enough check? FWIW the
pg_dump tests failed miserably until Corey and I got this part right, so
our usual tests should also offer some assurance.

Also, why do we need the clause "WHERE s.tablename = ANY($2)"? Isn't
that already implied by "JOIN unnest($1, $2) ... s.tablename =
u.tablename"?

Good question. Corey, do you recall why this was needed?

--
nathan

Attachments:

v12n4-0001-Skip-second-WriteToc-for-custom-format-dumps-w.patchtext/plain; charset=us-asciiDownload
From 0b20e8a4c8f153e9f292da82a9dbc82ed3adbb3e Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 31 Mar 2025 10:44:26 -0500
Subject: [PATCH v12n4 1/3] Skip second WriteToc() for custom-format dumps
 without data.

Presently, "pg_dump --format=custom" calls WriteToc() twice.  The
second call is intended to update the data offset information,
which allegedly makes parallel pg_restore significantly faster.
However, if we aren't dumping any data, this step accomplishes
nothing and can be skipped.  This is a preparatory optimization for
follow-up commits that will move the queries for attribute
statistics to WriteToc() to save memory.

Reviewed-by: Jeff Davis <pgsql@j-davis.com>
Discussion: https://postgr.es/m/Z9c1rbzZegYQTOQE%40nathan
---
 src/bin/pg_dump/pg_backup_custom.c | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup_custom.c b/src/bin/pg_dump/pg_backup_custom.c
index e44b887eb29..b971e3bc16e 100644
--- a/src/bin/pg_dump/pg_backup_custom.c
+++ b/src/bin/pg_dump/pg_backup_custom.c
@@ -755,9 +755,11 @@ _CloseArchive(ArchiveHandle *AH)
 		 * If possible, re-write the TOC in order to update the data offset
 		 * information.  This is not essential, as pg_restore can cope in most
 		 * cases without it; but it can make pg_restore significantly faster
-		 * in some situations (especially parallel restore).
+		 * in some situations (especially parallel restore).  We can skip this
+		 * step if we're not dumping any data.
 		 */
-		if (ctx->hasSeek &&
+		if (AH->public.dopt->dumpData &&
+			ctx->hasSeek &&
 			fseeko(AH->FH, tpos, SEEK_SET) == 0)
 			WriteToc(AH);
 	}
-- 
2.39.5 (Apple Git-154)

v12n4-0002-pg_dump-Reduce-memory-usage-of-dumps-with-stat.patchtext/plain; charset=us-asciiDownload
From 6e6d5345d45092bc0acc5ca31c7d7d663fe2ccad Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Tue, 1 Apr 2025 20:46:24 -0500
Subject: [PATCH v12n4 2/3] pg_dump: Reduce memory usage of dumps with
 statistics.

Right now, pg_dump stores all generated commands for statistics in
memory.  These commands can be quite large and therefore can
significantly increase pg_dump's memory footprint.  To fix, wait
until we are about to write out the commands before generating
them, and be sure to free the commands after writing.  This is
implemented via a new defnDumper callback that works much like the
dataDumper one but is specially designed for TOC entries.

Custom dumps that include data might write the TOC twice (to update
data offset information), which would ordinarily cause pg_dump to
run the attribute statistics queries twice.  However, as a hack, we
save the length of the written-out entry in the first pass, and we
skip over it in the second.  While there is no known technical
problem with executing the queries multiple times and rewriting the
results, it's expensive and feels risky, so it seems prudent to
avoid it.

Author: Corey Huinker <corey.huinker@gmail.com>
Reviewed-by: Jeff Davis <pgsql@j-davis.com>
Discussion: https://postgr.es/m/CADkLM%3Dc%2Br05srPy9w%2B-%2BnbmLEo15dKXYQ03Q_xyK%2BriJerigLQ%40mail.gmail.com
---
 src/bin/pg_dump/pg_backup.h          |  1 +
 src/bin/pg_dump/pg_backup_archiver.c | 44 ++++++++++++++++++++++++++--
 src/bin/pg_dump/pg_backup_archiver.h |  6 ++++
 src/bin/pg_dump/pg_dump.c            | 44 ++++++++++++++++++++--------
 4 files changed, 81 insertions(+), 14 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 658986de6f8..781f8fa1cc9 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -285,6 +285,7 @@ typedef int DumpId;
  * Function pointer prototypes for assorted callback methods.
  */
 
+typedef char *(*DefnDumperPtr) (Archive *AH, const void *userArg);
 typedef int (*DataDumperPtr) (Archive *AH, const void *userArg);
 
 typedef void (*SetupWorkerPtrType) (Archive *AH);
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 1d131e5a57d..334b5dedfd7 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -1266,6 +1266,9 @@ ArchiveEntry(Archive *AHX, CatalogId catalogId, DumpId dumpId,
 	newToc->dataDumperArg = opts->dumpArg;
 	newToc->hadDumper = opts->dumpFn ? true : false;
 
+	newToc->defnDumper = opts->defnFn;
+	newToc->defnDumperArg = opts->defnArg;
+
 	newToc->formatData = NULL;
 	newToc->dataLength = 0;
 
@@ -2621,7 +2624,33 @@ WriteToc(ArchiveHandle *AH)
 		WriteStr(AH, te->tag);
 		WriteStr(AH, te->desc);
 		WriteInt(AH, te->section);
-		WriteStr(AH, te->defn);
+
+		if (te->defnLen)
+		{
+			/*
+			 * We only set defnLen when a definition is generated by the
+			 * defnDumper during WriteToc(), so this must be a second
+			 * WriteToc() pass.  The defnDumper might execute queries, and
+			 * while running the same queries twice should in theory work
+			 * fine, it's expensive and feels risky.  So, we just seek through
+			 * those entries.  Presently, the only time we do a second
+			 * WriteToc() pass is for custom-format dumps when we've already
+			 * verified fseeko() works, so we can use it without checking.
+			 * We'll need to figure out something else if this changes.
+			 */
+			if (fseeko(AH->FH, te->defnLen, SEEK_CUR) != 0)
+				pg_fatal("error during file seek: %m");
+		}
+		else if (te->defnDumper)
+		{
+			char	   *defn = te->defnDumper((Archive *) AH, te->defnDumperArg);
+
+			te->defnLen = WriteStr(AH, defn);
+			pg_free(defn);
+		}
+		else
+			WriteStr(AH, te->defn);
+
 		WriteStr(AH, te->dropStmt);
 		WriteStr(AH, te->copyStmt);
 		WriteStr(AH, te->namespace);
@@ -3849,7 +3878,7 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 
 	/*
 	 * Actually print the definition.  Normally we can just print the defn
-	 * string if any, but we have three special cases:
+	 * string if any, but we have four special cases:
 	 *
 	 * 1. A crude hack for suppressing AUTHORIZATION clause that old pg_dump
 	 * versions put into CREATE SCHEMA.  Don't mutate the variant for schema
@@ -3862,6 +3891,10 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 	 * 3. ACL LARGE OBJECTS entries need special processing because they
 	 * contain only one copy of the ACL GRANT/REVOKE commands, which we must
 	 * apply to each large object listed in the associated BLOB METADATA.
+	 *
+	 * 4. Entries with a defnDumper need to call it to generate the
+	 * definition.  This is primarily intended to provide a way to save memory
+	 * for objects that need a lot of it (e.g., statistics data).
 	 */
 	if (ropt->noOwner &&
 		strcmp(te->desc, "SCHEMA") == 0 && strncmp(te->defn, "--", 2) != 0)
@@ -3877,6 +3910,13 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 	{
 		IssueACLPerBlob(AH, te);
 	}
+	else if (te->defnDumper)
+	{
+		char	   *defn = te->defnDumper((Archive *) AH, te->defnDumperArg);
+
+		ahprintf(AH, "%s\n\n", defn);
+		pg_free(defn);
+	}
 	else if (te->defn && strlen(te->defn) > 0)
 	{
 		ahprintf(AH, "%s\n\n", te->defn);
diff --git a/src/bin/pg_dump/pg_backup_archiver.h b/src/bin/pg_dump/pg_backup_archiver.h
index a2064f471ed..b7ebc2b39cd 100644
--- a/src/bin/pg_dump/pg_backup_archiver.h
+++ b/src/bin/pg_dump/pg_backup_archiver.h
@@ -368,6 +368,10 @@ struct _tocEntry
 	const void *dataDumperArg;	/* Arg for above routine */
 	void	   *formatData;		/* TOC Entry data specific to file format */
 
+	DefnDumperPtr defnDumper;	/* routine to dump definition statement */
+	const void *defnDumperArg;	/* arg for above routine */
+	size_t		defnLen;		/* length of dumped definition */
+
 	/* working state while dumping/restoring */
 	pgoff_t		dataLength;		/* item's data size; 0 if none or unknown */
 	int			reqs;			/* do we need schema and/or data of object
@@ -407,6 +411,8 @@ typedef struct _archiveOpts
 	int			nDeps;
 	DataDumperPtr dumpFn;
 	const void *dumpArg;
+	DefnDumperPtr defnFn;
+	const void *defnArg;
 } ArchiveOpts;
 #define ARCHIVE_OPTS(...) &(ArchiveOpts){__VA_ARGS__}
 /* Called to add a TOC entry */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 4ca34be230c..46fb70e0a8b 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -10560,13 +10560,16 @@ appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
 }
 
 /*
- * dumpRelationStats --
+ * dumpRelationStats_dumper --
  *
- * Dump command to import stats into the relation on the new database.
+ * Generate command to import stats into the relation on the new database.
+ * This routine is called by the Archiver when it wants the statistics to be
+ * dumped.
  */
-static void
-dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+static char *
+dumpRelationStats_dumper(Archive *fout, const void *userArg)
 {
+	const RelStatsInfo *rsinfo = (RelStatsInfo *) userArg;
 	const DumpableObject *dobj = &rsinfo->dobj;
 	PGresult   *res;
 	PQExpBuffer query;
@@ -10586,10 +10589,7 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 	int			i_range_length_histogram;
 	int			i_range_empty_frac;
 	int			i_range_bounds_histogram;
-
-	/* nothing to do if we are not dumping statistics */
-	if (!fout->dopt->dumpStatistics)
-		return;
+	char	   *ret;
 
 	query = createPQExpBuffer();
 	if (!fout->is_prepared[PREPQUERY_GETATTRIBUTESTATS])
@@ -10770,17 +10770,37 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 
 	PQclear(res);
 
+	destroyPQExpBuffer(query);
+	ret = out->data;
+	pg_free(out);
+	return ret;
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Make an ArchiveEntry for the relation statistics.  The Archiver will take
+ * care of gathering the statistics and generating the restore commands when
+ * they are needed.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	const DumpableObject *dobj = &rsinfo->dobj;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
 	ArchiveEntry(fout, nilCatalogId, createDumpId(),
 				 ARCHIVE_OPTS(.tag = dobj->name,
 							  .namespace = dobj->namespace->dobj.name,
 							  .description = "STATISTICS DATA",
 							  .section = rsinfo->section,
-							  .createStmt = out->data,
+							  .defnFn = dumpRelationStats_dumper,
+							  .defnArg = rsinfo,
 							  .deps = dobj->dependencies,
 							  .nDeps = dobj->nDeps));
-
-	destroyPQExpBuffer(out);
-	destroyPQExpBuffer(query);
 }
 
 /*
-- 
2.39.5 (Apple Git-154)

v12n4-0003-pg_dump-Retrieve-attribute-statistics-in-batch.patchtext/plain; charset=us-asciiDownload
From 299c7b6d0f5b009d42ee5a4a28278ab3c1bc725c Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Tue, 1 Apr 2025 21:00:24 -0500
Subject: [PATCH v12n4 3/3] pg_dump: Retrieve attribute statistics in batches.

Currently, pg_dump gathers attribute statistics with a query per
relation, which can cause pg_dump to take significantly longer,
especially when there are many tables.  This commit improves
matters by gathering attribute statistics for 64 relations at a
time.  Some simple testing showed this was the ideal batch size,
but performance may vary depending on workload.

This change increases the memory usage of pg_dump a bit, but that
isn't expected to be too egregious and is arguably well worth the
trade-off.

Author: Corey Huinker <corey.huinker@gmail.com>
Reviewed-by: Jeff Davis <pgsql@j-davis.com>
Discussion: https://postgr.es/m/CADkLM%3Dc%2Br05srPy9w%2B-%2BnbmLEo15dKXYQ03Q_xyK%2BriJerigLQ%40mail.gmail.com
---
 src/bin/pg_dump/pg_dump.c | 104 +++++++++++++++++++++++++++++++-------
 1 file changed, 86 insertions(+), 18 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 46fb70e0a8b..9d60c77865f 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -209,6 +209,9 @@ static int	nbinaryUpgradeClassOids = 0;
 static SequenceItem *sequences = NULL;
 static int	nsequences = 0;
 
+/* Maximum number of relations to fetch in a fetchAttributeStats() call. */
+#define MAX_ATTR_STATS_RELS 64
+
 /*
  * The default number of rows per INSERT when
  * --inserts is specified without --rows-per-insert
@@ -10559,6 +10562,63 @@ appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
 	appendPQExpBuffer(out, "::%s", argtype);
 }
 
+/*
+ * fetchAttributeStats --
+ *
+ * Fetch next batch of rows for getAttributeStats().
+ */
+static PGresult *
+fetchAttributeStats(Archive *fout)
+{
+	ArchiveHandle *AH = (ArchiveHandle *) fout;
+	PQExpBuffer nspnames = createPQExpBuffer();
+	PQExpBuffer relnames = createPQExpBuffer();
+	int			count = 0;
+	PGresult   *res = NULL;
+	static TocEntry *te;
+
+	/* If we're just starting, set our TOC pointer. */
+	if (!te)
+		te = AH->toc->next;
+
+	/*
+	 * Scan the TOC for the next set of relevant stats entries.  We assume
+	 * that statistics are dumped in the order they are listed in the TOC.
+	 * This is perhaps not the sturdiest assumption, but it appears to
+	 * presently be true in all cases.
+	 */
+	for (; te != AH->toc && count < MAX_ATTR_STATS_RELS; te = te->next)
+	{
+		if (te->reqs && strcmp(te->desc, "STATISTICS DATA") == 0)
+		{
+			RelStatsInfo *rsinfo = (RelStatsInfo *) te->defnDumperArg;
+
+			appendPQExpBuffer(nspnames, "%s%s", count ? "," : "",
+							  fmtId(rsinfo->dobj.namespace->dobj.name));
+			appendPQExpBuffer(relnames, "%s%s", count ? "," : "",
+							  fmtId(rsinfo->dobj.name));
+			count++;
+		}
+	}
+
+	/* Execute the query for the next batch of relations. */
+	if (count > 0)
+	{
+		PQExpBuffer query = createPQExpBuffer();
+
+		appendPQExpBuffer(query, "EXECUTE getAttributeStats("
+						  "'{%s}'::pg_catalog.name[],"
+						  "'{%s}'::pg_catalog.name[])",
+						  nspnames->data, relnames->data);
+		res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+		destroyPQExpBuffer(query);
+	}
+
+	destroyPQExpBuffer(nspnames);
+	destroyPQExpBuffer(relnames);
+	return res;
+}
+
 /*
  * dumpRelationStats_dumper --
  *
@@ -10571,9 +10631,12 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 {
 	const RelStatsInfo *rsinfo = (RelStatsInfo *) userArg;
 	const DumpableObject *dobj = &rsinfo->dobj;
-	PGresult   *res;
+	static PGresult *res;
+	static int	rownum;
 	PQExpBuffer query;
 	PQExpBuffer out;
+	int			i_schemaname;
+	int			i_tablename;
 	int			i_attname;
 	int			i_inherited;
 	int			i_null_frac;
@@ -10595,8 +10658,8 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 	if (!fout->is_prepared[PREPQUERY_GETATTRIBUTESTATS])
 	{
 		appendPQExpBufferStr(query,
-							 "PREPARE getAttributeStats(pg_catalog.name, pg_catalog.name) AS\n"
-							 "SELECT s.attname, s.inherited, "
+							 "PREPARE getAttributeStats(pg_catalog.name[], pg_catalog.name[]) AS\n"
+							 "SELECT s.schemaname, s.tablename, s.attname, s.inherited, "
 							 "s.null_frac, s.avg_width, s.n_distinct, "
 							 "s.most_common_vals, s.most_common_freqs, "
 							 "s.histogram_bounds, s.correlation, "
@@ -10616,9 +10679,11 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 
 		appendPQExpBufferStr(query,
 							 "FROM pg_catalog.pg_stats s "
-							 "WHERE s.schemaname = $1 "
-							 "AND s.tablename = $2 "
-							 "ORDER BY s.attname, s.inherited");
+							 "JOIN unnest($1, $2) WITH ORDINALITY AS u (schemaname, tablename, ord) "
+							 "ON s.schemaname = u.schemaname "
+							 "AND s.tablename = u.tablename "
+							 "WHERE s.tablename = ANY($2) "
+							 "ORDER BY u.ord, s.attname, s.inherited");
 
 		ExecuteSqlStatement(fout, query->data);
 
@@ -10648,16 +10713,16 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 
 	appendPQExpBufferStr(out, "\n);\n");
 
+	/* Fetch the next batch of attribute statistics if needed. */
+	if (rownum >= PQntuples(res))
+	{
+		PQclear(res);
+		res = fetchAttributeStats(fout);
+		rownum = 0;
+	}
 
-	/* fetch attribute stats */
-	appendPQExpBufferStr(query, "EXECUTE getAttributeStats(");
-	appendStringLiteralAH(query, dobj->namespace->dobj.name, fout);
-	appendPQExpBufferStr(query, ", ");
-	appendStringLiteralAH(query, dobj->name, fout);
-	appendPQExpBufferStr(query, ");");
-
-	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
-
+	i_schemaname = PQfnumber(res, "schemaname");
+	i_tablename = PQfnumber(res, "tablename");
 	i_attname = PQfnumber(res, "attname");
 	i_inherited = PQfnumber(res, "inherited");
 	i_null_frac = PQfnumber(res, "null_frac");
@@ -10675,10 +10740,15 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 	i_range_bounds_histogram = PQfnumber(res, "range_bounds_histogram");
 
 	/* restore attribute stats */
-	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	for (; rownum < PQntuples(res); rownum++)
 	{
 		const char *attname;
 
+		/* Stop if the next stat row in our cache isn't for this relation. */
+		if (strcmp(dobj->name, PQgetvalue(res, rownum, i_tablename)) != 0 ||
+			strcmp(dobj->namespace->dobj.name, PQgetvalue(res, rownum, i_schemaname)) != 0)
+			break;
+
 		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
 		appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
 						  fout->remoteVersion);
@@ -10768,8 +10838,6 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 		appendPQExpBufferStr(out, "\n);\n");
 	}
 
-	PQclear(res);
-
 	destroyPQExpBuffer(query);
 	ret = out->data;
 	pg_free(out);
-- 
2.39.5 (Apple Git-154)

#515Jeff Davis
pgsql@j-davis.com
In reply to: Nathan Bossart (#514)
Re: Statistics Import and Export

On Tue, 2025-04-01 at 22:21 -0500, Nathan Bossart wrote:

It certainly feels risky.  I was able to avoid executing the queries
twice
in all cases by saving the definition length in the TOC entry and
skipping
that many bytes the second time round.

That feels like a better approach.

  That's simple enough, but it relies
on various assumptions such as fseeko() being available (IIUC the
file will
only be open for writing so we cannot fall back on fread()) and
WriteStr()
returning an accurate value (which I'm skeptical of because some
formats
compress this data).  But AFAICT custom format is the only format
that does
a second WriteToc() pass at the moment, and it only does so when
fseeko()
is usable.

Even with those assumptions, I think it's much better than querying
twice and assuming that the results are the same.

  Plus, custom format doesn't appear to compress anything written
via WriteStr().

If WriteStr() was doing compression, that would make the second
WriteToc() pass to update the data offsets scary even in the existing
code.

We might be able to improve this by inventing a new callback that
fails for
all formats except for custom with feesko() available.  That would at
least
ensure hard failures if these assumptions change.  That problably
wouldn't
be terribly invasive.  I'm curious what you think.

That sounds fine, I'd say do that if it feels reasonable, and if the
extra callbacks get too messy, we can just document the assumptions
instead.

Hm.  One thing we could do is to send the TocEntry to the callback
and
verify that matches the one we were expecting to see next (as set by
a
previous call).  Does that sound like a strong enough check?

Again, I'd just be practical here and do the check if it feels natural,
and if not, improve the comments so that someone modifying the code
would know where to look.

Regards,
Jeff Davis

#516Andres Freund
andres@anarazel.de
In reply to: Jeff Davis (#515)
Re: Statistics Import and Export

Hi,

https://commitfest.postgresql.org/patch/4538/ is still in "needs review", even
though the feature really has been committed. Is that intention, e.g. to
track pending changes that we're planning to make?

Greetings,

Andres

#517Jeff Davis
pgsql@j-davis.com
In reply to: Nathan Bossart (#514)
Re: Statistics Import and Export

On Tue, 2025-04-01 at 22:21 -0500, Nathan Bossart wrote:

It certainly feels risky.  I was able to avoid executing the queries
twice
in all cases by saving the definition length in the TOC entry and
skipping
that many bytes the second time round.

Another idea that was under-discussed is whether the stats commands
should be in the TOC at all, or if they should be written as data
chunks.

Being in the TOC creates these issues with rewriting the TOC. Also, the
stats can be fairly large, especially for a wide table with a high
stats target, so the stats commands can increase the size of the TOC by
a lot.

But putting them in the data area doesn't seem quite right either,
because the data is just data, whereas the stats are a list of SQL
commands ("SELECT pg_restore_relation_stats(...); ..."). Also, if we
went down that road, we'd have to consider parallelism, which might
defeat the batching work that we're trying to do.

Regards,
Jeff Davis

#518Nathan Bossart
nathandbossart@gmail.com
In reply to: Jeff Davis (#515)
3 attachment(s)
Re: Statistics Import and Export

On Tue, Apr 01, 2025 at 10:44:19PM -0700, Jeff Davis wrote:

On Tue, 2025-04-01 at 22:21 -0500, Nathan Bossart wrote:

We might be able to improve this by inventing a new callback that fails for
all formats except for custom with feesko() available.� That would at least
ensure hard failures if these assumptions change.� That problably wouldn't
be terribly invasive.� I'm curious what you think.

That sounds fine, I'd say do that if it feels reasonable, and if the
extra callbacks get too messy, we can just document the assumptions
instead.

I did write a version with callbacks, but it felt a bit silly because it is
very obviously intended for this one case. So, I removed them in the
attached patch set.

Hm.� One thing we could do is to send the TocEntry to the callback and
verify that matches the one we were expecting to see next (as set by a
previous call).� Does that sound like a strong enough check?

Again, I'd just be practical here and do the check if it feels natural,
and if not, improve the comments so that someone modifying the code
would know where to look.

Okay, here is an updated patch set. I did add some verification code,
which ended up being a really good idea because it revealed a couple of
cases we weren't handling:

* Besides custom format calling WriteToc() twice to update the data
offsets, tar format calls WriteToc() followed by RestoreArchive() to
write restore.sql. I couldn't think of a great way to avoid executing
the queries twice in this case, so I settled on allowing it for only that
mode. While we don't expect the second set of queries to result in
different stats definitions, even if it did, the worst case is that the
content of restore.sql (which isn't used by pg_restore) would be
different. I noticed some past discussion that seems to suggest that
this format might be a candidate for deprecation [0]/messages/by-id/20180727015306.fzlo4inv5i3zqr2c@alap3.anarazel.de, so I'm not sure
it's worth doing anything fancier.

* Our batching code assumes that stats entries are dumped in TOC order,
which unfortunately wasn't true for formats that use RestoreArchive() for
dumping. This is because RestoreArchive() does multiple passes through
the TOC and selectively dumps certain entries each time. This is
particularly troublesome for index stats and a subset of matview stats;
both are in SECTION_POST_DATA, but matview stats that depend on matview
data are dumped in RESTORE_PASS_POST_ACL, while all other stats data is
dumped in RESTORE_PASS_MAIN. To deal with this, I propose moving all
stats entries in SECTION_POST_DATA to RESTORE_PASS_POST_ACL, which
ensures that we always dump stats in TOC order. One convenient side
effect of this change is that we can revert a decent chunk of commit
a0a4601765. It might be possible to do better via smarter lookahead code
or a more sophisticated cache, but it's a bit late in the game for that.

[0]: /messages/by-id/20180727015306.fzlo4inv5i3zqr2c@alap3.anarazel.de

--
nathan

Attachments:

v12n5-0001-Skip-second-WriteToc-for-custom-format-dumps-w.patchtext/plain; charset=us-asciiDownload
From d79569827464e511a2f3c0ad489aaab7261e9e26 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 2 Apr 2025 17:12:19 -0500
Subject: [PATCH v12n5 1/3] Skip second WriteToc() for custom-format dumps
 without data.

Presently, "pg_dump --format=custom" calls WriteToc() twice.  The
second call is intended to update the data offset information,
which allegedly makes parallel pg_restore significantly faster.
However, if we aren't dumping any data, this step accomplishes
nothing and can be skipped.  This is a preparatory optimization for
follow-up commits that will move the queries for attribute
statistics to WriteToc()/_printTocEntry() to save memory.

Reviewed-by: Jeff Davis <pgsql@j-davis.com>
Discussion: https://postgr.es/m/Z9c1rbzZegYQTOQE%40nathan
---
 src/bin/pg_dump/pg_backup_custom.c | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup_custom.c b/src/bin/pg_dump/pg_backup_custom.c
index e44b887eb29..f7c3af56304 100644
--- a/src/bin/pg_dump/pg_backup_custom.c
+++ b/src/bin/pg_dump/pg_backup_custom.c
@@ -755,9 +755,11 @@ _CloseArchive(ArchiveHandle *AH)
 		 * If possible, re-write the TOC in order to update the data offset
 		 * information.  This is not essential, as pg_restore can cope in most
 		 * cases without it; but it can make pg_restore significantly faster
-		 * in some situations (especially parallel restore).
+		 * in some situations (especially parallel restore).  We can skip this
+		 * step if we're not dumping any data; there are no offsets to update
+		 * in that case.
 		 */
-		if (ctx->hasSeek &&
+		if (ctx->hasSeek && AH->public.dopt->dumpData &&
 			fseeko(AH->FH, tpos, SEEK_SET) == 0)
 			WriteToc(AH);
 	}
-- 
2.39.5 (Apple Git-154)

v12n5-0002-pg_dump-Reduce-memory-usage-of-dumps-with-stat.patchtext/plain; charset=us-asciiDownload
From 0467e2304eb9186065302b01d3b65b6a00879a44 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 2 Apr 2025 19:49:12 -0500
Subject: [PATCH v12n5 2/3] pg_dump: Reduce memory usage of dumps with
 statistics.

Right now, pg_dump stores all generated commands for statistics in
memory.  These commands can be quite large and therefore can
significantly increase pg_dump's memory footprint.  To fix, wait
until we are about to write out the commands before generating
them, and be sure to free the commands after writing.  This is
implemented via a new defnDumper callback that works much like the
dataDumper one but is specially designed for TOC entries.

Custom dumps that include data might write the TOC twice (to update
data offset information), which would ordinarily cause pg_dump to
run the attribute statistics queries twice.  However, as a hack, we
save the length of the written-out entry in the first pass, and we
skip over it in the second.  While there is no known technical
problem with executing the queries multiple times and rewriting the
results, it's expensive and feels risky, so it seems prudent to
avoid it.

As an exception, we _do_ execute the queries twice for the tar
format.  This format does a second pass through the TOC to generate
the restore.sql file, which isn't used by pg_restore, so different
results won't corrupt the output (it'll just be different).  We
could alternatively save the definition in memory the first time it
is generated, but that defeats the purpose of this change.  In any
case, past discussion indicates that the tar format might be a
candidate for deprecation, so it doesn't seem worth trying too much
harder.

Author: Corey Huinker <corey.huinker@gmail.com>
Co-authored-by: Nathan Bossart <nathandbossart@gmail.com>
Reviewed-by: Jeff Davis <pgsql@j-davis.com>
Discussion: https://postgr.es/m/CADkLM%3Dc%2Br05srPy9w%2B-%2BnbmLEo15dKXYQ03Q_xyK%2BriJerigLQ%40mail.gmail.com
---
 src/bin/pg_dump/pg_backup.h          |  1 +
 src/bin/pg_dump/pg_backup_archiver.c | 77 +++++++++++++++++++++++++++-
 src/bin/pg_dump/pg_backup_archiver.h |  6 +++
 src/bin/pg_dump/pg_dump.c            | 46 ++++++++++++-----
 4 files changed, 114 insertions(+), 16 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 658986de6f8..781f8fa1cc9 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -285,6 +285,7 @@ typedef int DumpId;
  * Function pointer prototypes for assorted callback methods.
  */
 
+typedef char *(*DefnDumperPtr) (Archive *AH, const void *userArg);
 typedef int (*DataDumperPtr) (Archive *AH, const void *userArg);
 
 typedef void (*SetupWorkerPtrType) (Archive *AH);
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 1d131e5a57d..d7c64583242 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -1266,6 +1266,9 @@ ArchiveEntry(Archive *AHX, CatalogId catalogId, DumpId dumpId,
 	newToc->dataDumperArg = opts->dumpArg;
 	newToc->hadDumper = opts->dumpFn ? true : false;
 
+	newToc->defnDumper = opts->defnFn;
+	newToc->defnDumperArg = opts->defnArg;
+
 	newToc->formatData = NULL;
 	newToc->dataLength = 0;
 
@@ -2621,7 +2624,40 @@ WriteToc(ArchiveHandle *AH)
 		WriteStr(AH, te->tag);
 		WriteStr(AH, te->desc);
 		WriteInt(AH, te->section);
-		WriteStr(AH, te->defn);
+
+		if (te->defnLen)
+		{
+			/*
+			 * defnLen should only be set for custom format's second call to
+			 * WriteToc(), which rewrites the TOC in place to update any data
+			 * offsets.  Rather than call the defnDumper a second time (which
+			 * would involve executing the queries again), just skip writing
+			 * the entry.  While regenerating the definition should in theory
+			 * produce the same result as before, it's expensive and feels
+			 * risky.
+			 *
+			 * The custom format only does a second WriteToc() if fseeko() is
+			 * usable (see _CloseArchive() in pg_backup_custom.c), so we can
+			 * just use it without checking.  For other formats, we fail
+			 * because this assumption must no longer hold true.
+			 */
+			if (AH->format != archCustom)
+				pg_fatal("unexpected TOC entry in WriteToc(): %d %s %s",
+						 te->dumpId, te->desc, te->tag);
+
+			if (fseeko(AH->FH, te->defnLen, SEEK_CUR != 0))
+				pg_fatal("error during file seek: %m");
+		}
+		else if (te->defnDumper)
+		{
+			char	   *defn = te->defnDumper((Archive *) AH, te->defnDumperArg);
+
+			te->defnLen = WriteStr(AH, defn);
+			pg_free(defn);
+		}
+		else
+			WriteStr(AH, te->defn);
+
 		WriteStr(AH, te->dropStmt);
 		WriteStr(AH, te->copyStmt);
 		WriteStr(AH, te->namespace);
@@ -3849,7 +3885,7 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 
 	/*
 	 * Actually print the definition.  Normally we can just print the defn
-	 * string if any, but we have three special cases:
+	 * string if any, but we have four special cases:
 	 *
 	 * 1. A crude hack for suppressing AUTHORIZATION clause that old pg_dump
 	 * versions put into CREATE SCHEMA.  Don't mutate the variant for schema
@@ -3862,6 +3898,10 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 	 * 3. ACL LARGE OBJECTS entries need special processing because they
 	 * contain only one copy of the ACL GRANT/REVOKE commands, which we must
 	 * apply to each large object listed in the associated BLOB METADATA.
+	 *
+	 * 4. Entries with a defnDumper need to call it to generate the
+	 * definition.  This is primarily intended to provide a way to save memory
+	 * for objects that need a lot of it (e.g., statistics data).
 	 */
 	if (ropt->noOwner &&
 		strcmp(te->desc, "SCHEMA") == 0 && strncmp(te->defn, "--", 2) != 0)
@@ -3877,6 +3917,39 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 	{
 		IssueACLPerBlob(AH, te);
 	}
+	else if (te->defnLen && AH->format != archTar)
+	{
+		/*
+		 * If defnLen is set, the defnDumper has already been called for this
+		 * TOC entry. We ordinarily don't expect a defnDumper to be called on
+		 * a TOC entry a second time in _printTocEntry(), but of course
+		 * there's an exception.  The tar format first calls WriteToc(), which
+		 * scans through the entire TOC, and then it later calls
+		 * RestoreArchive() to generate restore.sql, which scans through the
+		 * TOC again. There doesn't seem to be a good way to avoid calling the
+		 * defnDumper again in that case without storing the definition in
+		 * memory, which is what we're trying to avoid in the first place.
+		 * This second defnDumper invocation should generate the exact same
+		 * output, but even if it doesn't, the worst case is that the
+		 * restore.sql file (which isn't used by pg_restore) is incorrect.
+		 * Past discussion on the mailing list indicates that tar format isn't
+		 * known to be heavily used and might be a candidate for deprecation,
+		 * so it doesn't seem worth trying much harder here.
+		 *
+		 * In all other cases, encountering a TOC entry a second time in
+		 * _printTocEntry() is unexpected, so we fail because one of our
+		 * assumptions must no longer hold true.
+		 */
+		pg_fatal("unexpected TOC entry in _printTocEntry(): %d %s %s",
+				 te->dumpId, te->desc, te->tag);
+	}
+	else if (te->defnDumper)
+	{
+		char	   *defn = te->defnDumper((Archive *) AH, te->defnDumperArg);
+
+		te->defnLen = ahprintf(AH, "%s\n\n", defn);
+		pg_free(defn);
+	}
 	else if (te->defn && strlen(te->defn) > 0)
 	{
 		ahprintf(AH, "%s\n\n", te->defn);
diff --git a/src/bin/pg_dump/pg_backup_archiver.h b/src/bin/pg_dump/pg_backup_archiver.h
index a2064f471ed..b7ebc2b39cd 100644
--- a/src/bin/pg_dump/pg_backup_archiver.h
+++ b/src/bin/pg_dump/pg_backup_archiver.h
@@ -368,6 +368,10 @@ struct _tocEntry
 	const void *dataDumperArg;	/* Arg for above routine */
 	void	   *formatData;		/* TOC Entry data specific to file format */
 
+	DefnDumperPtr defnDumper;	/* routine to dump definition statement */
+	const void *defnDumperArg;	/* arg for above routine */
+	size_t		defnLen;		/* length of dumped definition */
+
 	/* working state while dumping/restoring */
 	pgoff_t		dataLength;		/* item's data size; 0 if none or unknown */
 	int			reqs;			/* do we need schema and/or data of object
@@ -407,6 +411,8 @@ typedef struct _archiveOpts
 	int			nDeps;
 	DataDumperPtr dumpFn;
 	const void *dumpArg;
+	DefnDumperPtr defnFn;
+	const void *defnArg;
 } ArchiveOpts;
 #define ARCHIVE_OPTS(...) &(ArchiveOpts){__VA_ARGS__}
 /* Called to add a TOC entry */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 04c87ba8854..026c8d2779c 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -10554,17 +10554,21 @@ appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
 }
 
 /*
- * dumpRelationStats --
+ * dumpRelationStats_dumper --
  *
- * Dump command to import stats into the relation on the new database.
+ * Generate command to import stats into the relation on the new database.
+ * This routine is called by the Archiver when it wants the statistics to be
+ * dumped.
  */
-static void
-dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+static char *
+dumpRelationStats_dumper(Archive *fout, const void *userArg)
 {
+	const RelStatsInfo *rsinfo = (RelStatsInfo *) userArg;
 	const DumpableObject *dobj = &rsinfo->dobj;
 	PGresult   *res;
 	PQExpBuffer query;
-	PQExpBuffer out;
+	PQExpBufferData out_data;
+	PQExpBuffer out = &out_data;
 	int			i_attname;
 	int			i_inherited;
 	int			i_null_frac;
@@ -10581,10 +10585,6 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 	int			i_range_empty_frac;
 	int			i_range_bounds_histogram;
 
-	/* nothing to do if we are not dumping statistics */
-	if (!fout->dopt->dumpStatistics)
-		return;
-
 	query = createPQExpBuffer();
 	if (!fout->is_prepared[PREPQUERY_GETATTRIBUTESTATS])
 	{
@@ -10620,7 +10620,7 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 		resetPQExpBuffer(query);
 	}
 
-	out = createPQExpBuffer();
+	initPQExpBuffer(out);
 
 	/* restore relation stats */
 	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
@@ -10764,17 +10764,35 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 
 	PQclear(res);
 
+	destroyPQExpBuffer(query);
+	return out->data;
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Make an ArchiveEntry for the relation statistics.  The Archiver will take
+ * care of gathering the statistics and generating the restore commands when
+ * they are needed.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	const DumpableObject *dobj = &rsinfo->dobj;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
 	ArchiveEntry(fout, nilCatalogId, createDumpId(),
 				 ARCHIVE_OPTS(.tag = dobj->name,
 							  .namespace = dobj->namespace->dobj.name,
 							  .description = "STATISTICS DATA",
 							  .section = rsinfo->section,
-							  .createStmt = out->data,
+							  .defnFn = dumpRelationStats_dumper,
+							  .defnArg = rsinfo,
 							  .deps = dobj->dependencies,
 							  .nDeps = dobj->nDeps));
-
-	destroyPQExpBuffer(out);
-	destroyPQExpBuffer(query);
 }
 
 /*
-- 
2.39.5 (Apple Git-154)

v12n5-0003-pg_dump-Retrieve-attribute-statistics-in-batch.patchtext/plain; charset=us-asciiDownload
From 420bd1d32782a713f5ccbc60f15aa93a3a277f7a Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 2 Apr 2025 20:55:12 -0500
Subject: [PATCH v12n5 3/3] pg_dump: Retrieve attribute statistics in batches.

Currently, pg_dump gathers attribute statistics with a query per
relation, which can cause pg_dump to take significantly longer,
especially when there are many tables.  This commit improves
matters by gathering attribute statistics for 64 relations at a
time.  Some simple testing showed this was the ideal batch size,
but performance may vary depending on workload.  This change
increases the memory usage of pg_dump a bit, but that isn't
expected to be too egregious and is arguably well worth the
trade-off.

Our lookahead code for determining the next batch of relations for
which to gather attribute statistics is simple: we walk the TOC
sequentially looking for eligible entries.  However, the assumption
that we will dump all such entries in this order doesn't hold up
for dump formats that use RestoreArchive().  This is because
RestoreArchive() does multiple passes through the TOC and
selectively dumps certain entries each time.  This is particularly
troublesome for index stats and a subset of matview stats; both are
in SECTION_POST_DATA, but matview stats that depend on matview data
are dumped in RESTORE_PASS_POST_ACL, while all other statistics
data is dumped in RESTORE_PASS_MAIN.  To deal with this, this
commit moves all statistics data entries in SECTION_POST_DATA to
RESTORE_PASS_POST_ACL, which ensures that we always dump statistics
data entries in TOC order.  One convenient side effect of this
change is that we can revert a decent chunk of commit a0a4601765.

Author: Corey Huinker <corey.huinker@gmail.com>
Co-authored-by: Nathan Bossart <nathandbossart@gmail.com>
Reviewed-by: Jeff Davis <pgsql@j-davis.com>
Discussion: https://postgr.es/m/CADkLM%3Dc%2Br05srPy9w%2B-%2BnbmLEo15dKXYQ03Q_xyK%2BriJerigLQ%40mail.gmail.com
---
 src/bin/pg_dump/pg_backup.h          |   5 +-
 src/bin/pg_dump/pg_backup_archiver.c |  56 +++++-----
 src/bin/pg_dump/pg_dump.c            | 146 +++++++++++++++++++++++----
 3 files changed, 155 insertions(+), 52 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 781f8fa1cc9..9005b4253b4 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -285,7 +285,10 @@ typedef int DumpId;
  * Function pointer prototypes for assorted callback methods.
  */
 
-typedef char *(*DefnDumperPtr) (Archive *AH, const void *userArg);
+/* forward declaration to avoid including pg_backup_archiver.h here */
+typedef struct _tocEntry TocEntry;
+
+typedef char *(*DefnDumperPtr) (Archive *AH, const void *userArg, const TocEntry *te);
 typedef int (*DataDumperPtr) (Archive *AH, const void *userArg);
 
 typedef void (*SetupWorkerPtrType) (Archive *AH);
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index d7c64583242..7343867584a 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -72,7 +72,7 @@ static void processEncodingEntry(ArchiveHandle *AH, TocEntry *te);
 static void processStdStringsEntry(ArchiveHandle *AH, TocEntry *te);
 static void processSearchPathEntry(ArchiveHandle *AH, TocEntry *te);
 static int	_tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH);
-static RestorePass _tocEntryRestorePass(ArchiveHandle *AH, TocEntry *te);
+static RestorePass _tocEntryRestorePass(TocEntry *te);
 static bool _tocEntryIsACL(TocEntry *te);
 static void _disableTriggersIfNecessary(ArchiveHandle *AH, TocEntry *te);
 static void _enableTriggersIfNecessary(ArchiveHandle *AH, TocEntry *te);
@@ -102,8 +102,7 @@ static void pending_list_append(TocEntry *l, TocEntry *te);
 static void pending_list_remove(TocEntry *te);
 static int	TocEntrySizeCompareQsort(const void *p1, const void *p2);
 static int	TocEntrySizeCompareBinaryheap(void *p1, void *p2, void *arg);
-static void move_to_ready_heap(ArchiveHandle *AH,
-							   TocEntry *pending_list,
+static void move_to_ready_heap(TocEntry *pending_list,
 							   binaryheap *ready_heap,
 							   RestorePass pass);
 static TocEntry *pop_next_work_item(binaryheap *ready_heap,
@@ -749,7 +748,7 @@ RestoreArchive(Archive *AHX)
 			if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) == 0)
 				continue;		/* ignore if not to be dumped at all */
 
-			switch (_tocEntryRestorePass(AH, te))
+			switch (_tocEntryRestorePass(te))
 			{
 				case RESTORE_PASS_MAIN:
 					(void) restore_toc_entry(AH, te, false);
@@ -768,7 +767,7 @@ RestoreArchive(Archive *AHX)
 			for (te = AH->toc->next; te != AH->toc; te = te->next)
 			{
 				if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) != 0 &&
-					_tocEntryRestorePass(AH, te) == RESTORE_PASS_ACL)
+					_tocEntryRestorePass(te) == RESTORE_PASS_ACL)
 					(void) restore_toc_entry(AH, te, false);
 			}
 		}
@@ -778,7 +777,7 @@ RestoreArchive(Archive *AHX)
 			for (te = AH->toc->next; te != AH->toc; te = te->next)
 			{
 				if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) != 0 &&
-					_tocEntryRestorePass(AH, te) == RESTORE_PASS_POST_ACL)
+					_tocEntryRestorePass(te) == RESTORE_PASS_POST_ACL)
 					(void) restore_toc_entry(AH, te, false);
 			}
 		}
@@ -2650,7 +2649,7 @@ WriteToc(ArchiveHandle *AH)
 		}
 		else if (te->defnDumper)
 		{
-			char	   *defn = te->defnDumper((Archive *) AH, te->defnDumperArg);
+			char	   *defn = te->defnDumper((Archive *) AH, te->defnDumperArg, te);
 
 			te->defnLen = WriteStr(AH, defn);
 			pg_free(defn);
@@ -3256,7 +3255,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
  * See notes with the RestorePass typedef in pg_backup_archiver.h.
  */
 static RestorePass
-_tocEntryRestorePass(ArchiveHandle *AH, TocEntry *te)
+_tocEntryRestorePass(TocEntry *te)
 {
 	/* "ACL LANGUAGE" was a crock emitted only in PG 7.4 */
 	if (strcmp(te->desc, "ACL") == 0 ||
@@ -3279,23 +3278,17 @@ _tocEntryRestorePass(ArchiveHandle *AH, TocEntry *te)
 
 	/*
 	 * If statistics data is dependent on materialized view data, it must be
-	 * deferred to RESTORE_PASS_POST_ACL.
+	 * deferred to RESTORE_PASS_POST_ACL.  Those entries are marked with
+	 * SECTION_POST_DATA already, and some other stats entries (e.g., stats
+	 * for indexes) will also be marked SECTION_POST_DATA.  Furthermore, our
+	 * lookahead code in fetchAttributeStats() assumes we dump all statistics
+	 * data entries in TOC order.  To ensure this assumption holds, we move
+	 * all statistics data entries in SECTION_POST_DATA to
+	 * RESTORE_PASS_POST_ACL.
 	 */
-	if (strcmp(te->desc, "STATISTICS DATA") == 0)
-	{
-		for (int i = 0; i < te->nDeps; i++)
-		{
-			DumpId		depid = te->dependencies[i];
-
-			if (depid <= AH->maxDumpId && AH->tocsByDumpId[depid] != NULL)
-			{
-				TocEntry   *otherte = AH->tocsByDumpId[depid];
-
-				if (strcmp(otherte->desc, "MATERIALIZED VIEW DATA") == 0)
-					return RESTORE_PASS_POST_ACL;
-			}
-		}
-	}
+	if (strcmp(te->desc, "STATISTICS DATA") == 0 &&
+		te->section == SECTION_POST_DATA)
+		return RESTORE_PASS_POST_ACL;
 
 	/* All else can be handled in the main pass. */
 	return RESTORE_PASS_MAIN;
@@ -3945,7 +3938,7 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 	}
 	else if (te->defnDumper)
 	{
-		char	   *defn = te->defnDumper((Archive *) AH, te->defnDumperArg);
+		char	   *defn = te->defnDumper((Archive *) AH, te->defnDumperArg, te);
 
 		te->defnLen = ahprintf(AH, "%s\n\n", defn);
 		pg_free(defn);
@@ -4343,7 +4336,7 @@ restore_toc_entries_prefork(ArchiveHandle *AH, TocEntry *pending_list)
 		 * not set skipped_some in this case, since by assumption no main-pass
 		 * items could depend on these.
 		 */
-		if (_tocEntryRestorePass(AH, next_work_item) != RESTORE_PASS_MAIN)
+		if (_tocEntryRestorePass(next_work_item) != RESTORE_PASS_MAIN)
 			do_now = false;
 
 		if (do_now)
@@ -4425,7 +4418,7 @@ restore_toc_entries_parallel(ArchiveHandle *AH, ParallelState *pstate,
 	 * process in the current restore pass.
 	 */
 	AH->restorePass = RESTORE_PASS_MAIN;
-	move_to_ready_heap(AH, pending_list, ready_heap, AH->restorePass);
+	move_to_ready_heap(pending_list, ready_heap, AH->restorePass);
 
 	/*
 	 * main parent loop
@@ -4474,7 +4467,7 @@ restore_toc_entries_parallel(ArchiveHandle *AH, ParallelState *pstate,
 			/* Advance to next restore pass */
 			AH->restorePass++;
 			/* That probably allows some stuff to be made ready */
-			move_to_ready_heap(AH, pending_list, ready_heap, AH->restorePass);
+			move_to_ready_heap(pending_list, ready_heap, AH->restorePass);
 			/* Loop around to see if anything's now ready */
 			continue;
 		}
@@ -4645,8 +4638,7 @@ TocEntrySizeCompareBinaryheap(void *p1, void *p2, void *arg)
  * which applies the same logic one-at-a-time.)
  */
 static void
-move_to_ready_heap(ArchiveHandle *AH,
-				   TocEntry *pending_list,
+move_to_ready_heap(TocEntry *pending_list,
 				   binaryheap *ready_heap,
 				   RestorePass pass)
 {
@@ -4659,7 +4651,7 @@ move_to_ready_heap(ArchiveHandle *AH,
 		next_te = te->pending_next;
 
 		if (te->depCount == 0 &&
-			_tocEntryRestorePass(AH, te) == pass)
+			_tocEntryRestorePass(te) == pass)
 		{
 			/* Remove it from pending_list ... */
 			pending_list_remove(te);
@@ -5053,7 +5045,7 @@ reduce_dependencies(ArchiveHandle *AH, TocEntry *te,
 		 * memberships changed.
 		 */
 		if (otherte->depCount == 0 &&
-			_tocEntryRestorePass(AH, otherte) == AH->restorePass &&
+			_tocEntryRestorePass(otherte) == AH->restorePass &&
 			otherte->pending_prev != NULL &&
 			ready_heap != NULL)
 		{
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 026c8d2779c..1b8303cd0ce 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -209,6 +209,9 @@ static int	nbinaryUpgradeClassOids = 0;
 static SequenceItem *sequences = NULL;
 static int	nsequences = 0;
 
+/* Maximum number of relations to fetch in a fetchAttributeStats() call. */
+#define MAX_ATTR_STATS_RELS 64
+
 /*
  * The default number of rows per INSERT when
  * --inserts is specified without --rows-per-insert
@@ -10553,6 +10556,78 @@ appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
 	appendPQExpBuffer(out, "::%s", argtype);
 }
 
+/*
+ * fetchAttributeStats --
+ *
+ * Fetch next batch of rows for getAttributeStats().
+ */
+static PGresult *
+fetchAttributeStats(Archive *fout)
+{
+	ArchiveHandle *AH = (ArchiveHandle *) fout;
+	PQExpBuffer nspnames = createPQExpBuffer();
+	PQExpBuffer relnames = createPQExpBuffer();
+	int			count = 0;
+	PGresult   *res = NULL;
+	static TocEntry *te;
+	static bool restarted;
+
+	/* If we're just starting, set our TOC pointer. */
+	if (!te)
+		te = AH->toc->next;
+
+	/*
+	 * We can't avoid a second TOC scan for the tar format because it writes
+	 * restore.sql separately, which means we must execute all of our queries
+	 * a second time.  This feels risky, but there is no known reason it
+	 * should generate different output than the first pass.  Even if it does,
+	 * the worst case is that restore.sql might have different statistics data
+	 * than the archive.
+	 */
+	if (!restarted && te == AH->toc && AH->format == archTar)
+	{
+		te = AH->toc->next;
+		restarted = true;
+	}
+
+	/*
+	 * Scan the TOC for the next set of relevant stats entries.  We assume
+	 * that statistics are dumped in the order they are listed in the TOC.
+	 * This is perhaps not the sturdiest assumption, so we verify it matches
+	 * reality in dumpRelationStats_dumper().
+	 */
+	for (; te != AH->toc && count < MAX_ATTR_STATS_RELS; te = te->next)
+	{
+		if (te->reqs && strcmp(te->desc, "STATISTICS DATA") == 0)
+		{
+			RelStatsInfo *rsinfo = (RelStatsInfo *) te->defnDumperArg;
+
+			appendPQExpBuffer(nspnames, "%s%s", count ? "," : "",
+							  fmtId(rsinfo->dobj.namespace->dobj.name));
+			appendPQExpBuffer(relnames, "%s%s", count ? "," : "",
+							  fmtId(rsinfo->dobj.name));
+			count++;
+		}
+	}
+
+	/* Execute the query for the next batch of relations. */
+	if (count > 0)
+	{
+		PQExpBuffer query = createPQExpBuffer();
+
+		appendPQExpBuffer(query, "EXECUTE getAttributeStats("
+						  "'{%s}'::pg_catalog.name[],"
+						  "'{%s}'::pg_catalog.name[])",
+						  nspnames->data, relnames->data);
+		res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+		destroyPQExpBuffer(query);
+	}
+
+	destroyPQExpBuffer(nspnames);
+	destroyPQExpBuffer(relnames);
+	return res;
+}
+
 /*
  * dumpRelationStats_dumper --
  *
@@ -10561,14 +10636,17 @@ appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
  * dumped.
  */
 static char *
-dumpRelationStats_dumper(Archive *fout, const void *userArg)
+dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 {
 	const RelStatsInfo *rsinfo = (RelStatsInfo *) userArg;
 	const DumpableObject *dobj = &rsinfo->dobj;
-	PGresult   *res;
+	static PGresult *res;
+	static int	rownum;
 	PQExpBuffer query;
 	PQExpBufferData out_data;
 	PQExpBuffer out = &out_data;
+	int			i_schemaname;
+	int			i_tablename;
 	int			i_attname;
 	int			i_inherited;
 	int			i_null_frac;
@@ -10584,13 +10662,30 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 	int			i_range_length_histogram;
 	int			i_range_empty_frac;
 	int			i_range_bounds_histogram;
+	static TocEntry *next_te;
+
+	/*
+	 * fetchAttributeStats() assumes that the statistics are dumped in the
+	 * order they are listed in the TOC.  We verify that here for safety.
+	 */
+	if (!next_te)
+		next_te = ((ArchiveHandle *) fout)->toc;
+
+	next_te = next_te->next;
+	while (!next_te->reqs || strcmp(next_te->desc, "STATISTICS DATA") != 0)
+		next_te = next_te->next;
+
+	if (te != next_te)
+		pg_fatal("stats dumped out of order (current: %d %s %s) (expected: %d %s %s)",
+				 te->dumpId, te->desc, te->tag,
+				 next_te->dumpId, next_te->desc, next_te->tag);
 
 	query = createPQExpBuffer();
 	if (!fout->is_prepared[PREPQUERY_GETATTRIBUTESTATS])
 	{
 		appendPQExpBufferStr(query,
-							 "PREPARE getAttributeStats(pg_catalog.name, pg_catalog.name) AS\n"
-							 "SELECT s.attname, s.inherited, "
+							 "PREPARE getAttributeStats(pg_catalog.name[], pg_catalog.name[]) AS\n"
+							 "SELECT s.schemaname, s.tablename, s.attname, s.inherited, "
 							 "s.null_frac, s.avg_width, s.n_distinct, "
 							 "s.most_common_vals, s.most_common_freqs, "
 							 "s.histogram_bounds, s.correlation, "
@@ -10608,11 +10703,21 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 								 "NULL AS range_empty_frac,"
 								 "NULL AS range_bounds_histogram ");
 
+		/*
+		 * The results must be in the order of the relations supplied in the
+		 * parameters to ensure we remain in sync as we walk through the TOC.
+		 * The redundant filter clause on s.tablename = ANY(...) seems
+		 * sufficient to convince the planner to use the
+		 * pg_class_relname_nsp_index, which avoids an full scan of pg_stats.
+		 * This may not work for all versions.
+		 */
 		appendPQExpBufferStr(query,
 							 "FROM pg_catalog.pg_stats s "
-							 "WHERE s.schemaname = $1 "
-							 "AND s.tablename = $2 "
-							 "ORDER BY s.attname, s.inherited");
+							 "JOIN unnest($1, $2) WITH ORDINALITY AS u (schemaname, tablename, ord) "
+							 "ON s.schemaname = u.schemaname "
+							 "AND s.tablename = u.tablename "
+							 "WHERE s.tablename = ANY($2) "
+							 "ORDER BY u.ord, s.attname, s.inherited");
 
 		ExecuteSqlStatement(fout, query->data);
 
@@ -10642,16 +10747,16 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 
 	appendPQExpBufferStr(out, "\n);\n");
 
+	/* Fetch the next batch of attribute statistics if needed. */
+	if (rownum >= PQntuples(res))
+	{
+		PQclear(res);
+		res = fetchAttributeStats(fout);
+		rownum = 0;
+	}
 
-	/* fetch attribute stats */
-	appendPQExpBufferStr(query, "EXECUTE getAttributeStats(");
-	appendStringLiteralAH(query, dobj->namespace->dobj.name, fout);
-	appendPQExpBufferStr(query, ", ");
-	appendStringLiteralAH(query, dobj->name, fout);
-	appendPQExpBufferStr(query, ");");
-
-	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
-
+	i_schemaname = PQfnumber(res, "schemaname");
+	i_tablename = PQfnumber(res, "tablename");
 	i_attname = PQfnumber(res, "attname");
 	i_inherited = PQfnumber(res, "inherited");
 	i_null_frac = PQfnumber(res, "null_frac");
@@ -10669,10 +10774,15 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 	i_range_bounds_histogram = PQfnumber(res, "range_bounds_histogram");
 
 	/* restore attribute stats */
-	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	for (; rownum < PQntuples(res); rownum++)
 	{
 		const char *attname;
 
+		/* Stop if the next stat row in our cache isn't for this relation. */
+		if (strcmp(dobj->name, PQgetvalue(res, rownum, i_tablename)) != 0 ||
+			strcmp(dobj->namespace->dobj.name, PQgetvalue(res, rownum, i_schemaname)) != 0)
+			break;
+
 		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
 		appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
 						  fout->remoteVersion);
@@ -10762,8 +10872,6 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 		appendPQExpBufferStr(out, "\n);\n");
 	}
 
-	PQclear(res);
-
 	destroyPQExpBuffer(query);
 	return out->data;
 }
-- 
2.39.5 (Apple Git-154)

#519Corey Huinker
corey.huinker@gmail.com
In reply to: Nathan Bossart (#514)
Re: Statistics Import and Export

Also, why do we need the clause "WHERE s.tablename = ANY($2)"? Isn't
that already implied by "JOIN unnest($1, $2) ... s.tablename =
u.tablename"?

Good question. Corey, do you recall why this was needed?

In my patch, that SQL statement came with the comment:

+ /*
+ * The results must be in the order of relations supplied in the
+ * parameters to ensure that they are in sync with a walk of the TOC.
+ *
+ * The redundant (and incomplete) filter clause on s.tablename = ANY(...)
+ * is a way to lead the query into using the index
+ * pg_class_relname_nsp_index which in turn allows the planner to avoid an
+ * expensive full scan of pg_stats.
+ *
+ * We may need to adjust this query for versions that are not so easily
+ * led.
+ */
#520Nathan Bossart
nathandbossart@gmail.com
In reply to: Corey Huinker (#519)
Re: Statistics Import and Export

On Wed, Apr 02, 2025 at 10:34:58PM -0400, Corey Huinker wrote:

Also, why do we need the clause "WHERE s.tablename = ANY($2)"? Isn't
that already implied by "JOIN unnest($1, $2) ... s.tablename =
u.tablename"?

Good question. Corey, do you recall why this was needed?

In my patch, that SQL statement came with the comment:

+ /*
+ * The results must be in the order of relations supplied in the
+ * parameters to ensure that they are in sync with a walk of the TOC.
+ *
+ * The redundant (and incomplete) filter clause on s.tablename = ANY(...)
+ * is a way to lead the query into using the index
+ * pg_class_relname_nsp_index which in turn allows the planner to avoid an
+ * expensive full scan of pg_stats.
+ *
+ * We may need to adjust this query for versions that are not so easily
+ * led.
+ */

Thanks. I included that in the latest patch set.

--
nathan

#521Jeff Davis
pgsql@j-davis.com
In reply to: Nathan Bossart (#518)
Re: Statistics Import and Export

On Wed, 2025-04-02 at 21:26 -0500, Nathan Bossart wrote:

Okay, here is an updated patch set.

* Besides custom format calling WriteToc() twice to update the data
  offsets, tar format ... even if it did, the worst case is that
the
  content of restore.sql (which isn't used by pg_restore) would be
  different.  I noticed some past discussion that seems to suggest
that
  this format might be a candidate for deprecation [0], so I'm not
sure
  it's worth doing anything fancier.

I agree that the risk for tar format seems much lower.

* Our batching code assumes that stats entries are dumped in TOC
order,

...

I propose moving all
  stats entries in SECTION_POST_DATA to RESTORE_PASS_POST_ACL, which
  ensures that we always dump stats in TOC order.  One convenient
side
  effect of this change is that we can revert a decent chunk of
commit
  a0a4601765.  It might be possible to do better via smarter
lookahead code
  or a more sophisticated cache, but it's a bit late in the game for
that.

This simplifies commit a0a4601765. I'd break out that simplification as
a separate commit to make it easier to understand what happened.

In patch 0003, there are quite a few static function-scoped variables,
which is not a style that I'm used to. One idea is to bundle them into
a struct representing the cache state (including enough information to
fetch the next batch), and have a single static variable that points to
that.

Also in 0003, the "next_te" variable is a bit confusing, because it's
actually the last TocEntry, until it's advanced to point to the current
one.

Other than that, looks good to me.

Regards,
Jeff Davis

#522Nathan Bossart
nathandbossart@gmail.com
In reply to: Jeff Davis (#521)
4 attachment(s)
Re: Statistics Import and Export

Thanks for reviewing.

On Thu, Apr 03, 2025 at 03:23:40PM -0700, Jeff Davis wrote:

This simplifies commit a0a4601765. I'd break out that simplification as
a separate commit to make it easier to understand what happened.

Done.

In patch 0003, there are quite a few static function-scoped variables,
which is not a style that I'm used to. One idea is to bundle them into
a struct representing the cache state (including enough information to
fetch the next batch), and have a single static variable that points to
that.

As discussed off-list, I didn't take this suggestion for now. Corey did
this originally, and I converted it to static function-scoped variables 1)
to reduce patch size and 2) because I noticed that each of the state
variables were only needed in one function. I agree that a struct might be
slightly more readable, but we can always change this in the future if
desired.

Also in 0003, the "next_te" variable is a bit confusing, because it's
actually the last TocEntry, until it's advanced to point to the current
one.

I've renamed it to expected_te.

Other than that, looks good to me.

Great. I'm planning to commit the attached patch set tomorrow morning.

For the record, I spent most of today trying very hard to fix the layering
violations in 0002. While I was successful, the result was awkward,
complicated, and nigh unreadable. This is now the second time I've
attempted to fix this and have felt the result was worse than where I
started. So, I added extremely descriptive comments instead. I'm hoping
that it will be possible to clean this up with some additional work in v19.
I have a few ideas, but if anyone has suggestions, I'm all ears.

--
nathan

Attachments:

v12n6-0001-Skip-second-WriteToc-call-for-custom-format-du.patchtext/plain; charset=us-asciiDownload
From a5a31f2754bf69a81fdc48769c1ee950317a2cf0 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Thu, 3 Apr 2025 13:20:28 -0500
Subject: [PATCH v12n6 1/4] Skip second WriteToc() call for custom-format dumps
 without data.

Presently, "pg_dump --format=custom" calls WriteToc() twice.  The
second call updates the data offset information, which allegedly
makes parallel pg_restore significantly faster.  However, if we're
not dumping any data, there are no data offsets to update, so we
can skip this step.

Reviewed-by: Jeff Davis <pgsql@j-davis.com>
Discussion: https://postgr.es/m/Z9c1rbzZegYQTOQE%40nathan
---
 src/bin/pg_dump/pg_backup_custom.c | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup_custom.c b/src/bin/pg_dump/pg_backup_custom.c
index e44b887eb29..f7c3af56304 100644
--- a/src/bin/pg_dump/pg_backup_custom.c
+++ b/src/bin/pg_dump/pg_backup_custom.c
@@ -755,9 +755,11 @@ _CloseArchive(ArchiveHandle *AH)
 		 * If possible, re-write the TOC in order to update the data offset
 		 * information.  This is not essential, as pg_restore can cope in most
 		 * cases without it; but it can make pg_restore significantly faster
-		 * in some situations (especially parallel restore).
+		 * in some situations (especially parallel restore).  We can skip this
+		 * step if we're not dumping any data; there are no offsets to update
+		 * in that case.
 		 */
-		if (ctx->hasSeek &&
+		if (ctx->hasSeek && AH->public.dopt->dumpData &&
 			fseeko(AH->FH, tpos, SEEK_SET) == 0)
 			WriteToc(AH);
 	}
-- 
2.39.5 (Apple Git-154)

v12n6-0002-pg_dump-Reduce-memory-usage-of-dumps-with-stat.patchtext/plain; charset=us-asciiDownload
From 0244b3f02e083e6c37a2c282292d4d8fa0a69fed Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Thu, 3 Apr 2025 17:15:51 -0500
Subject: [PATCH v12n6 2/4] pg_dump: Reduce memory usage of dumps with
 statistics.

Right now, pg_dump stores all generated commands for statistics in
memory.  These commands can be quite large and therefore can
significantly increase pg_dump's memory footprint.  To fix, wait
until we are about to write out the commands before generating
them, and be sure to free the commands after writing.  This is
implemented via a new defnDumper callback that works much like the
dataDumper one but is specifically designed for TOC entries.

Custom dumps that include data might write the TOC twice (to update
data offset information), which would ordinarily cause pg_dump to
run the attribute statistics queries twice.  However, as a hack, we
save the length of the written-out entry in the first pass and skip
over it in the second.  While there is no known technical issue
with executing the queries multiple times and rewriting the
results, it's expensive and feels risky, so let's avoid it.

As an exception, we _do_ execute the queries twice for the tar
format.  This format does a second pass through the TOC to generate
the restore.sql file.  pg_restore doesn't use this file, so even if
the second round of queries returns different results than the
first, it won't corrupt the output; the archive and restore.sql
file will just have different content.

Author: Corey Huinker <corey.huinker@gmail.com>
Co-authored-by: Nathan Bossart <nathandbossart@gmail.com>
Reviewed-by: Jeff Davis <pgsql@j-davis.com>
Discussion: https://postgr.es/m/CADkLM%3Dc%2Br05srPy9w%2B-%2BnbmLEo15dKXYQ03Q_xyK%2BriJerigLQ%40mail.gmail.com
---
 src/bin/pg_dump/pg_backup.h          |  1 +
 src/bin/pg_dump/pg_backup_archiver.c | 84 +++++++++++++++++++++++++++-
 src/bin/pg_dump/pg_backup_archiver.h |  6 ++
 src/bin/pg_dump/pg_dump.c            | 46 ++++++++++-----
 4 files changed, 121 insertions(+), 16 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 658986de6f8..781f8fa1cc9 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -285,6 +285,7 @@ typedef int DumpId;
  * Function pointer prototypes for assorted callback methods.
  */
 
+typedef char *(*DefnDumperPtr) (Archive *AH, const void *userArg);
 typedef int (*DataDumperPtr) (Archive *AH, const void *userArg);
 
 typedef void (*SetupWorkerPtrType) (Archive *AH);
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 1d131e5a57d..3debd0892c7 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -1266,6 +1266,9 @@ ArchiveEntry(Archive *AHX, CatalogId catalogId, DumpId dumpId,
 	newToc->dataDumperArg = opts->dumpArg;
 	newToc->hadDumper = opts->dumpFn ? true : false;
 
+	newToc->defnDumper = opts->defnFn;
+	newToc->defnDumperArg = opts->defnArg;
+
 	newToc->formatData = NULL;
 	newToc->dataLength = 0;
 
@@ -2621,7 +2624,45 @@ WriteToc(ArchiveHandle *AH)
 		WriteStr(AH, te->tag);
 		WriteStr(AH, te->desc);
 		WriteInt(AH, te->section);
-		WriteStr(AH, te->defn);
+
+		if (te->defnLen)
+		{
+			/*
+			 * defnLen should only be set for custom format's second call to
+			 * WriteToc(), which rewrites the TOC in place to update data
+			 * offsets.  Instead of calling the defnDumper a second time
+			 * (which could involve re-executing queries), just skip writing
+			 * the entry.  While regenerating the definition should
+			 * theoretically produce the same result as before, it's expensive
+			 * and feels risky.
+			 *
+			 * The custom format only calls WriteToc() a second time if
+			 * fseeko() is usable (see _CloseArchive() in pg_backup_custom.c),
+			 * so we can safely use it without checking.  For other formats,
+			 * we fail because one of our assumptions must no longer hold
+			 * true.
+			 *
+			 * XXX This is certainly a layering violation, but the alternative
+			 * is an awkward and complicated callback infrastructure for this
+			 * special case.  This might be worth revisiting in the future.
+			 */
+			if (AH->format != archCustom)
+				pg_fatal("unexpected TOC entry in WriteToc(): %d %s %s",
+						 te->dumpId, te->desc, te->tag);
+
+			if (fseeko(AH->FH, te->defnLen, SEEK_CUR != 0))
+				pg_fatal("error during file seek: %m");
+		}
+		else if (te->defnDumper)
+		{
+			char	   *defn = te->defnDumper((Archive *) AH, te->defnDumperArg);
+
+			te->defnLen = WriteStr(AH, defn);
+			pg_free(defn);
+		}
+		else
+			WriteStr(AH, te->defn);
+
 		WriteStr(AH, te->dropStmt);
 		WriteStr(AH, te->copyStmt);
 		WriteStr(AH, te->namespace);
@@ -3849,7 +3890,7 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 
 	/*
 	 * Actually print the definition.  Normally we can just print the defn
-	 * string if any, but we have three special cases:
+	 * string if any, but we have four special cases:
 	 *
 	 * 1. A crude hack for suppressing AUTHORIZATION clause that old pg_dump
 	 * versions put into CREATE SCHEMA.  Don't mutate the variant for schema
@@ -3862,6 +3903,11 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 	 * 3. ACL LARGE OBJECTS entries need special processing because they
 	 * contain only one copy of the ACL GRANT/REVOKE commands, which we must
 	 * apply to each large object listed in the associated BLOB METADATA.
+	 *
+	 * 4. Entries with a defnDumper need to call it to generate the
+	 * definition.  This is primarily intended to provide a way to save memory
+	 * for objects that would otherwise need a lot of it (e.g., statistics
+	 * data).
 	 */
 	if (ropt->noOwner &&
 		strcmp(te->desc, "SCHEMA") == 0 && strncmp(te->defn, "--", 2) != 0)
@@ -3877,6 +3923,40 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 	{
 		IssueACLPerBlob(AH, te);
 	}
+	else if (te->defnLen && AH->format != archTar)
+	{
+		/*
+		 * If defnLen is set, the defnDumper has already been called for this
+		 * TOC entry.  We don't normally expect a defnDumper to be called for
+		 * a TOC entry a second time in _printTocEntry(), but there's an
+		 * exception.  The tar format first calls WriteToc(), which scans the
+		 * entire TOC, and then it later calls RestoreArchive() to generate
+		 * restore.sql, which scans the TOC again.  There doesn't appear to be
+		 * a good way to prevent a second defnDumper call in this case without
+		 * storing the definition in memory, which defeats the purpose.  This
+		 * second defnDumper invocation should generate the same output as the
+		 * first, but even if it doesn't, the worst-case scenario is that the
+		 * content of the archive and restore.sql (which isn't used by
+		 * pg_restore) will differ.
+		 *
+		 * In all other cases, encountering a TOC entry a second time in
+		 * _printTocEntry() is unexpected, so we fail because one of our
+		 * assumptions must no longer hold true.
+		 *
+		 * XXX This is certainly a layering violation, but the alternative is
+		 * an awkward and complicated callback infrastructure for this special
+		 * case.  This might be worth revisiting in the future.
+		 */
+		pg_fatal("unexpected TOC entry in _printTocEntry(): %d %s %s",
+				 te->dumpId, te->desc, te->tag);
+	}
+	else if (te->defnDumper)
+	{
+		char	   *defn = te->defnDumper((Archive *) AH, te->defnDumperArg);
+
+		te->defnLen = ahprintf(AH, "%s\n\n", defn);
+		pg_free(defn);
+	}
 	else if (te->defn && strlen(te->defn) > 0)
 	{
 		ahprintf(AH, "%s\n\n", te->defn);
diff --git a/src/bin/pg_dump/pg_backup_archiver.h b/src/bin/pg_dump/pg_backup_archiver.h
index a2064f471ed..b7ebc2b39cd 100644
--- a/src/bin/pg_dump/pg_backup_archiver.h
+++ b/src/bin/pg_dump/pg_backup_archiver.h
@@ -368,6 +368,10 @@ struct _tocEntry
 	const void *dataDumperArg;	/* Arg for above routine */
 	void	   *formatData;		/* TOC Entry data specific to file format */
 
+	DefnDumperPtr defnDumper;	/* routine to dump definition statement */
+	const void *defnDumperArg;	/* arg for above routine */
+	size_t		defnLen;		/* length of dumped definition */
+
 	/* working state while dumping/restoring */
 	pgoff_t		dataLength;		/* item's data size; 0 if none or unknown */
 	int			reqs;			/* do we need schema and/or data of object
@@ -407,6 +411,8 @@ typedef struct _archiveOpts
 	int			nDeps;
 	DataDumperPtr dumpFn;
 	const void *dumpArg;
+	DefnDumperPtr defnFn;
+	const void *defnArg;
 } ArchiveOpts;
 #define ARCHIVE_OPTS(...) &(ArchiveOpts){__VA_ARGS__}
 /* Called to add a TOC entry */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index eef74f78271..77fce7c8738 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -10554,17 +10554,21 @@ appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
 }
 
 /*
- * dumpRelationStats --
+ * dumpRelationStats_dumper --
  *
- * Dump command to import stats into the relation on the new database.
+ * Generate command to import stats into the relation on the new database.
+ * This routine is called by the Archiver when it wants the statistics to be
+ * dumped.
  */
-static void
-dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+static char *
+dumpRelationStats_dumper(Archive *fout, const void *userArg)
 {
+	const RelStatsInfo *rsinfo = (RelStatsInfo *) userArg;
 	const DumpableObject *dobj = &rsinfo->dobj;
 	PGresult   *res;
 	PQExpBuffer query;
-	PQExpBuffer out;
+	PQExpBufferData out_data;
+	PQExpBuffer out = &out_data;
 	int			i_attname;
 	int			i_inherited;
 	int			i_null_frac;
@@ -10581,10 +10585,6 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 	int			i_range_empty_frac;
 	int			i_range_bounds_histogram;
 
-	/* nothing to do if we are not dumping statistics */
-	if (!fout->dopt->dumpStatistics)
-		return;
-
 	query = createPQExpBuffer();
 	if (!fout->is_prepared[PREPQUERY_GETATTRIBUTESTATS])
 	{
@@ -10620,7 +10620,7 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 		resetPQExpBuffer(query);
 	}
 
-	out = createPQExpBuffer();
+	initPQExpBuffer(out);
 
 	/* restore relation stats */
 	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
@@ -10764,17 +10764,35 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 
 	PQclear(res);
 
+	destroyPQExpBuffer(query);
+	return out->data;
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Make an ArchiveEntry for the relation statistics.  The Archiver will take
+ * care of gathering the statistics and generating the restore commands when
+ * they are needed.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	const DumpableObject *dobj = &rsinfo->dobj;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
 	ArchiveEntry(fout, nilCatalogId, createDumpId(),
 				 ARCHIVE_OPTS(.tag = dobj->name,
 							  .namespace = dobj->namespace->dobj.name,
 							  .description = "STATISTICS DATA",
 							  .section = rsinfo->section,
-							  .createStmt = out->data,
+							  .defnFn = dumpRelationStats_dumper,
+							  .defnArg = rsinfo,
 							  .deps = dobj->dependencies,
 							  .nDeps = dobj->nDeps));
-
-	destroyPQExpBuffer(out);
-	destroyPQExpBuffer(query);
 }
 
 /*
-- 
2.39.5 (Apple Git-154)

v12n6-0003-pg_dump-Retrieve-attribute-statistics-in-batch.patchtext/plain; charset=us-asciiDownload
From 8e63be0265c5489818043696e3880790ffcee374 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Thu, 3 Apr 2025 17:20:01 -0500
Subject: [PATCH v12n6 3/4] pg_dump: Retrieve attribute statistics in batches.

Currently, pg_dump gathers attribute statistics with a query per
relation, which can cause pg_dump to take significantly longer,
especially when there are many tables.  This commit address this by
teaching pg_dump to gather attribute statistics for 64 relations at
a time.  Some simple tests showed this was the optimal batch size,
but performance may vary depending on the workload.  While this
change increases pg_dump's memory usage a bit, it isn't expected to
be too egregious and seems well worth the trade-off.

Our lookahead code determines the next batch of relations by
searching the TOC sequentially for relevant entries.  This approach
assumes that we will dump all such entries in TOC order, which
unfortunately isn't true for dump formats that use
RestoreArchive().  RestoreArchive() does multiple passes through
the TOC and selectively dumps certain groups of entries each time.
This is particularly problematic for index stats and a subset of
matview stats; both are in SECTION_POST_DATA, but matview stats
that depend on matview data are dumped in RESTORE_PASS_POST_ACL,
while all other stats data entries are dumped in RESTORE_PASS_MAIN.
To handle this, this commit moves all statistics data entries in
SECTION_POST_DATA to RESTORE_PASS_POST_ACL, which ensures that we
always dump them in TOC order.  A convenient side effect of this
change is that we can revert a decent chunk of commit a0a4601765,
but that is left for a follow-up commit.

Author: Corey Huinker <corey.huinker@gmail.com>
Co-authored-by: Nathan Bossart <nathandbossart@gmail.com>
Reviewed-by: Jeff Davis <pgsql@j-davis.com>
Discussion: https://postgr.es/m/CADkLM%3Dc%2Br05srPy9w%2B-%2BnbmLEo15dKXYQ03Q_xyK%2BriJerigLQ%40mail.gmail.com
---
 src/bin/pg_dump/pg_backup.h          |   5 +-
 src/bin/pg_dump/pg_backup_archiver.c |  30 +++---
 src/bin/pg_dump/pg_dump.c            | 148 +++++++++++++++++++++++----
 3 files changed, 145 insertions(+), 38 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 781f8fa1cc9..9005b4253b4 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -285,7 +285,10 @@ typedef int DumpId;
  * Function pointer prototypes for assorted callback methods.
  */
 
-typedef char *(*DefnDumperPtr) (Archive *AH, const void *userArg);
+/* forward declaration to avoid including pg_backup_archiver.h here */
+typedef struct _tocEntry TocEntry;
+
+typedef char *(*DefnDumperPtr) (Archive *AH, const void *userArg, const TocEntry *te);
 typedef int (*DataDumperPtr) (Archive *AH, const void *userArg);
 
 typedef void (*SetupWorkerPtrType) (Archive *AH);
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 3debd0892c7..84f32aef411 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2655,7 +2655,7 @@ WriteToc(ArchiveHandle *AH)
 		}
 		else if (te->defnDumper)
 		{
-			char	   *defn = te->defnDumper((Archive *) AH, te->defnDumperArg);
+			char	   *defn = te->defnDumper((Archive *) AH, te->defnDumperArg, te);
 
 			te->defnLen = WriteStr(AH, defn);
 			pg_free(defn);
@@ -3284,23 +3284,17 @@ _tocEntryRestorePass(ArchiveHandle *AH, TocEntry *te)
 
 	/*
 	 * If statistics data is dependent on materialized view data, it must be
-	 * deferred to RESTORE_PASS_POST_ACL.
+	 * deferred to RESTORE_PASS_POST_ACL.  Those entries are already marked
+	 * with SECTION_POST_DATA, and some other stats entries (e.g., index
+	 * stats) will also be marked SECTION_POST_DATA.  Additionally, our
+	 * lookahead code in fetchAttributeStats() assumes that we dump all
+	 * statistics data entries in TOC order.  To ensure this assumption holds,
+	 * we move all statistics data entries in SECTION_POST_DATA to
+	 * RESTORE_PASS_POST_ACL.
 	 */
-	if (strcmp(te->desc, "STATISTICS DATA") == 0)
-	{
-		for (int i = 0; i < te->nDeps; i++)
-		{
-			DumpId		depid = te->dependencies[i];
-
-			if (depid <= AH->maxDumpId && AH->tocsByDumpId[depid] != NULL)
-			{
-				TocEntry   *otherte = AH->tocsByDumpId[depid];
-
-				if (strcmp(otherte->desc, "MATERIALIZED VIEW DATA") == 0)
-					return RESTORE_PASS_POST_ACL;
-			}
-		}
-	}
+	if (strcmp(te->desc, "STATISTICS DATA") == 0 &&
+		te->section == SECTION_POST_DATA)
+		return RESTORE_PASS_POST_ACL;
 
 	/* All else can be handled in the main pass. */
 	return RESTORE_PASS_MAIN;
@@ -3952,7 +3946,7 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 	}
 	else if (te->defnDumper)
 	{
-		char	   *defn = te->defnDumper((Archive *) AH, te->defnDumperArg);
+		char	   *defn = te->defnDumper((Archive *) AH, te->defnDumperArg, te);
 
 		te->defnLen = ahprintf(AH, "%s\n\n", defn);
 		pg_free(defn);
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 77fce7c8738..a3bcd9c019a 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -209,6 +209,9 @@ static int	nbinaryUpgradeClassOids = 0;
 static SequenceItem *sequences = NULL;
 static int	nsequences = 0;
 
+/* Maximum number of relations to fetch in a fetchAttributeStats() call. */
+#define MAX_ATTR_STATS_RELS 64
+
 /*
  * The default number of rows per INSERT when
  * --inserts is specified without --rows-per-insert
@@ -10553,6 +10556,79 @@ appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
 	appendPQExpBuffer(out, "::%s", argtype);
 }
 
+/*
+ * fetchAttributeStats --
+ *
+ * Fetch next batch of rows for getAttributeStats().
+ */
+static PGresult *
+fetchAttributeStats(Archive *fout)
+{
+	ArchiveHandle *AH = (ArchiveHandle *) fout;
+	PQExpBuffer nspnames = createPQExpBuffer();
+	PQExpBuffer relnames = createPQExpBuffer();
+	int			count = 0;
+	PGresult   *res = NULL;
+	static TocEntry *te;
+	static bool restarted;
+
+	/* If we're just starting, set our TOC pointer. */
+	if (!te)
+		te = AH->toc->next;
+
+	/*
+	 * We can't easily avoid a second TOC scan for the tar format because it
+	 * writes restore.sql separately, which means we must execute the queries
+	 * twice.  This feels risky, but there is no known reason it should
+	 * generate different output than the first pass.  Even if it does, the
+	 * worst-case scenario is that restore.sql might have different statistics
+	 * data than the archive.
+	 */
+	if (!restarted && te == AH->toc && AH->format == archTar)
+	{
+		te = AH->toc->next;
+		restarted = true;
+	}
+
+	/*
+	 * Scan the TOC for the next set of relevant stats entries.  We assume
+	 * that statistics are dumped in the order they are listed in the TOC.
+	 * This is perhaps not the sturdiest assumption, so we verify it matches
+	 * reality in dumpRelationStats_dumper().
+	 */
+	for (; te != AH->toc && count < MAX_ATTR_STATS_RELS; te = te->next)
+	{
+		if ((te->reqs & REQ_STATS) != 0 &&
+			strcmp(te->desc, "STATISTICS DATA") == 0)
+		{
+			RelStatsInfo *rsinfo = (RelStatsInfo *) te->defnDumperArg;
+
+			appendPQExpBuffer(nspnames, "%s%s", count ? "," : "",
+							  fmtId(rsinfo->dobj.namespace->dobj.name));
+			appendPQExpBuffer(relnames, "%s%s", count ? "," : "",
+							  fmtId(rsinfo->dobj.name));
+			count++;
+		}
+	}
+
+	/* Execute the query for the next batch of relations. */
+	if (count > 0)
+	{
+		PQExpBuffer query = createPQExpBuffer();
+
+		appendPQExpBuffer(query, "EXECUTE getAttributeStats("
+						  "'{%s}'::pg_catalog.name[],"
+						  "'{%s}'::pg_catalog.name[])",
+						  nspnames->data, relnames->data);
+		res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+		destroyPQExpBuffer(query);
+	}
+
+	destroyPQExpBuffer(nspnames);
+	destroyPQExpBuffer(relnames);
+	return res;
+}
+
 /*
  * dumpRelationStats_dumper --
  *
@@ -10561,14 +10637,17 @@ appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
  * dumped.
  */
 static char *
-dumpRelationStats_dumper(Archive *fout, const void *userArg)
+dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 {
 	const RelStatsInfo *rsinfo = (RelStatsInfo *) userArg;
 	const DumpableObject *dobj = &rsinfo->dobj;
-	PGresult   *res;
+	static PGresult *res;
+	static int	rownum;
 	PQExpBuffer query;
 	PQExpBufferData out_data;
 	PQExpBuffer out = &out_data;
+	int			i_schemaname;
+	int			i_tablename;
 	int			i_attname;
 	int			i_inherited;
 	int			i_null_frac;
@@ -10584,13 +10663,31 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 	int			i_range_length_histogram;
 	int			i_range_empty_frac;
 	int			i_range_bounds_histogram;
+	static TocEntry *expected_te;
+
+	/*
+	 * fetchAttributeStats() assumes that the statistics are dumped in the
+	 * order they are listed in the TOC.  We verify that here for safety.
+	 */
+	if (!expected_te)
+		expected_te = ((ArchiveHandle *) fout)->toc;
+
+	expected_te = expected_te->next;
+	while ((expected_te->reqs & REQ_STATS) == 0 ||
+		   strcmp(expected_te->desc, "STATISTICS DATA") != 0)
+		expected_te = expected_te->next;
+
+	if (te != expected_te)
+		pg_fatal("stats dumped out of order (current: %d %s %s) (expected: %d %s %s)",
+				 te->dumpId, te->desc, te->tag,
+				 expected_te->dumpId, expected_te->desc, expected_te->tag);
 
 	query = createPQExpBuffer();
 	if (!fout->is_prepared[PREPQUERY_GETATTRIBUTESTATS])
 	{
 		appendPQExpBufferStr(query,
-							 "PREPARE getAttributeStats(pg_catalog.name, pg_catalog.name) AS\n"
-							 "SELECT s.attname, s.inherited, "
+							 "PREPARE getAttributeStats(pg_catalog.name[], pg_catalog.name[]) AS\n"
+							 "SELECT s.schemaname, s.tablename, s.attname, s.inherited, "
 							 "s.null_frac, s.avg_width, s.n_distinct, "
 							 "s.most_common_vals, s.most_common_freqs, "
 							 "s.histogram_bounds, s.correlation, "
@@ -10608,11 +10705,21 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 								 "NULL AS range_empty_frac,"
 								 "NULL AS range_bounds_histogram ");
 
+		/*
+		 * The results must be in the order of the relations supplied in the
+		 * parameters to ensure we remain in sync as we walk through the TOC.
+		 * The redundant filter clause on s.tablename = ANY(...) seems
+		 * sufficient to convince the planner to use
+		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
+		 * This may not work for all versions.
+		 */
 		appendPQExpBufferStr(query,
 							 "FROM pg_catalog.pg_stats s "
-							 "WHERE s.schemaname = $1 "
-							 "AND s.tablename = $2 "
-							 "ORDER BY s.attname, s.inherited");
+							 "JOIN unnest($1, $2) WITH ORDINALITY AS u (schemaname, tablename, ord) "
+							 "ON s.schemaname = u.schemaname "
+							 "AND s.tablename = u.tablename "
+							 "WHERE s.tablename = ANY($2) "
+							 "ORDER BY u.ord, s.attname, s.inherited");
 
 		ExecuteSqlStatement(fout, query->data);
 
@@ -10642,16 +10749,16 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 
 	appendPQExpBufferStr(out, "\n);\n");
 
+	/* Fetch the next batch of attribute statistics if needed. */
+	if (rownum >= PQntuples(res))
+	{
+		PQclear(res);
+		res = fetchAttributeStats(fout);
+		rownum = 0;
+	}
 
-	/* fetch attribute stats */
-	appendPQExpBufferStr(query, "EXECUTE getAttributeStats(");
-	appendStringLiteralAH(query, dobj->namespace->dobj.name, fout);
-	appendPQExpBufferStr(query, ", ");
-	appendStringLiteralAH(query, dobj->name, fout);
-	appendPQExpBufferStr(query, ");");
-
-	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
-
+	i_schemaname = PQfnumber(res, "schemaname");
+	i_tablename = PQfnumber(res, "tablename");
 	i_attname = PQfnumber(res, "attname");
 	i_inherited = PQfnumber(res, "inherited");
 	i_null_frac = PQfnumber(res, "null_frac");
@@ -10669,10 +10776,15 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 	i_range_bounds_histogram = PQfnumber(res, "range_bounds_histogram");
 
 	/* restore attribute stats */
-	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	for (; rownum < PQntuples(res); rownum++)
 	{
 		const char *attname;
 
+		/* Stop if the next stat row in our cache isn't for this relation. */
+		if (strcmp(dobj->name, PQgetvalue(res, rownum, i_tablename)) != 0 ||
+			strcmp(dobj->namespace->dobj.name, PQgetvalue(res, rownum, i_schemaname)) != 0)
+			break;
+
 		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
 		appendPQExpBuffer(out, "\t'version', '%u'::integer,\n",
 						  fout->remoteVersion);
@@ -10762,8 +10874,6 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg)
 		appendPQExpBufferStr(out, "\n);\n");
 	}
 
-	PQclear(res);
-
 	destroyPQExpBuffer(query);
 	return out->data;
 }
-- 
2.39.5 (Apple Git-154)

v12n6-0004-Partially-revert-commit-a0a4601765.patchtext/plain; charset=us-asciiDownload
From 3860fc596ea6038c5f4edeb1ab3de85d8a297013 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Thu, 3 Apr 2025 20:13:40 -0500
Subject: [PATCH v12n6 4/4] Partially revert commit a0a4601765.

Thanks to commit XXXXXXXXXX, which simplified some code in
_tocEntryRestorePass(), we can remove the now-unused ArchiveHandle
parameter from _tocEntryRestorePass() and move_to_ready_heap().

Reviewed-by: Jeff Davis <pgsql@j-davis.com>
Discussion: https://postgr.es/m/Z-3x2AnPCP331JA3%40nathan
---
 src/bin/pg_dump/pg_backup_archiver.c | 26 ++++++++++++--------------
 1 file changed, 12 insertions(+), 14 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 84f32aef411..5ccedc383bb 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -72,7 +72,7 @@ static void processEncodingEntry(ArchiveHandle *AH, TocEntry *te);
 static void processStdStringsEntry(ArchiveHandle *AH, TocEntry *te);
 static void processSearchPathEntry(ArchiveHandle *AH, TocEntry *te);
 static int	_tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH);
-static RestorePass _tocEntryRestorePass(ArchiveHandle *AH, TocEntry *te);
+static RestorePass _tocEntryRestorePass(TocEntry *te);
 static bool _tocEntryIsACL(TocEntry *te);
 static void _disableTriggersIfNecessary(ArchiveHandle *AH, TocEntry *te);
 static void _enableTriggersIfNecessary(ArchiveHandle *AH, TocEntry *te);
@@ -102,8 +102,7 @@ static void pending_list_append(TocEntry *l, TocEntry *te);
 static void pending_list_remove(TocEntry *te);
 static int	TocEntrySizeCompareQsort(const void *p1, const void *p2);
 static int	TocEntrySizeCompareBinaryheap(void *p1, void *p2, void *arg);
-static void move_to_ready_heap(ArchiveHandle *AH,
-							   TocEntry *pending_list,
+static void move_to_ready_heap(TocEntry *pending_list,
 							   binaryheap *ready_heap,
 							   RestorePass pass);
 static TocEntry *pop_next_work_item(binaryheap *ready_heap,
@@ -749,7 +748,7 @@ RestoreArchive(Archive *AHX)
 			if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) == 0)
 				continue;		/* ignore if not to be dumped at all */
 
-			switch (_tocEntryRestorePass(AH, te))
+			switch (_tocEntryRestorePass(te))
 			{
 				case RESTORE_PASS_MAIN:
 					(void) restore_toc_entry(AH, te, false);
@@ -768,7 +767,7 @@ RestoreArchive(Archive *AHX)
 			for (te = AH->toc->next; te != AH->toc; te = te->next)
 			{
 				if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) != 0 &&
-					_tocEntryRestorePass(AH, te) == RESTORE_PASS_ACL)
+					_tocEntryRestorePass(te) == RESTORE_PASS_ACL)
 					(void) restore_toc_entry(AH, te, false);
 			}
 		}
@@ -778,7 +777,7 @@ RestoreArchive(Archive *AHX)
 			for (te = AH->toc->next; te != AH->toc; te = te->next)
 			{
 				if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) != 0 &&
-					_tocEntryRestorePass(AH, te) == RESTORE_PASS_POST_ACL)
+					_tocEntryRestorePass(te) == RESTORE_PASS_POST_ACL)
 					(void) restore_toc_entry(AH, te, false);
 			}
 		}
@@ -3261,7 +3260,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
  * See notes with the RestorePass typedef in pg_backup_archiver.h.
  */
 static RestorePass
-_tocEntryRestorePass(ArchiveHandle *AH, TocEntry *te)
+_tocEntryRestorePass(TocEntry *te)
 {
 	/* "ACL LANGUAGE" was a crock emitted only in PG 7.4 */
 	if (strcmp(te->desc, "ACL") == 0 ||
@@ -4344,7 +4343,7 @@ restore_toc_entries_prefork(ArchiveHandle *AH, TocEntry *pending_list)
 		 * not set skipped_some in this case, since by assumption no main-pass
 		 * items could depend on these.
 		 */
-		if (_tocEntryRestorePass(AH, next_work_item) != RESTORE_PASS_MAIN)
+		if (_tocEntryRestorePass(next_work_item) != RESTORE_PASS_MAIN)
 			do_now = false;
 
 		if (do_now)
@@ -4426,7 +4425,7 @@ restore_toc_entries_parallel(ArchiveHandle *AH, ParallelState *pstate,
 	 * process in the current restore pass.
 	 */
 	AH->restorePass = RESTORE_PASS_MAIN;
-	move_to_ready_heap(AH, pending_list, ready_heap, AH->restorePass);
+	move_to_ready_heap(pending_list, ready_heap, AH->restorePass);
 
 	/*
 	 * main parent loop
@@ -4475,7 +4474,7 @@ restore_toc_entries_parallel(ArchiveHandle *AH, ParallelState *pstate,
 			/* Advance to next restore pass */
 			AH->restorePass++;
 			/* That probably allows some stuff to be made ready */
-			move_to_ready_heap(AH, pending_list, ready_heap, AH->restorePass);
+			move_to_ready_heap(pending_list, ready_heap, AH->restorePass);
 			/* Loop around to see if anything's now ready */
 			continue;
 		}
@@ -4646,8 +4645,7 @@ TocEntrySizeCompareBinaryheap(void *p1, void *p2, void *arg)
  * which applies the same logic one-at-a-time.)
  */
 static void
-move_to_ready_heap(ArchiveHandle *AH,
-				   TocEntry *pending_list,
+move_to_ready_heap(TocEntry *pending_list,
 				   binaryheap *ready_heap,
 				   RestorePass pass)
 {
@@ -4660,7 +4658,7 @@ move_to_ready_heap(ArchiveHandle *AH,
 		next_te = te->pending_next;
 
 		if (te->depCount == 0 &&
-			_tocEntryRestorePass(AH, te) == pass)
+			_tocEntryRestorePass(te) == pass)
 		{
 			/* Remove it from pending_list ... */
 			pending_list_remove(te);
@@ -5054,7 +5052,7 @@ reduce_dependencies(ArchiveHandle *AH, TocEntry *te,
 		 * memberships changed.
 		 */
 		if (otherte->depCount == 0 &&
-			_tocEntryRestorePass(AH, otherte) == AH->restorePass &&
+			_tocEntryRestorePass(otherte) == AH->restorePass &&
 			otherte->pending_prev != NULL &&
 			ready_heap != NULL)
 		{
-- 
2.39.5 (Apple Git-154)

#523Nathan Bossart
nathandbossart@gmail.com
In reply to: Nathan Bossart (#522)
Re: Statistics Import and Export

On Thu, Apr 03, 2025 at 09:19:51PM -0500, Nathan Bossart wrote:

Great. I'm planning to commit the attached patch set tomorrow morning.

Committed.

--
nathan

#524Nathan Bossart
nathandbossart@gmail.com
In reply to: Nathan Bossart (#523)
Re: Statistics Import and Export

On Fri, Apr 04, 2025 at 02:56:54PM -0500, Nathan Bossart wrote:

Committed.

I see the buildfarm failure and am working on a fix.

--
nathan

#525Nathan Bossart
nathandbossart@gmail.com
In reply to: Nathan Bossart (#524)
Re: Statistics Import and Export

On Fri, Apr 04, 2025 at 03:06:45PM -0500, Nathan Bossart wrote:

I see the buildfarm failure and am working on a fix.

I pushed commit 8ec0aae to fix this.

--
nathan

#526Nathan Bossart
nathandbossart@gmail.com
In reply to: Nathan Bossart (#525)
Re: Statistics Import and Export

On Fri, Apr 04, 2025 at 03:58:53PM -0500, Nathan Bossart wrote:

I pushed commit 8ec0aae to fix this.

And now I'm seeing cross-version test failures due to our use of WITH
ORDINALITY, which wasn't added until v9.4. Looking into it...

--
nathan

#527Corey Huinker
corey.huinker@gmail.com
In reply to: Nathan Bossart (#526)
1 attachment(s)
Re: Statistics Import and Export

On Fri, Apr 4, 2025 at 6:25 PM Nathan Bossart <nathandbossart@gmail.com>
wrote:

On Fri, Apr 04, 2025 at 03:58:53PM -0500, Nathan Bossart wrote:

I pushed commit 8ec0aae to fix this.

And now I'm seeing cross-version test failures due to our use of WITH
ORDINALITY, which wasn't added until v9.4. Looking into it...

This patch shrinks the array size to 1 for versions < 9.4, which keeps the
modern code fairly elegant.

Attachments:

v1-0001-Fall-back-to-single-attribute-stat-fetching-for-v.patchtext/x-patch; charset=US-ASCII; name=v1-0001-Fall-back-to-single-attribute-stat-fetching-for-v.patchDownload
From fe551ab55622f95d84ac4c4d79fba898c6b60057 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Fri, 4 Apr 2025 19:30:00 -0400
Subject: [PATCH v1] Fall back to single attribute stat fetching for versions <
 9.4

Existing attribute statistics batch fetching query relies on the
existence of WITH ORDINALTIY, as well as multi-parameter unnest() calls.
Without those, we have no choice but to fall back to single relation
fetching.

Preserve the existing array building infrastructure, and drop the array
size to 1 for older versions.
---
 src/bin/pg_dump/pg_dump.c | 35 +++++++++++++++++++++++++++--------
 1 file changed, 27 insertions(+), 8 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 0e915432e77..b88188448b0 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -10571,6 +10571,14 @@ fetchAttributeStats(Archive *fout)
 	PGresult   *res = NULL;
 	static TocEntry *te;
 	static bool restarted;
+	int			max_rels = MAX_ATTR_STATS_RELS;
+
+	/*
+	 * Versions prior to 9.4 lack the unnest() WITH ORDINALITY feature
+	 * that we need to keep the relation batches in order.
+	 */
+	if (fout->remoteVersion < 90400)
+		max_rels = 1;
 
 	/* If we're just starting, set our TOC pointer. */
 	if (!te)
@@ -10596,7 +10604,7 @@ fetchAttributeStats(Archive *fout)
 	 * This is perhaps not the sturdiest assumption, so we verify it matches
 	 * reality in dumpRelationStats_dumper().
 	 */
-	for (; te != AH->toc && count < MAX_ATTR_STATS_RELS; te = te->next)
+	for (; te != AH->toc && count < max_rels; te = te->next)
 	{
 		if ((te->reqs & REQ_STATS) != 0 &&
 			strcmp(te->desc, "STATISTICS DATA") == 0)
@@ -10709,14 +10717,25 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 		 * sufficient to convince the planner to use
 		 * pg_class_relname_nsp_index, which avoids a full scan of pg_stats.
 		 * This may not work for all versions.
+		 *
+		 * WITH ORDINALITY was introduced in 9.4, and multi-argument unnest()
+		 * was introduced in 9.3. Rather than create a bunch of corner-cases,
+		 * we simply fall back to fetching a single relation per call.
 		 */
-		appendPQExpBufferStr(query,
-							 "FROM pg_catalog.pg_stats s "
-							 "JOIN unnest($1, $2) WITH ORDINALITY AS u (schemaname, tablename, ord) "
-							 "ON s.schemaname = u.schemaname "
-							 "AND s.tablename = u.tablename "
-							 "WHERE s.tablename = ANY($2) "
-							 "ORDER BY u.ord, s.attname, s.inherited");
+		if (fout->remoteVersion >= 90400)
+			appendPQExpBufferStr(query,
+								 "FROM pg_catalog.pg_stats s "
+								 "JOIN unnest($1, $2) WITH ORDINALITY AS u (schemaname, tablename, ord) "
+								 "ON s.schemaname = u.schemaname "
+								 "AND s.tablename = u.tablename "
+								 "WHERE s.tablename = ANY($2) "
+								 "ORDER BY u.ord, s.attname, s.inherited");
+		else
+			appendPQExpBufferStr(query,
+								 "FROM pg_catalog.pg_stats s "
+								 "WHERE s.schemaname = ($1::text[])[1] "
+								 "AND s.tablename = ($2::text[])[1] "
+								 "ORDER BY s.attname, s.inherited");
 
 		ExecuteSqlStatement(fout, query->data);
 

base-commit: 0f43083d16f4be7c01efa80d05d0eef5e5ff69d3
-- 
2.49.0

#528Nathan Bossart
nathandbossart@gmail.com
In reply to: Corey Huinker (#527)
Re: Statistics Import and Export

On Fri, Apr 04, 2025 at 07:32:48PM -0400, Corey Huinker wrote:

This patch shrinks the array size to 1 for versions < 9.4, which keeps the
modern code fairly elegant.

Committed.

--
nathan

#529Greg Sabino Mullane
htamfids@gmail.com
In reply to: Robert Haas (#513)
Re: Statistics Import and Export

On Tue, Apr 1, 2025 at 10:24 PM Robert Haas <robertmhaas@gmail.com> wrote:

On Tue, Apr 1, 2025 at 4:24 PM Jeff Davis <pgsql@j-davis.com> wrote:

On Tue, 2025-04-01 at 09:37 -0400, Robert Haas wrote:

I don't think I was aware of the open item; I was just catching up on
email.

I lean towards making it opt-in for pg_dump and opt-out for pg_upgrade.

Big +1.

I may have missed something (we seem to have a lot of threads for this
subject), but we are in beta and both pg_dump and pg_upgrade seem to be
opt-out? I still object strongly to this; pg_dump is meant to be a
canonical representation of the schema and data. Adding metadata that can
change from dump to dump seems wrong, and should be opt-in. I've not been
convinced otherwise why stats should be output by default.

To be clear, I 100% want it to be the default for pg_upgrade.

Maybe we are just leaving it enabled to see if anyone complains in beta,
but I don't want us to forget about it. :)

Cheers,
Greg

--
Crunchy Data - https://www.crunchydata.com
Enterprise Postgres Software Products & Tech Support

#530Hari Krishna Sunder
hari.db.pg@gmail.com
In reply to: Nathan Bossart (#528)
1 attachment(s)
Re: Statistics Import and Export

We found a minor issue when testing statistics import with upgrading from
versions older than v14. (We have VACUUM and ANALYZE disabled)
3d351d916b20534f973eda760cde17d96545d4c4
<https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=3d351d916b20534f973eda760cde17d96545d4c4&gt;
changed
the default value for reltuples from 0 to -1. So when such tables are
imported they get the pg13 default of 0 which in pg18 is treated
as "vacuumed and seen to be empty" instead of "never yet vacuumed". The
planner then proceeds to pick seq scans even if there are indexes for these
tables.
This is a very narrow edge case and the next VACUUM or ANALYZE will fix it
but the perf of these tables immediately after the upgrade is considerably
affected.

Can we instead use -1 if the version is older than 14, and reltuples is 0?
This will have the unintended consequence of treating a truly empty table
as "never yet vacuumed", but that should be fine as empty tables are going
to be fast regardless of the plan picked.

PS: This is my first patch, so apologies for any issues with the patch.

On Fri, Apr 4, 2025 at 7:06 PM Nathan Bossart <nathandbossart@gmail.com>
wrote:

Show quoted text

On Fri, Apr 04, 2025 at 07:32:48PM -0400, Corey Huinker wrote:

This patch shrinks the array size to 1 for versions < 9.4, which keeps

the

modern code fairly elegant.

Committed.

--
nathan

Attachments:

0001-Stats-import-Fix-default-reltuples-on-versions-older.patchapplication/octet-stream; name=0001-Stats-import-Fix-default-reltuples-on-versions-older.patchDownload
From 043ff3784e19c62615a8faff3ae65966cb83e557 Mon Sep 17 00:00:00 2001
From: Hari Krishna Sunder <hari90@users.noreply.github.com>
Date: Tue, 13 May 2025 23:26:32 +0000
Subject: [PATCH] Stats import: Fix default reltuples on versions older than 14

---
 src/bin/pg_dump/pg_dump.c | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index e2e7975b34e..e75f3ca4cab 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -10924,7 +10924,10 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 	appendStringLiteralAH(out, rsinfo->dobj.name, fout);
 	appendPQExpBufferStr(out, ",\n");
 	appendPQExpBuffer(out, "\t'relpages', '%d'::integer,\n", rsinfo->relpages);
-	appendPQExpBuffer(out, "\t'reltuples', '%s'::real,\n", rsinfo->reltuples);
+	if (fout->remoteVersion < 140000 && strcmp("0", rsinfo->reltuples) == 0)
+		appendPQExpBufferStr(out, "\t'reltuples', '-1'::real,\n");
+	else
+		appendPQExpBuffer(out, "\t'reltuples', '%s'::real,\n", rsinfo->reltuples);
 	appendPQExpBuffer(out, "\t'relallvisible', '%d'::integer",
 					  rsinfo->relallvisible);
 
-- 
2.26.0

#531Nathan Bossart
nathandbossart@gmail.com
In reply to: Hari Krishna Sunder (#530)
Re: Statistics Import and Export

On Tue, May 13, 2025 at 05:01:02PM -0700, Hari Krishna Sunder wrote:

We found a minor issue when testing statistics import with upgrading from
versions older than v14. (We have VACUUM and ANALYZE disabled)
3d351d916b20534f973eda760cde17d96545d4c4
<https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=3d351d916b20534f973eda760cde17d96545d4c4&gt;
changed
the default value for reltuples from 0 to -1. So when such tables are
imported they get the pg13 default of 0 which in pg18 is treated
as "vacuumed and seen to be empty" instead of "never yet vacuumed". The
planner then proceeds to pick seq scans even if there are indexes for these
tables.
This is a very narrow edge case and the next VACUUM or ANALYZE will fix it
but the perf of these tables immediately after the upgrade is considerably
affected.

There was a similar report for vacuumdb's new --missing-stats-only option.
We fixed that in commit 9879105 by removing the check for reltuples != 0,
which means that --missing-stats-only will process empty tables.

Can we instead use -1 if the version is older than 14, and reltuples is 0?
This will have the unintended consequence of treating a truly empty table
as "never yet vacuumed", but that should be fine as empty tables are going
to be fast regardless of the plan picked.

I'm inclined to agree that we should do this. Even if it's much more
likely that 0 means empty versus not-yet-processed, the one-time cost of
processing some empty tables doesn't sound too bad. In any case, since
this only applies to upgrades from <v14, that trade-off should dissipate
over time.

PS: This is my first patch, so apologies for any issues with the patch.

It needs a comment, but otherwise it looks generally reasonable to me after
a quick glance.

--
nathan

#532Hari Krishna Sunder
hari.db.pg@gmail.com
In reply to: Nathan Bossart (#531)
1 attachment(s)
Re: Statistics Import and Export

Thanks Nathan.
Here is the patch with a comment.

On Wed, May 14, 2025 at 8:53 AM Nathan Bossart <nathandbossart@gmail.com>
wrote:

Show quoted text

On Tue, May 13, 2025 at 05:01:02PM -0700, Hari Krishna Sunder wrote:

We found a minor issue when testing statistics import with upgrading from
versions older than v14. (We have VACUUM and ANALYZE disabled)
3d351d916b20534f973eda760cde17d96545d4c4
<

https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=3d351d916b20534f973eda760cde17d96545d4c4

changed
the default value for reltuples from 0 to -1. So when such tables are
imported they get the pg13 default of 0 which in pg18 is treated
as "vacuumed and seen to be empty" instead of "never yet vacuumed". The
planner then proceeds to pick seq scans even if there are indexes for

these

tables.
This is a very narrow edge case and the next VACUUM or ANALYZE will fix

it

but the perf of these tables immediately after the upgrade is

considerably

affected.

There was a similar report for vacuumdb's new --missing-stats-only option.
We fixed that in commit 9879105 by removing the check for reltuples != 0,
which means that --missing-stats-only will process empty tables.

Can we instead use -1 if the version is older than 14, and reltuples is

0?

This will have the unintended consequence of treating a truly empty table
as "never yet vacuumed", but that should be fine as empty tables are

going

to be fast regardless of the plan picked.

I'm inclined to agree that we should do this. Even if it's much more
likely that 0 means empty versus not-yet-processed, the one-time cost of
processing some empty tables doesn't sound too bad. In any case, since
this only applies to upgrades from <v14, that trade-off should dissipate
over time.

PS: This is my first patch, so apologies for any issues with the patch.

It needs a comment, but otherwise it looks generally reasonable to me after
a quick glance.

--
nathan

Attachments:

0001-Stats-import-Fix-default-reltuples-on-versions-older.patchapplication/octet-stream; name=0001-Stats-import-Fix-default-reltuples-on-versions-older.patchDownload
From f3024a4b50af63f61fb91a82e7b91c98f9bf882d Mon Sep 17 00:00:00 2001
From: Hari Krishna Sunder <hari90@users.noreply.github.com>
Date: Tue, 13 May 2025 23:26:32 +0000
Subject: [PATCH] Stats import: Fix default reltuples on versions older than 14

---
 src/bin/pg_dump/pg_dump.c | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index e2e7975b34e..45548004240 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -10924,7 +10924,17 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 	appendStringLiteralAH(out, rsinfo->dobj.name, fout);
 	appendPQExpBufferStr(out, ",\n");
 	appendPQExpBuffer(out, "\t'relpages', '%d'::integer,\n", rsinfo->relpages);
-	appendPQExpBuffer(out, "\t'reltuples', '%s'::real,\n", rsinfo->reltuples);
+
+	/*
+	 * Before version 14, the default value for reltuples of tables that had not yet been ANALYZED
+	 * was 0. In version 14, the default value was changed to -1. Even if the table is empty, let's
+	 * just assume it has not yet been ANALYZED and set to -1.
+	 */
+	if (fout->remoteVersion < 140000 && strcmp("0", rsinfo->reltuples) == 0)
+		appendPQExpBufferStr(out, "\t'reltuples', '-1'::real,\n");
+	else
+		appendPQExpBuffer(out, "\t'reltuples', '%s'::real,\n", rsinfo->reltuples);
+
 	appendPQExpBuffer(out, "\t'relallvisible', '%d'::integer",
 					  rsinfo->relallvisible);
 
-- 
2.26.0

#533Hari Krishna Sunder
hari.db.pg@gmail.com
In reply to: Hari Krishna Sunder (#532)
Re: Statistics Import and Export

Gentle ping on this.

---
Hari Krishna Sunder

On Wed, May 14, 2025 at 1:30 PM Hari Krishna Sunder <hari.db.pg@gmail.com>
wrote:

Show quoted text

Thanks Nathan.
Here is the patch with a comment.

On Wed, May 14, 2025 at 8:53 AM Nathan Bossart <nathandbossart@gmail.com>
wrote:

On Tue, May 13, 2025 at 05:01:02PM -0700, Hari Krishna Sunder wrote:

We found a minor issue when testing statistics import with upgrading

from

versions older than v14. (We have VACUUM and ANALYZE disabled)
3d351d916b20534f973eda760cde17d96545d4c4
<

https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=3d351d916b20534f973eda760cde17d96545d4c4

changed
the default value for reltuples from 0 to -1. So when such tables are
imported they get the pg13 default of 0 which in pg18 is treated
as "vacuumed and seen to be empty" instead of "never yet vacuumed". The
planner then proceeds to pick seq scans even if there are indexes for

these

tables.
This is a very narrow edge case and the next VACUUM or ANALYZE will fix

it

but the perf of these tables immediately after the upgrade is

considerably

affected.

There was a similar report for vacuumdb's new --missing-stats-only option.
We fixed that in commit 9879105 by removing the check for reltuples != 0,
which means that --missing-stats-only will process empty tables.

Can we instead use -1 if the version is older than 14, and reltuples is

0?

This will have the unintended consequence of treating a truly empty

table

as "never yet vacuumed", but that should be fine as empty tables are

going

to be fast regardless of the plan picked.

I'm inclined to agree that we should do this. Even if it's much more
likely that 0 means empty versus not-yet-processed, the one-time cost of
processing some empty tables doesn't sound too bad. In any case, since
this only applies to upgrades from <v14, that trade-off should dissipate
over time.

PS: This is my first patch, so apologies for any issues with the patch.

It needs a comment, but otherwise it looks generally reasonable to me
after
a quick glance.

--
nathan

#534Michael Paquier
michael@paquier.xyz
In reply to: Hari Krishna Sunder (#533)
Re: Statistics Import and Export

On Fri, May 16, 2025 at 11:47:12AM -0700, Hari Krishna Sunder wrote:

Gentle ping on this.

Most of the major PostgreSQL developers were at pgconf.dev held in
Montreal last week, explaining a reduction in the activity of the
mailing lists.

Your initial report was on Monday the 14th, with this ping being on
Friday the 16th, both happening during the conference. I suspect that
that there was just no time for folks of this thread to be able to
provide feedback for your patch, so please be a bit more patient.

Thanks!
--
Michael

#535Nathan Bossart
nathandbossart@gmail.com
In reply to: Hari Krishna Sunder (#532)
Re: Statistics Import and Export

On Wed, May 14, 2025 at 01:30:48PM -0700, Hari Krishna Sunder wrote:

Here is the patch with a comment.

Thanks.

On Wed, May 14, 2025 at 8:53 AM Nathan Bossart <nathandbossart@gmail.com>
wrote:

There was a similar report for vacuumdb's new --missing-stats-only option.
We fixed that in commit 9879105 by removing the check for reltuples != 0,
which means that --missing-stats-only will process empty tables.

I'm wondering if we should revert commit 9879105 if we take this change,
which solves the --missing-stats-only problem in a different way. My
current thinking is that we should just leave it in place, if for no other
reason than analyzing some empty tables seems unlikely to cause too much
trouble. Thoughts?

--
nathan

#536Hari Krishna Sunder
hari.db.pg@gmail.com
In reply to: Nathan Bossart (#535)
Re: Statistics Import and Export

Sorry didn't know about the conference.

I think it would be better to revert 9879105 since there can be a
considerable number of true empty tables that we don’t need to process.

---
Hari Krishna Sunder

On Mon, May 19, 2025 at 9:51 AM Nathan Bossart <nathandbossart@gmail.com>
wrote:

Show quoted text

On Wed, May 14, 2025 at 01:30:48PM -0700, Hari Krishna Sunder wrote:

Here is the patch with a comment.

Thanks.

On Wed, May 14, 2025 at 8:53 AM Nathan Bossart <nathandbossart@gmail.com

wrote:

There was a similar report for vacuumdb's new --missing-stats-only

option.

We fixed that in commit 9879105 by removing the check for reltuples !=

0,

which means that --missing-stats-only will process empty tables.

I'm wondering if we should revert commit 9879105 if we take this change,
which solves the --missing-stats-only problem in a different way. My
current thinking is that we should just leave it in place, if for no other
reason than analyzing some empty tables seems unlikely to cause too much
trouble. Thoughts?

--
nathan

#537Nathan Bossart
nathandbossart@gmail.com
In reply to: Hari Krishna Sunder (#536)
Re: Statistics Import and Export

On Mon, May 19, 2025 at 02:13:45PM -0700, Hari Krishna Sunder wrote:

I think it would be better to revert 9879105 since there can be a
considerable number of true empty tables that we don�t need to process.

I'm not sure that's a use-case we really need to optimize. Even with
100,000 empty tables, "vacuumdb --analyze-only --missing-stats-only --jobs
64" completes in ~5.5 seconds on my laptop. Plus, even if reltuples is 0,
there might actually be rows in the table, in which case analyzing it will
produce rows in pg_statistic.

--
nathan

#538Hari Krishna Sunder
hari.db.pg@gmail.com
In reply to: Nathan Bossart (#537)
Re: Statistics Import and Export

Ah ya, forgot that reltuples are not always accurate. This sounds
reasonable to me.

On Mon, May 19, 2025 at 2:32 PM Nathan Bossart <nathandbossart@gmail.com>
wrote:

Show quoted text

On Mon, May 19, 2025 at 02:13:45PM -0700, Hari Krishna Sunder wrote:

I think it would be better to revert 9879105 since there can be a
considerable number of true empty tables that we don´t need to process.

I'm not sure that's a use-case we really need to optimize. Even with
100,000 empty tables, "vacuumdb --analyze-only --missing-stats-only --jobs
64" completes in ~5.5 seconds on my laptop. Plus, even if reltuples is 0,
there might actually be rows in the table, in which case analyzing it will
produce rows in pg_statistic.

--
nathan

#539Nathan Bossart
nathandbossart@gmail.com
In reply to: Hari Krishna Sunder (#538)
1 attachment(s)
Re: Statistics Import and Export

On Tue, May 20, 2025 at 10:32:39AM -0700, Hari Krishna Sunder wrote:

Ah ya, forgot that reltuples are not always accurate. This sounds
reasonable to me.

Cool. Here is what I have staged for commit, which I am planning to do
shortly.

--
nathan

Attachments:

v3-0001-pg_dump-Adjust-reltuples-from-0-to-1-for-dumps-on.patchtext/plain; charset=us-asciiDownload
From e68770a6089500e6b4d02bcb3009ec12da392d5f Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 21 May 2025 10:53:29 -0500
Subject: [PATCH v3 1/1] pg_dump: Adjust reltuples from 0 to -1 for dumps on
 older versions.

Before v14, a reltuples value of 0 was ambiguous: it could either
mean the relation is empty, or it could mean that it hadn't yet
been vacuumed or analyzed.  (Commit 3d351d916b taught v14 and newer
to use -1 for the latter case.)  This ambiguity can cause the
planner to choose inefficient plans after restoring to v18 or
newer.  To fix, let's just dump reltuples as -1 in that case.  This
will cause some truly empty tables to be seen as not-yet-processed,
but that seems unlikely to cause too much trouble in practice.

Commit 9879105024 fixed a similar problem for vacuumdb by removing
the check for reltuples != 0.  Presumably we could reinstate that
check now, but I've chosen to leave it in place in case reltuples
isn't accurate.  As before, processing some empty tables seems
pretty innocuous.

Author: Hari Krishna Sunder <hari.db.pg@gmail.com>
Discussion: https://postgr.es/m/CAAeiqZ0o2p4SX5_xPcuAbbsmXjg6MJLNuPYSLUjC%3DWh-VeW64A%40mail.gmail.com
---
 src/bin/pg_dump/pg_dump.c | 15 ++++++++++++++-
 1 file changed, 14 insertions(+), 1 deletion(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index c73e73a87d1..23c8c25e33d 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -10929,7 +10929,20 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te)
 	appendStringLiteralAH(out, rsinfo->dobj.name, fout);
 	appendPQExpBufferStr(out, ",\n");
 	appendPQExpBuffer(out, "\t'relpages', '%d'::integer,\n", rsinfo->relpages);
-	appendPQExpBuffer(out, "\t'reltuples', '%s'::real,\n", rsinfo->reltuples);
+
+	/*
+	 * Before v14, a reltuples value of 0 was ambiguous: it could either mean
+	 * the relation is empty, or it could mean that it hadn't yet been
+	 * vacuumed or analyzed.  (Newer versions use -1 for the latter case.)
+	 * This ambiguity can cause the planner to choose inefficient plans after
+	 * restoring to v18 or newer.  To deal with this, let's just set reltuples
+	 * to -1 in that case.
+	 */
+	if (fout->remoteVersion < 140000 && strcmp("0", rsinfo->reltuples) == 0)
+		appendPQExpBufferStr(out, "\t'reltuples', '-1'::real,\n");
+	else
+		appendPQExpBuffer(out, "\t'reltuples', '%s'::real,\n", rsinfo->reltuples);
+
 	appendPQExpBuffer(out, "\t'relallvisible', '%d'::integer",
 					  rsinfo->relallvisible);
 
-- 
2.39.5 (Apple Git-154)

#540Hari Krishna Sunder
hari.db.pg@gmail.com
In reply to: Nathan Bossart (#539)
Re: Statistics Import and Export

Looks good to me.

On Wed, May 21, 2025 at 9:08 AM Nathan Bossart <nathandbossart@gmail.com>
wrote:

Show quoted text

On Tue, May 20, 2025 at 10:32:39AM -0700, Hari Krishna Sunder wrote:

Ah ya, forgot that reltuples are not always accurate. This sounds
reasonable to me.

Cool. Here is what I have staged for commit, which I am planning to do
shortly.

--
nathan

#541Jeff Davis
pgsql@j-davis.com
In reply to: Nathan Bossart (#539)
Re: Statistics Import and Export

On Wed, 2025-05-21 at 11:08 -0500, Nathan Bossart wrote:

On Tue, May 20, 2025 at 10:32:39AM -0700, Hari Krishna Sunder wrote:

Ah ya, forgot that reltuples are not always accurate. This sounds
reasonable to me.

Cool.  Here is what I have staged for commit, which I am planning to
do
shortly.

Originally, one of the reasons we added a version field during dump is
so that some future version could reinterpret stats in older dump files
during import.

This patch is using a newer version of pg_dump to interpret stats from
older versions during export. That might be fine, but it would be good
to understand where the line is between things we should reinterpret
during export vs things we should reinterpret during import.

Regards,
Jeff Davis

#542Nathan Bossart
nathandbossart@gmail.com
In reply to: Jeff Davis (#541)
Re: Statistics Import and Export

On Wed, May 21, 2025 at 02:14:55PM -0700, Jeff Davis wrote:

Originally, one of the reasons we added a version field during dump is
so that some future version could reinterpret stats in older dump files
during import.

This patch is using a newer version of pg_dump to interpret stats from
older versions during export. That might be fine, but it would be good
to understand where the line is between things we should reinterpret
during export vs things we should reinterpret during import.

I don't know precisely where that line might be, but in this case, the
dumped stats have no hope of restoring into anything older than v18 (since
the stats import functions won't exist), which is well past the point where
we started using -1 for reltuples. If we could dump the stats from v13 and
restore them into v13, then I think there would be a reasonably strong
argument for dumping it as-is and reinterpreting as necessary during
import. But I see no particular benefit from moving the complexity to the
import side here.

Does that seem like a reasonable position to you? Is there anything else
we should consider?

--
nathan

#543Corey Huinker
corey.huinker@gmail.com
In reply to: Nathan Bossart (#542)
Re: Statistics Import and Export

I don't know precisely where that line might be, but in this case, the
dumped stats have no hope of restoring into anything older than v18 (since
the stats import functions won't exist), which is well past the point where
we started using -1 for reltuples. If we could dump the stats from v13 and
restore them into v13, then I think there would be a reasonably strong
argument for dumping it as-is and reinterpreting as necessary during
import. But I see no particular benefit from moving the complexity to the
import side here.

Definitely keep complexity on the export-side.

Mapping reltuples 0 -> -1 if system version < 14 like the original patch
did makes the most sense to me. That allows vacuumdb to go back to ignoring
tables that are seemingly empty while still vacuuming the tables that had
the pre-14 suspicious 0 reltuples value.

Does that seem like a reasonable position to you? Is there anything else
we should consider?

Automatically vacuuming tables that purport to be empty may not take much
time, but it may alarm users using --missing-only, wondering why so many
tables didn't get stats imported, especially if we introduce a --dry-run
parameter which would answer for a user the question "what tables does
vacuumdb think are missing statistics?".

#544Jeff Davis
pgsql@j-davis.com
In reply to: Nathan Bossart (#542)
Re: Statistics Import and Export

On Wed, 2025-05-21 at 16:29 -0500, Nathan Bossart wrote:

I don't know precisely where that line might be, but in this case,
the
dumped stats have no hope of restoring into anything older than
v18... But I see no particular benefit from moving the complexity
to the
import side here.

That's fine with me. Perhaps we should just say that pre-18 behavior
differences can be fixed up during export, and post-18 behavior
differences are fixed up during import?

Regards,
Jeff Davis

#545Robert Haas
robertmhaas@gmail.com
In reply to: Greg Sabino Mullane (#529)
Re: Statistics Import and Export

On Sat, May 10, 2025 at 3:51 PM Greg Sabino Mullane <htamfids@gmail.com> wrote:

I may have missed something (we seem to have a lot of threads for this subject), but we are in beta and both pg_dump and pg_upgrade seem to be opt-out? I still object strongly to this; pg_dump is meant to be a canonical representation of the schema and data. Adding metadata that can change from dump to dump seems wrong, and should be opt-in. I've not been convinced otherwise why stats should be output by default.

To be clear, I 100% want it to be the default for pg_upgrade.

Maybe we are just leaving it enabled to see if anyone complains in beta, but I don't want us to forget about it. :)

Yeah. This could use comments from a few more people, but I really
hope we don't ship the final release this way. We do have a "Enable
statistics in pg_dump by default" item in the open items list under
"Decisions to Recheck Mid-Beta", but that's arguably now. It also sort
of looks like we might have a consensus anyway: Jeff said "I lean
towards making it opt-in for pg_dump and opt-out for pg_upgrade" and I
agree with that and it seems you do, too. So perhaps Jeff should make
it so?

--
Robert Haas
EDB: http://www.enterprisedb.com

#546Nathan Bossart
nathandbossart@gmail.com
In reply to: Robert Haas (#545)
Re: Statistics Import and Export

On Thu, May 22, 2025 at 10:20:16AM -0400, Robert Haas wrote:

It also sort
of looks like we might have a consensus anyway: Jeff said "I lean
towards making it opt-in for pg_dump and opt-out for pg_upgrade" and I
agree with that and it seems you do, too. So perhaps Jeff should make
it so?

+1, I think we should go ahead and do this. If Jeff can't get to it, I'm
happy to pick it up in the next week or so.

--
nathan

#547Tom Lane
tgl@sss.pgh.pa.us
In reply to: Nathan Bossart (#546)
Re: Statistics Import and Export

Nathan Bossart <nathandbossart@gmail.com> writes:

On Thu, May 22, 2025 at 10:20:16AM -0400, Robert Haas wrote:

It also sort
of looks like we might have a consensus anyway: Jeff said "I lean
towards making it opt-in for pg_dump and opt-out for pg_upgrade" and I
agree with that and it seems you do, too. So perhaps Jeff should make
it so?

+1, I think we should go ahead and do this. If Jeff can't get to it, I'm
happy to pick it up in the next week or so.

Works for me, too.

regards, tom lane

#548Nathan Bossart
nathandbossart@gmail.com
In reply to: Jeff Davis (#544)
Re: Statistics Import and Export

On Wed, May 21, 2025 at 04:53:17PM -0700, Jeff Davis wrote:

On Wed, 2025-05-21 at 16:29 -0500, Nathan Bossart wrote:

I don't know precisely where that line might be, but in this case,
the
dumped stats have no hope of restoring into anything older than
v18... But I see no particular benefit from moving the complexity
to the
import side here.

That's fine with me. Perhaps we should just say that pre-18 behavior
differences can be fixed up during export, and post-18 behavior
differences are fixed up during import?

WFM. I've committed the patch.

--
nathan

#549Jeff Davis
pgsql@j-davis.com
In reply to: Robert Haas (#545)
1 attachment(s)
Re: Statistics Import and Export

On Thu, 2025-05-22 at 10:20 -0400, Robert Haas wrote:

Yeah. This could use comments from a few more people, but I really
hope we don't ship the final release this way. We do have a "Enable
statistics in pg_dump by default" item in the open items list under
"Decisions to Recheck Mid-Beta", but that's arguably now. It also
sort
of looks like we might have a consensus anyway: Jeff said "I lean
towards making it opt-in for pg_dump and opt-out for pg_upgrade" and
I
agree with that and it seems you do, too. So perhaps Jeff should make
it so?

Patch attached.

A couple minor points:

* The default for pg_restore is --no-statistics. That could cause a
minor surprise if the user specifies --with-statistics for pg_dump and
not for pg_restore. An argument could be made that "if the stats are
there, restore them", and I don't have a strong opinion about this
point, but defaulting to --no-statistics seems more consistent with
pg_dump.

* I added --with-statistics to most of the pg_dump tests. We can be
more judicious about which tests exercise statistics as a separate
commit, but I didn't want to change the test results as a part of this
commit.

Regards,
Jeff Davis

Attachments:

v1-0001-Change-defaults-for-statistics-export.patchtext/x-patch; charset=UTF-8; name=v1-0001-Change-defaults-for-statistics-export.patchDownload
From b76cb91441e2eefe278249e23fcd703d27a85a06 Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Thu, 22 May 2025 11:03:03 -0700
Subject: [PATCH v1] Change defaults for statistics export.

Set the default behavior of pg_dump, pg_dumpall, and pg_restore to be
--no-statistics. Leave the default for pg_upgrade to be
--with-statistics.

Discussion: https://postgr.es/m/CA+TgmoZ9=RnWcCOZiKYYjZs_AW1P4QXCw--h4dOLLHuf1Omung@mail.gmail.com
---
 src/bin/pg_dump/pg_backup_archiver.c |  4 +-
 src/bin/pg_dump/t/002_pg_dump.pl     | 59 ++++++++++++++++++++++++++++
 src/bin/pg_upgrade/dump.c            |  2 +-
 src/bin/pg_upgrade/pg_upgrade.c      |  6 ++-
 4 files changed, 66 insertions(+), 5 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index afa42337b11..a66d88bbc51 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -152,7 +152,7 @@ InitDumpOptions(DumpOptions *opts)
 	opts->dumpSections = DUMP_UNSECTIONED;
 	opts->dumpSchema = true;
 	opts->dumpData = true;
-	opts->dumpStatistics = true;
+	opts->dumpStatistics = false;
 }
 
 /*
@@ -1101,7 +1101,7 @@ NewRestoreOptions(void)
 	opts->compression_spec.level = 0;
 	opts->dumpSchema = true;
 	opts->dumpData = true;
-	opts->dumpStatistics = true;
+	opts->dumpStatistics = false;
 
 	return opts;
 }
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index cf34f71ea11..386e21e0c59 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -68,6 +68,7 @@ my %pgdump_runs = (
 			'--no-data',
 			'--sequence-data',
 			'--binary-upgrade',
+			'--with-statistics',
 			'--dbname' => 'postgres',    # alternative way to specify database
 		],
 		restore_cmd => [
@@ -75,6 +76,7 @@ my %pgdump_runs = (
 			'--format' => 'custom',
 			'--verbose',
 			'--file' => "$tempdir/binary_upgrade.sql",
+			'--with-statistics',
 			"$tempdir/binary_upgrade.dump",
 		],
 	},
@@ -88,11 +90,13 @@ my %pgdump_runs = (
 			'--format' => 'custom',
 			'--compress' => '1',
 			'--file' => "$tempdir/compression_gzip_custom.dump",
+			'--with-statistics',
 			'postgres',
 		],
 		restore_cmd => [
 			'pg_restore',
 			'--file' => "$tempdir/compression_gzip_custom.sql",
+			'--with-statistics',
 			"$tempdir/compression_gzip_custom.dump",
 		],
 		command_like => {
@@ -115,6 +119,7 @@ my %pgdump_runs = (
 			'--format' => 'directory',
 			'--compress' => 'gzip:1',
 			'--file' => "$tempdir/compression_gzip_dir",
+			'--with-statistics',
 			'postgres',
 		],
 		# Give coverage for manually compressed blobs.toc files during
@@ -132,6 +137,7 @@ my %pgdump_runs = (
 			'pg_restore',
 			'--jobs' => '2',
 			'--file' => "$tempdir/compression_gzip_dir.sql",
+			'--with-statistics',
 			"$tempdir/compression_gzip_dir",
 		],
 	},
@@ -144,6 +150,7 @@ my %pgdump_runs = (
 			'--format' => 'plain',
 			'--compress' => '1',
 			'--file' => "$tempdir/compression_gzip_plain.sql.gz",
+			'--with-statistics',
 			'postgres',
 		],
 		# Decompress the generated file to run through the tests.
@@ -162,11 +169,13 @@ my %pgdump_runs = (
 			'--format' => 'custom',
 			'--compress' => 'lz4',
 			'--file' => "$tempdir/compression_lz4_custom.dump",
+			'--with-statistics',
 			'postgres',
 		],
 		restore_cmd => [
 			'pg_restore',
 			'--file' => "$tempdir/compression_lz4_custom.sql",
+			'--with-statistics',
 			"$tempdir/compression_lz4_custom.dump",
 		],
 		command_like => {
@@ -189,6 +198,7 @@ my %pgdump_runs = (
 			'--format' => 'directory',
 			'--compress' => 'lz4:1',
 			'--file' => "$tempdir/compression_lz4_dir",
+			'--with-statistics',
 			'postgres',
 		],
 		# Verify that data files were compressed
@@ -200,6 +210,7 @@ my %pgdump_runs = (
 			'pg_restore',
 			'--jobs' => '2',
 			'--file' => "$tempdir/compression_lz4_dir.sql",
+			'--with-statistics',
 			"$tempdir/compression_lz4_dir",
 		],
 	},
@@ -212,6 +223,7 @@ my %pgdump_runs = (
 			'--format' => 'plain',
 			'--compress' => 'lz4',
 			'--file' => "$tempdir/compression_lz4_plain.sql.lz4",
+			'--with-statistics',
 			'postgres',
 		],
 		# Decompress the generated file to run through the tests.
@@ -233,11 +245,13 @@ my %pgdump_runs = (
 			'--format' => 'custom',
 			'--compress' => 'zstd',
 			'--file' => "$tempdir/compression_zstd_custom.dump",
+			'--with-statistics',
 			'postgres',
 		],
 		restore_cmd => [
 			'pg_restore',
 			'--file' => "$tempdir/compression_zstd_custom.sql",
+			'--with-statistics',
 			"$tempdir/compression_zstd_custom.dump",
 		],
 		command_like => {
@@ -259,6 +273,7 @@ my %pgdump_runs = (
 			'--format' => 'directory',
 			'--compress' => 'zstd:1',
 			'--file' => "$tempdir/compression_zstd_dir",
+			'--with-statistics',
 			'postgres',
 		],
 		# Give coverage for manually compressed blobs.toc files during
@@ -279,6 +294,7 @@ my %pgdump_runs = (
 			'pg_restore',
 			'--jobs' => '2',
 			'--file' => "$tempdir/compression_zstd_dir.sql",
+			'--with-statistics',
 			"$tempdir/compression_zstd_dir",
 		],
 	},
@@ -292,6 +308,7 @@ my %pgdump_runs = (
 			'--format' => 'plain',
 			'--compress' => 'zstd:long',
 			'--file' => "$tempdir/compression_zstd_plain.sql.zst",
+			'--with-statistics',
 			'postgres',
 		],
 		# Decompress the generated file to run through the tests.
@@ -310,6 +327,7 @@ my %pgdump_runs = (
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/clean.sql",
 			'--clean',
+			'--with-statistics',
 			'--dbname' => 'postgres',    # alternative way to specify database
 		],
 	},
@@ -320,6 +338,7 @@ my %pgdump_runs = (
 			'--clean',
 			'--if-exists',
 			'--encoding' => 'UTF8',      # no-op, just for testing
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -338,6 +357,7 @@ my %pgdump_runs = (
 			'--create',
 			'--no-reconnect',    # no-op, just for testing
 			'--verbose',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -356,6 +376,7 @@ my %pgdump_runs = (
 		dump_cmd => [
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/defaults.sql",
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -364,6 +385,7 @@ my %pgdump_runs = (
 		dump_cmd => [
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/defaults_no_public.sql",
+			'--with-statistics',
 			'regress_pg_dump_test',
 		],
 	},
@@ -373,6 +395,7 @@ my %pgdump_runs = (
 			'pg_dump', '--no-sync',
 			'--clean',
 			'--file' => "$tempdir/defaults_no_public_clean.sql",
+			'--with-statistics',
 			'regress_pg_dump_test',
 		],
 	},
@@ -381,6 +404,7 @@ my %pgdump_runs = (
 		dump_cmd => [
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/defaults_public_owner.sql",
+			'--with-statistics',
 			'regress_public_owner',
 		],
 	},
@@ -395,12 +419,14 @@ my %pgdump_runs = (
 			'pg_dump',
 			'--format' => 'custom',
 			'--file' => "$tempdir/defaults_custom_format.dump",
+			'--with-statistics',
 			'postgres',
 		],
 		restore_cmd => [
 			'pg_restore',
 			'--format' => 'custom',
 			'--file' => "$tempdir/defaults_custom_format.sql",
+			'--with-statistics',
 			"$tempdir/defaults_custom_format.dump",
 		],
 		command_like => {
@@ -425,12 +451,14 @@ my %pgdump_runs = (
 			'pg_dump',
 			'--format' => 'directory',
 			'--file' => "$tempdir/defaults_dir_format",
+			'--with-statistics',
 			'postgres',
 		],
 		restore_cmd => [
 			'pg_restore',
 			'--format' => 'directory',
 			'--file' => "$tempdir/defaults_dir_format.sql",
+			'--with-statistics',
 			"$tempdir/defaults_dir_format",
 		],
 		command_like => {
@@ -456,11 +484,13 @@ my %pgdump_runs = (
 			'--format' => 'directory',
 			'--jobs' => 2,
 			'--file' => "$tempdir/defaults_parallel",
+			'--with-statistics',
 			'postgres',
 		],
 		restore_cmd => [
 			'pg_restore',
 			'--file' => "$tempdir/defaults_parallel.sql",
+			'--with-statistics',
 			"$tempdir/defaults_parallel",
 		],
 	},
@@ -472,12 +502,14 @@ my %pgdump_runs = (
 			'pg_dump',
 			'--format' => 'tar',
 			'--file' => "$tempdir/defaults_tar_format.tar",
+			'--with-statistics',
 			'postgres',
 		],
 		restore_cmd => [
 			'pg_restore',
 			'--format' => 'tar',
 			'--file' => "$tempdir/defaults_tar_format.sql",
+			'--with-statistics',
 			"$tempdir/defaults_tar_format.tar",
 		],
 	},
@@ -486,6 +518,7 @@ my %pgdump_runs = (
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/exclude_dump_test_schema.sql",
 			'--exclude-schema' => 'dump_test',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -494,6 +527,7 @@ my %pgdump_runs = (
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/exclude_test_table.sql",
 			'--exclude-table' => 'dump_test.test_table',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -502,6 +536,7 @@ my %pgdump_runs = (
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/exclude_measurement.sql",
 			'--exclude-table-and-children' => 'dump_test.measurement',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -511,6 +546,7 @@ my %pgdump_runs = (
 			'--file' => "$tempdir/exclude_measurement_data.sql",
 			'--exclude-table-data-and-children' => 'dump_test.measurement',
 			'--no-unlogged-table-data',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -520,6 +556,7 @@ my %pgdump_runs = (
 			'--file' => "$tempdir/exclude_test_table_data.sql",
 			'--exclude-table-data' => 'dump_test.test_table',
 			'--no-unlogged-table-data',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -538,6 +575,7 @@ my %pgdump_runs = (
 			'--file' => "$tempdir/pg_dumpall_globals.sql",
 			'--globals-only',
 			'--no-sync',
+			'--with-statistics',
 		],
 	},
 	pg_dumpall_globals_clean => {
@@ -547,12 +585,14 @@ my %pgdump_runs = (
 			'--globals-only',
 			'--clean',
 			'--no-sync',
+			'--with-statistics',
 		],
 	},
 	pg_dumpall_dbprivs => {
 		dump_cmd => [
 			'pg_dumpall', '--no-sync',
 			'--file' => "$tempdir/pg_dumpall_dbprivs.sql",
+			'--with-statistics',
 		],
 	},
 	pg_dumpall_exclude => {
@@ -562,6 +602,7 @@ my %pgdump_runs = (
 			'--file' => "$tempdir/pg_dumpall_exclude.sql",
 			'--exclude-database' => '*dump_test*',
 			'--no-sync',
+			'--with-statistics',
 		],
 	},
 	no_toast_compression => {
@@ -569,6 +610,7 @@ my %pgdump_runs = (
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/no_toast_compression.sql",
 			'--no-toast-compression',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -577,6 +619,7 @@ my %pgdump_runs = (
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/no_large_objects.sql",
 			'--no-large-objects',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -585,6 +628,7 @@ my %pgdump_runs = (
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/no_policies.sql",
 			'--no-policies',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -593,6 +637,7 @@ my %pgdump_runs = (
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/no_privs.sql",
 			'--no-privileges',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -601,6 +646,7 @@ my %pgdump_runs = (
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/no_owner.sql",
 			'--no-owner',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -609,6 +655,7 @@ my %pgdump_runs = (
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/no_table_access_method.sql",
 			'--no-table-access-method',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -617,6 +664,7 @@ my %pgdump_runs = (
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/only_dump_test_schema.sql",
 			'--schema' => 'dump_test',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -627,6 +675,7 @@ my %pgdump_runs = (
 			'--table' => 'dump_test.test_table',
 			'--lock-wait-timeout' =>
 			  (1000 * $PostgreSQL::Test::Utils::timeout_default),
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -637,6 +686,7 @@ my %pgdump_runs = (
 			'--table-and-children' => 'dump_test.measurement',
 			'--lock-wait-timeout' =>
 			  (1000 * $PostgreSQL::Test::Utils::timeout_default),
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -646,6 +696,7 @@ my %pgdump_runs = (
 			'--file' => "$tempdir/role.sql",
 			'--role' => 'regress_dump_test_role',
 			'--schema' => 'dump_test_second_schema',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -658,11 +709,13 @@ my %pgdump_runs = (
 			'--file' => "$tempdir/role_parallel",
 			'--role' => 'regress_dump_test_role',
 			'--schema' => 'dump_test_second_schema',
+			'--with-statistics',
 			'postgres',
 		],
 		restore_cmd => [
 			'pg_restore',
 			'--file' => "$tempdir/role_parallel.sql",
+			'--with-statistics',
 			"$tempdir/role_parallel",
 		],
 	},
@@ -691,6 +744,7 @@ my %pgdump_runs = (
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/section_pre_data.sql",
 			'--section' => 'pre-data',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -699,6 +753,7 @@ my %pgdump_runs = (
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/section_data.sql",
 			'--section' => 'data',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -707,6 +762,7 @@ my %pgdump_runs = (
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/section_post_data.sql",
 			'--section' => 'post-data',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -717,6 +773,7 @@ my %pgdump_runs = (
 			'--schema' => 'dump_test',
 			'--large-objects',
 			'--no-large-objects',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -732,6 +789,7 @@ my %pgdump_runs = (
 			'pg_dump', '--no-sync',
 			"--file=$tempdir/no_data_no_schema.sql", '--no-data',
 			'--no-schema', 'postgres',
+			'--with-statistics',
 		],
 	},
 	statistics_only => {
@@ -752,6 +810,7 @@ my %pgdump_runs = (
 		dump_cmd => [
 			'pg_dump', '--no-sync',
 			"--file=$tempdir/no_schema.sql", '--no-schema',
+			'--with-statistics',
 			'postgres',
 		],
 	},);
diff --git a/src/bin/pg_upgrade/dump.c b/src/bin/pg_upgrade/dump.c
index 23cb08e8347..183f08ce1e8 100644
--- a/src/bin/pg_upgrade/dump.c
+++ b/src/bin/pg_upgrade/dump.c
@@ -58,7 +58,7 @@ generate_old_dump(void)
 						   (user_opts.transfer_mode == TRANSFER_MODE_SWAP) ?
 						   "" : "--sequence-data",
 						   log_opts.verbose ? "--verbose" : "",
-						   user_opts.do_statistics ? "" : "--no-statistics",
+						   user_opts.do_statistics ? "--with-statistics" : "--no-statistics",
 						   log_opts.dumpdir,
 						   sql_file_name, escaped_connstr.data);
 
diff --git a/src/bin/pg_upgrade/pg_upgrade.c b/src/bin/pg_upgrade/pg_upgrade.c
index 536e49d2616..81a394f249d 100644
--- a/src/bin/pg_upgrade/pg_upgrade.c
+++ b/src/bin/pg_upgrade/pg_upgrade.c
@@ -618,12 +618,13 @@ create_new_objects(void)
 				  NULL,
 				  true,
 				  true,
-				  "\"%s/pg_restore\" %s %s --exit-on-error --verbose "
+				  "\"%s/pg_restore\" %s %s %s --exit-on-error --verbose "
 				  "--transaction-size=%d "
 				  "--dbname postgres \"%s/%s\"",
 				  new_cluster.bindir,
 				  cluster_conn_opts(&new_cluster),
 				  create_opts,
+				  user_opts.do_statistics ? "--with-statistics" : "--no-statistics",
 				  RESTORE_TRANSACTION_SIZE,
 				  log_opts.dumpdir,
 				  sql_file_name);
@@ -672,12 +673,13 @@ create_new_objects(void)
 
 		parallel_exec_prog(log_file_name,
 						   NULL,
-						   "\"%s/pg_restore\" %s %s --exit-on-error --verbose "
+						   "\"%s/pg_restore\" %s %s %s --exit-on-error --verbose "
 						   "--transaction-size=%d "
 						   "--dbname template1 \"%s/%s\"",
 						   new_cluster.bindir,
 						   cluster_conn_opts(&new_cluster),
 						   create_opts,
+						   user_opts.do_statistics ? "--with-statistics" : "--no-statistics",
 						   txn_size,
 						   log_opts.dumpdir,
 						   sql_file_name);
-- 
2.43.0

#550Hari Krishna Sunder
hari.db.pg@gmail.com
In reply to: Nathan Bossart (#548)
Re: Statistics Import and Export

Thanks for the help. This has unblocked us!

On Thu, May 22, 2025 at 8:25 AM Nathan Bossart <nathandbossart@gmail.com>
wrote:

Show quoted text

On Wed, May 21, 2025 at 04:53:17PM -0700, Jeff Davis wrote:

On Wed, 2025-05-21 at 16:29 -0500, Nathan Bossart wrote:

I don't know precisely where that line might be, but in this case,
the
dumped stats have no hope of restoring into anything older than
v18... But I see no particular benefit from moving the complexity
to the
import side here.

That's fine with me. Perhaps we should just say that pre-18 behavior
differences can be fixed up during export, and post-18 behavior
differences are fixed up during import?

WFM. I've committed the patch.

--
nathan

#551Greg Sabino Mullane
htamfids@gmail.com
In reply to: Jeff Davis (#549)
Re: Statistics Import and Export

On Thu, May 22, 2025 at 2:52 PM Jeff Davis <pgsql@j-davis.com> wrote:

* The default for pg_restore is --no-statistics. That could cause a minor
surprise if the user specifies --with-statistics for pg_dump and
not for pg_restore. An argument could be made that "if the stats are
there, restore them", and I don't have a strong opinion about this point,
but defaulting to --no-statistics seems more consistent with pg_dump.

Hm...somewhat to my own surprise, I don't like this. If it's in the dump,
restore it.

Cheers,
Greg

--
Crunchy Data - https://www.crunchydata.com
Enterprise Postgres Software Products & Tech Support

#552Nathan Bossart
nathandbossart@gmail.com
In reply to: Greg Sabino Mullane (#551)
Re: Statistics Import and Export

On Thu, May 22, 2025 at 03:29:38PM -0400, Greg Sabino Mullane wrote:

On Thu, May 22, 2025 at 2:52 PM Jeff Davis <pgsql@j-davis.com> wrote:

* The default for pg_restore is --no-statistics. That could cause a minor
surprise if the user specifies --with-statistics for pg_dump and
not for pg_restore. An argument could be made that "if the stats are
there, restore them", and I don't have a strong opinion about this point,
but defaulting to --no-statistics seems more consistent with pg_dump.

Hm...somewhat to my own surprise, I don't like this. If it's in the dump,
restore it.

+1, I think defaulting to restoring everything in the dump file is much
less surprising than the alternative.

--
nathan

#553Robert Haas
robertmhaas@gmail.com
In reply to: Nathan Bossart (#552)
Re: Statistics Import and Export

On Thu, May 22, 2025 at 3:36 PM Nathan Bossart <nathandbossart@gmail.com> wrote:

+1, I think defaulting to restoring everything in the dump file is much
less surprising than the alternative.

+1.

--
Robert Haas
EDB: http://www.enterprisedb.com

#554Tom Lane
tgl@sss.pgh.pa.us
In reply to: Greg Sabino Mullane (#551)
Re: Statistics Import and Export

Greg Sabino Mullane <htamfids@gmail.com> writes:

On Thu, May 22, 2025 at 2:52 PM Jeff Davis <pgsql@j-davis.com> wrote:

* The default for pg_restore is --no-statistics. That could cause a minor
surprise if the user specifies --with-statistics for pg_dump and
not for pg_restore.

Hm...somewhat to my own surprise, I don't like this. If it's in the dump,
restore it.

Yeah, I tend to lean that way too. If the user went out of their way
to say --with-statistics for pg_dump, how likely is it that they
don't want the statistics restored?

Another argument pointing in that direction is that the definition
Jeff proposes creates an inconsistency in the output between text
mode:

pg_dump --with-statistics ... | psql

and non-text mode:

pg_dump -Fc --with-statistics ... | pg_restore

There is no additional filter in text mode, so I think pg_restore's
default behavior should also be "no additional filter".

regards, tom lane

#555Jeff Davis
pgsql@j-davis.com
In reply to: Tom Lane (#554)
1 attachment(s)
Re: Statistics Import and Export

On Thu, 2025-05-22 at 15:41 -0400, Tom Lane wrote:

There is no additional filter in text mode, so I think pg_restore's
default behavior should also be "no additional filter".

Attached. Only the defaults for pg_dump and pg_dumpall are changed, and
pg_upgrade explicitly specifies --with-statistics.

Regards,
Jeff Davis

Attachments:

v2-0001-Change-defaults-for-statistics-export.patchtext/x-patch; charset=UTF-8; name=v2-0001-Change-defaults-for-statistics-export.patchDownload
From 5b73253f8848638f1754f4b9da82e90e8814b4b1 Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Thu, 22 May 2025 11:03:03 -0700
Subject: [PATCH v2] Change defaults for statistics export.

Set the default behavior of pg_dump, pg_dumpall, and pg_restore to be
--no-statistics. Leave the default for pg_upgrade to be
--with-statistics.

Discussion: https://postgr.es/m/CA+TgmoZ9=RnWcCOZiKYYjZs_AW1P4QXCw--h4dOLLHuf1Omung@mail.gmail.com
---
 doc/src/sgml/ref/pg_dump.sgml        |  4 +-
 doc/src/sgml/ref/pg_dumpall.sgml     |  4 +-
 src/bin/pg_dump/pg_backup_archiver.c |  2 +-
 src/bin/pg_dump/t/002_pg_dump.pl     | 59 ++++++++++++++++++++++++++++
 src/bin/pg_upgrade/dump.c            |  2 +-
 5 files changed, 65 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index c10bca63e55..995d8f9a040 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -1134,7 +1134,7 @@ PostgreSQL documentation
       <term><option>--no-statistics</option></term>
       <listitem>
        <para>
-        Do not dump statistics.
+        Do not dump statistics. This is the default.
        </para>
       </listitem>
      </varlistentry>
@@ -1461,7 +1461,7 @@ PostgreSQL documentation
       <term><option>--with-statistics</option></term>
       <listitem>
        <para>
-        Dump statistics. This is the default.
+        Dump statistics.
        </para>
       </listitem>
      </varlistentry>
diff --git a/doc/src/sgml/ref/pg_dumpall.sgml b/doc/src/sgml/ref/pg_dumpall.sgml
index 8c5141d036c..81d34df3386 100644
--- a/doc/src/sgml/ref/pg_dumpall.sgml
+++ b/doc/src/sgml/ref/pg_dumpall.sgml
@@ -567,7 +567,7 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       <term><option>--no-statistics</option></term>
       <listitem>
        <para>
-        Do not dump statistics.
+        Do not dump statistics. This is the default.
        </para>
       </listitem>
      </varlistentry>
@@ -741,7 +741,7 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       <term><option>--with-statistics</option></term>
       <listitem>
        <para>
-        Dump statistics. This is the default.
+        Dump statistics.
        </para>
       </listitem>
      </varlistentry>
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index afa42337b11..175fe9c4273 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -152,7 +152,7 @@ InitDumpOptions(DumpOptions *opts)
 	opts->dumpSections = DUMP_UNSECTIONED;
 	opts->dumpSchema = true;
 	opts->dumpData = true;
-	opts->dumpStatistics = true;
+	opts->dumpStatistics = false;
 }
 
 /*
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index cf34f71ea11..386e21e0c59 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -68,6 +68,7 @@ my %pgdump_runs = (
 			'--no-data',
 			'--sequence-data',
 			'--binary-upgrade',
+			'--with-statistics',
 			'--dbname' => 'postgres',    # alternative way to specify database
 		],
 		restore_cmd => [
@@ -75,6 +76,7 @@ my %pgdump_runs = (
 			'--format' => 'custom',
 			'--verbose',
 			'--file' => "$tempdir/binary_upgrade.sql",
+			'--with-statistics',
 			"$tempdir/binary_upgrade.dump",
 		],
 	},
@@ -88,11 +90,13 @@ my %pgdump_runs = (
 			'--format' => 'custom',
 			'--compress' => '1',
 			'--file' => "$tempdir/compression_gzip_custom.dump",
+			'--with-statistics',
 			'postgres',
 		],
 		restore_cmd => [
 			'pg_restore',
 			'--file' => "$tempdir/compression_gzip_custom.sql",
+			'--with-statistics',
 			"$tempdir/compression_gzip_custom.dump",
 		],
 		command_like => {
@@ -115,6 +119,7 @@ my %pgdump_runs = (
 			'--format' => 'directory',
 			'--compress' => 'gzip:1',
 			'--file' => "$tempdir/compression_gzip_dir",
+			'--with-statistics',
 			'postgres',
 		],
 		# Give coverage for manually compressed blobs.toc files during
@@ -132,6 +137,7 @@ my %pgdump_runs = (
 			'pg_restore',
 			'--jobs' => '2',
 			'--file' => "$tempdir/compression_gzip_dir.sql",
+			'--with-statistics',
 			"$tempdir/compression_gzip_dir",
 		],
 	},
@@ -144,6 +150,7 @@ my %pgdump_runs = (
 			'--format' => 'plain',
 			'--compress' => '1',
 			'--file' => "$tempdir/compression_gzip_plain.sql.gz",
+			'--with-statistics',
 			'postgres',
 		],
 		# Decompress the generated file to run through the tests.
@@ -162,11 +169,13 @@ my %pgdump_runs = (
 			'--format' => 'custom',
 			'--compress' => 'lz4',
 			'--file' => "$tempdir/compression_lz4_custom.dump",
+			'--with-statistics',
 			'postgres',
 		],
 		restore_cmd => [
 			'pg_restore',
 			'--file' => "$tempdir/compression_lz4_custom.sql",
+			'--with-statistics',
 			"$tempdir/compression_lz4_custom.dump",
 		],
 		command_like => {
@@ -189,6 +198,7 @@ my %pgdump_runs = (
 			'--format' => 'directory',
 			'--compress' => 'lz4:1',
 			'--file' => "$tempdir/compression_lz4_dir",
+			'--with-statistics',
 			'postgres',
 		],
 		# Verify that data files were compressed
@@ -200,6 +210,7 @@ my %pgdump_runs = (
 			'pg_restore',
 			'--jobs' => '2',
 			'--file' => "$tempdir/compression_lz4_dir.sql",
+			'--with-statistics',
 			"$tempdir/compression_lz4_dir",
 		],
 	},
@@ -212,6 +223,7 @@ my %pgdump_runs = (
 			'--format' => 'plain',
 			'--compress' => 'lz4',
 			'--file' => "$tempdir/compression_lz4_plain.sql.lz4",
+			'--with-statistics',
 			'postgres',
 		],
 		# Decompress the generated file to run through the tests.
@@ -233,11 +245,13 @@ my %pgdump_runs = (
 			'--format' => 'custom',
 			'--compress' => 'zstd',
 			'--file' => "$tempdir/compression_zstd_custom.dump",
+			'--with-statistics',
 			'postgres',
 		],
 		restore_cmd => [
 			'pg_restore',
 			'--file' => "$tempdir/compression_zstd_custom.sql",
+			'--with-statistics',
 			"$tempdir/compression_zstd_custom.dump",
 		],
 		command_like => {
@@ -259,6 +273,7 @@ my %pgdump_runs = (
 			'--format' => 'directory',
 			'--compress' => 'zstd:1',
 			'--file' => "$tempdir/compression_zstd_dir",
+			'--with-statistics',
 			'postgres',
 		],
 		# Give coverage for manually compressed blobs.toc files during
@@ -279,6 +294,7 @@ my %pgdump_runs = (
 			'pg_restore',
 			'--jobs' => '2',
 			'--file' => "$tempdir/compression_zstd_dir.sql",
+			'--with-statistics',
 			"$tempdir/compression_zstd_dir",
 		],
 	},
@@ -292,6 +308,7 @@ my %pgdump_runs = (
 			'--format' => 'plain',
 			'--compress' => 'zstd:long',
 			'--file' => "$tempdir/compression_zstd_plain.sql.zst",
+			'--with-statistics',
 			'postgres',
 		],
 		# Decompress the generated file to run through the tests.
@@ -310,6 +327,7 @@ my %pgdump_runs = (
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/clean.sql",
 			'--clean',
+			'--with-statistics',
 			'--dbname' => 'postgres',    # alternative way to specify database
 		],
 	},
@@ -320,6 +338,7 @@ my %pgdump_runs = (
 			'--clean',
 			'--if-exists',
 			'--encoding' => 'UTF8',      # no-op, just for testing
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -338,6 +357,7 @@ my %pgdump_runs = (
 			'--create',
 			'--no-reconnect',    # no-op, just for testing
 			'--verbose',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -356,6 +376,7 @@ my %pgdump_runs = (
 		dump_cmd => [
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/defaults.sql",
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -364,6 +385,7 @@ my %pgdump_runs = (
 		dump_cmd => [
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/defaults_no_public.sql",
+			'--with-statistics',
 			'regress_pg_dump_test',
 		],
 	},
@@ -373,6 +395,7 @@ my %pgdump_runs = (
 			'pg_dump', '--no-sync',
 			'--clean',
 			'--file' => "$tempdir/defaults_no_public_clean.sql",
+			'--with-statistics',
 			'regress_pg_dump_test',
 		],
 	},
@@ -381,6 +404,7 @@ my %pgdump_runs = (
 		dump_cmd => [
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/defaults_public_owner.sql",
+			'--with-statistics',
 			'regress_public_owner',
 		],
 	},
@@ -395,12 +419,14 @@ my %pgdump_runs = (
 			'pg_dump',
 			'--format' => 'custom',
 			'--file' => "$tempdir/defaults_custom_format.dump",
+			'--with-statistics',
 			'postgres',
 		],
 		restore_cmd => [
 			'pg_restore',
 			'--format' => 'custom',
 			'--file' => "$tempdir/defaults_custom_format.sql",
+			'--with-statistics',
 			"$tempdir/defaults_custom_format.dump",
 		],
 		command_like => {
@@ -425,12 +451,14 @@ my %pgdump_runs = (
 			'pg_dump',
 			'--format' => 'directory',
 			'--file' => "$tempdir/defaults_dir_format",
+			'--with-statistics',
 			'postgres',
 		],
 		restore_cmd => [
 			'pg_restore',
 			'--format' => 'directory',
 			'--file' => "$tempdir/defaults_dir_format.sql",
+			'--with-statistics',
 			"$tempdir/defaults_dir_format",
 		],
 		command_like => {
@@ -456,11 +484,13 @@ my %pgdump_runs = (
 			'--format' => 'directory',
 			'--jobs' => 2,
 			'--file' => "$tempdir/defaults_parallel",
+			'--with-statistics',
 			'postgres',
 		],
 		restore_cmd => [
 			'pg_restore',
 			'--file' => "$tempdir/defaults_parallel.sql",
+			'--with-statistics',
 			"$tempdir/defaults_parallel",
 		],
 	},
@@ -472,12 +502,14 @@ my %pgdump_runs = (
 			'pg_dump',
 			'--format' => 'tar',
 			'--file' => "$tempdir/defaults_tar_format.tar",
+			'--with-statistics',
 			'postgres',
 		],
 		restore_cmd => [
 			'pg_restore',
 			'--format' => 'tar',
 			'--file' => "$tempdir/defaults_tar_format.sql",
+			'--with-statistics',
 			"$tempdir/defaults_tar_format.tar",
 		],
 	},
@@ -486,6 +518,7 @@ my %pgdump_runs = (
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/exclude_dump_test_schema.sql",
 			'--exclude-schema' => 'dump_test',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -494,6 +527,7 @@ my %pgdump_runs = (
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/exclude_test_table.sql",
 			'--exclude-table' => 'dump_test.test_table',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -502,6 +536,7 @@ my %pgdump_runs = (
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/exclude_measurement.sql",
 			'--exclude-table-and-children' => 'dump_test.measurement',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -511,6 +546,7 @@ my %pgdump_runs = (
 			'--file' => "$tempdir/exclude_measurement_data.sql",
 			'--exclude-table-data-and-children' => 'dump_test.measurement',
 			'--no-unlogged-table-data',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -520,6 +556,7 @@ my %pgdump_runs = (
 			'--file' => "$tempdir/exclude_test_table_data.sql",
 			'--exclude-table-data' => 'dump_test.test_table',
 			'--no-unlogged-table-data',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -538,6 +575,7 @@ my %pgdump_runs = (
 			'--file' => "$tempdir/pg_dumpall_globals.sql",
 			'--globals-only',
 			'--no-sync',
+			'--with-statistics',
 		],
 	},
 	pg_dumpall_globals_clean => {
@@ -547,12 +585,14 @@ my %pgdump_runs = (
 			'--globals-only',
 			'--clean',
 			'--no-sync',
+			'--with-statistics',
 		],
 	},
 	pg_dumpall_dbprivs => {
 		dump_cmd => [
 			'pg_dumpall', '--no-sync',
 			'--file' => "$tempdir/pg_dumpall_dbprivs.sql",
+			'--with-statistics',
 		],
 	},
 	pg_dumpall_exclude => {
@@ -562,6 +602,7 @@ my %pgdump_runs = (
 			'--file' => "$tempdir/pg_dumpall_exclude.sql",
 			'--exclude-database' => '*dump_test*',
 			'--no-sync',
+			'--with-statistics',
 		],
 	},
 	no_toast_compression => {
@@ -569,6 +610,7 @@ my %pgdump_runs = (
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/no_toast_compression.sql",
 			'--no-toast-compression',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -577,6 +619,7 @@ my %pgdump_runs = (
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/no_large_objects.sql",
 			'--no-large-objects',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -585,6 +628,7 @@ my %pgdump_runs = (
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/no_policies.sql",
 			'--no-policies',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -593,6 +637,7 @@ my %pgdump_runs = (
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/no_privs.sql",
 			'--no-privileges',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -601,6 +646,7 @@ my %pgdump_runs = (
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/no_owner.sql",
 			'--no-owner',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -609,6 +655,7 @@ my %pgdump_runs = (
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/no_table_access_method.sql",
 			'--no-table-access-method',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -617,6 +664,7 @@ my %pgdump_runs = (
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/only_dump_test_schema.sql",
 			'--schema' => 'dump_test',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -627,6 +675,7 @@ my %pgdump_runs = (
 			'--table' => 'dump_test.test_table',
 			'--lock-wait-timeout' =>
 			  (1000 * $PostgreSQL::Test::Utils::timeout_default),
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -637,6 +686,7 @@ my %pgdump_runs = (
 			'--table-and-children' => 'dump_test.measurement',
 			'--lock-wait-timeout' =>
 			  (1000 * $PostgreSQL::Test::Utils::timeout_default),
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -646,6 +696,7 @@ my %pgdump_runs = (
 			'--file' => "$tempdir/role.sql",
 			'--role' => 'regress_dump_test_role',
 			'--schema' => 'dump_test_second_schema',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -658,11 +709,13 @@ my %pgdump_runs = (
 			'--file' => "$tempdir/role_parallel",
 			'--role' => 'regress_dump_test_role',
 			'--schema' => 'dump_test_second_schema',
+			'--with-statistics',
 			'postgres',
 		],
 		restore_cmd => [
 			'pg_restore',
 			'--file' => "$tempdir/role_parallel.sql",
+			'--with-statistics',
 			"$tempdir/role_parallel",
 		],
 	},
@@ -691,6 +744,7 @@ my %pgdump_runs = (
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/section_pre_data.sql",
 			'--section' => 'pre-data',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -699,6 +753,7 @@ my %pgdump_runs = (
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/section_data.sql",
 			'--section' => 'data',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -707,6 +762,7 @@ my %pgdump_runs = (
 			'pg_dump', '--no-sync',
 			'--file' => "$tempdir/section_post_data.sql",
 			'--section' => 'post-data',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -717,6 +773,7 @@ my %pgdump_runs = (
 			'--schema' => 'dump_test',
 			'--large-objects',
 			'--no-large-objects',
+			'--with-statistics',
 			'postgres',
 		],
 	},
@@ -732,6 +789,7 @@ my %pgdump_runs = (
 			'pg_dump', '--no-sync',
 			"--file=$tempdir/no_data_no_schema.sql", '--no-data',
 			'--no-schema', 'postgres',
+			'--with-statistics',
 		],
 	},
 	statistics_only => {
@@ -752,6 +810,7 @@ my %pgdump_runs = (
 		dump_cmd => [
 			'pg_dump', '--no-sync',
 			"--file=$tempdir/no_schema.sql", '--no-schema',
+			'--with-statistics',
 			'postgres',
 		],
 	},);
diff --git a/src/bin/pg_upgrade/dump.c b/src/bin/pg_upgrade/dump.c
index 23cb08e8347..183f08ce1e8 100644
--- a/src/bin/pg_upgrade/dump.c
+++ b/src/bin/pg_upgrade/dump.c
@@ -58,7 +58,7 @@ generate_old_dump(void)
 						   (user_opts.transfer_mode == TRANSFER_MODE_SWAP) ?
 						   "" : "--sequence-data",
 						   log_opts.verbose ? "--verbose" : "",
-						   user_opts.do_statistics ? "" : "--no-statistics",
+						   user_opts.do_statistics ? "--with-statistics" : "--no-statistics",
 						   log_opts.dumpdir,
 						   sql_file_name, escaped_connstr.data);
 
-- 
2.43.0